Compare commits
357 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 | |||
| dea184e9ec | |||
| e119bf9040 | |||
| c7f982e190 | |||
| 2e2dd628c1 | |||
| 5ac5a8a799 | |||
| 7d75ab417a | |||
| 3ca81e409a |
+28
-12
@@ -1,6 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: ["matrix-org", "import", "jsdoc", "n"],
|
plugins: ["matrix-org", "import", "jsdoc", "n", "@vitest"],
|
||||||
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/jest", "plugin:import/typescript"],
|
extends: ["plugin:matrix-org/babel", "plugin:import/typescript"],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
project: ["./tsconfig.json"],
|
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: [
|
overrides: [
|
||||||
{
|
{
|
||||||
@@ -147,11 +137,37 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ["spec/**/*.ts"],
|
files: ["spec/**/*.ts"],
|
||||||
|
extends: ["plugin:@vitest/legacy-recommended"],
|
||||||
rules: {
|
rules: {
|
||||||
// We don't need super strict typing in test utilities
|
// We don't need super strict typing in test utilities
|
||||||
"@typescript-eslint/explicit-function-return-type": "off",
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
"@typescript-eslint/explicit-member-accessibility": "off",
|
"@typescript-eslint/explicit-member-accessibility": "off",
|
||||||
"@typescript-eslint/no-empty-object-type": "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
|
* @matrix-org/element-web-reviewers
|
||||||
/.github/workflows/** @matrix-org/element-web-team
|
/.github/workflows/** @matrix-org/element-web-team
|
||||||
/package.json @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
|
/scripts/** @matrix-org/element-web-team
|
||||||
/src/webrtc @matrix-org/element-call-reviewers
|
/src/webrtc @matrix-org/element-call-reviewers
|
||||||
/src/matrixrtc @matrix-org/element-call-reviewers
|
/src/matrixrtc @matrix-org/element-call-reviewers
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ runs:
|
|||||||
|
|
||||||
- name: Upload tarball signature
|
- name: Upload tarball signature
|
||||||
if: ${{ inputs.upload-url }}
|
if: ${{ inputs.upload-url }}
|
||||||
uses: shogo82148/actions-upload-release-asset@610b1987249a69a79de9565777e112fb38f22436 # v1
|
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ inputs.upload-url }}
|
upload_url: ${{ inputs.upload-url }}
|
||||||
asset_path: ${{ env.VERSION }}.tar.gz.asc
|
asset_path: ${{ env.VERSION }}.tar.gz.asc
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ runs:
|
|||||||
|
|
||||||
- name: Upload asset signatures
|
- name: Upload asset signatures
|
||||||
if: inputs.gpg-fingerprint
|
if: inputs.gpg-fingerprint
|
||||||
uses: shogo82148/actions-upload-release-asset@610b1987249a69a79de9565777e112fb38f22436 # v1
|
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ inputs.upload-url }}
|
upload_url: ${{ inputs.upload-url }}
|
||||||
asset_path: ${{ inputs.asset-path }}.asc
|
asset_path: ${{ inputs.asset-path }}.asc
|
||||||
|
|
||||||
- name: Upload assets
|
- name: Upload assets
|
||||||
uses: shogo82148/actions-upload-release-asset@610b1987249a69a79de9565777e112fb38f22436 # v1
|
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ inputs.upload-url }}
|
upload_url: ${{ inputs.upload-url }}
|
||||||
asset_path: ${{ inputs.asset-path }}
|
asset_path: ${{ inputs.asset-path }}
|
||||||
|
|||||||
@@ -41,3 +41,6 @@
|
|||||||
- name: "Z-Flaky-Test"
|
- name: "Z-Flaky-Test"
|
||||||
description: "A test is raising false alarms"
|
description: "A test is raising false alarms"
|
||||||
color: "ededed"
|
color: "ededed"
|
||||||
|
- name: "Z-Skip-Coverage"
|
||||||
|
description: "Skip SonarQube coverage for this PR"
|
||||||
|
color: "ededed"
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
name: Backport
|
name: Backport
|
||||||
on:
|
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:
|
types:
|
||||||
- closed
|
- closed
|
||||||
- labeled
|
- labeled
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
name: Deploy documentation PR preview
|
name: Deploy documentation PR preview
|
||||||
|
|
||||||
on:
|
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"]
|
workflows: ["Static Analysis"]
|
||||||
types:
|
types:
|
||||||
- completed
|
- completed
|
||||||
@@ -15,7 +17,7 @@ jobs:
|
|||||||
deployments: write
|
deployments: write
|
||||||
steps:
|
steps:
|
||||||
- name: 📥 Download artifact
|
- name: 📥 Download artifact
|
||||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Triggers after the "Downstream artifacts" build has finished, to run the
|
# 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:
|
on:
|
||||||
merge_group:
|
merge_group:
|
||||||
types: [checks_requested]
|
types: [checks_requested]
|
||||||
@@ -21,11 +21,12 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
playwright:
|
playwright:
|
||||||
name: 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:
|
permissions:
|
||||||
actions: read
|
actions: read
|
||||||
issues: read
|
issues: read
|
||||||
pull-requests: read
|
pull-requests: read
|
||||||
|
contents: read
|
||||||
with:
|
with:
|
||||||
matrix-js-sdk-sha: ${{ github.sha }}
|
matrix-js-sdk-sha: ${{ github.sha }}
|
||||||
# We only want to run the playwright tests on merge queue to prevent regressions
|
# We only want to run the playwright tests on merge queue to prevent regressions
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ jobs:
|
|||||||
|
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
|
- name: Notify element-web repo that a new SDK build is on develop so it can CI against it
|
||||||
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3
|
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
repository: ${{ matrix.repo }}
|
repository: ${{ matrix.repo }}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
name: Pull Request
|
name: Pull Request
|
||||||
on:
|
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]
|
types: [opened, edited, labeled, unlabeled, synchronize]
|
||||||
merge_group:
|
merge_group:
|
||||||
types: [checks_requested]
|
types: [checks_requested]
|
||||||
@@ -15,7 +18,7 @@ jobs:
|
|||||||
name: Preview Changelog
|
name: Preview Changelog
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: mheap/github-action-required-labels@8afbe8ae6ab7647d0c9f0cfa7c2f939650d22509 # v5
|
- uses: mheap/github-action-required-labels@0ac283b4e65c1fb28ce6079dea5546ceca98ccbe # v5
|
||||||
if: github.event_name != 'merge_group'
|
if: github.event_name != 'merge_group'
|
||||||
with:
|
with:
|
||||||
labels: |
|
labels: |
|
||||||
@@ -35,7 +38,7 @@ jobs:
|
|||||||
pull-requests: read
|
pull-requests: read
|
||||||
steps:
|
steps:
|
||||||
- name: Add notice
|
- 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')
|
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
@@ -49,7 +52,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Check membership
|
- 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
|
uses: tspascoal/get-user-teams-membership@57e9f42acd78f4d0f496b3be4368fc5f62696662 # v3
|
||||||
id: teams
|
id: teams
|
||||||
with:
|
with:
|
||||||
@@ -60,7 +63,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Add label
|
- name: Add label
|
||||||
if: steps.teams.outputs.isTeamMember == 'false'
|
if: steps.teams.outputs.isTeamMember == 'false'
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
github.rest.issues.addLabels({
|
github.rest.issues.addLabels({
|
||||||
@@ -73,13 +76,15 @@ jobs:
|
|||||||
close-if-fork-develop:
|
close-if-fork-develop:
|
||||||
name: Forbid develop branch fork contributions
|
name: Forbid develop branch fork contributions
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
if: >
|
if: >
|
||||||
github.event.action == 'opened' &&
|
github.event.action == 'opened' &&
|
||||||
github.event.pull_request.head.ref == 'develop' &&
|
github.event.pull_request.head.ref == 'develop' &&
|
||||||
github.event.pull_request.head.repo.full_name != github.repository
|
github.event.pull_request.head.repo.full_name != github.repository
|
||||||
steps:
|
steps:
|
||||||
- name: Close pull request
|
- name: Close pull request
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
github.rest.issues.createComment({
|
github.rest.issues.createComment({
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Check for X-Release-Blocker label on any open issues or PRs
|
- 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:
|
env:
|
||||||
REPO: ${{ inputs.repository }}
|
REPO: ${{ inputs.repository }}
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -16,18 +16,20 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: 🧮 Checkout code
|
- name: 🧮 Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
with:
|
with:
|
||||||
ref: staging
|
ref: staging
|
||||||
fetch-depth: 0
|
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:
|
with:
|
||||||
node-version-file: package.json
|
node-version-file: package.json
|
||||||
cache: "yarn"
|
cache: "pnpm"
|
||||||
|
|
||||||
- name: Install Deps
|
- name: Install Deps
|
||||||
run: "yarn install --frozen-lockfile"
|
run: "pnpm install --frozen-lockfile"
|
||||||
|
|
||||||
- uses: t3chguy/release-drafter@105e541c2c3d857f032bd522c0764694758fabad
|
- uses: t3chguy/release-drafter@105e541c2c3d857f032bd522c0764694758fabad
|
||||||
id: draft-release
|
id: draft-release
|
||||||
@@ -37,7 +39,7 @@ jobs:
|
|||||||
disable-autolabeler: true
|
disable-autolabeler: true
|
||||||
|
|
||||||
- name: Get actions scripts
|
- name: Get actions scripts
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
with:
|
with:
|
||||||
repository: matrix-org/matrix-js-sdk
|
repository: matrix-org/matrix-js-sdk
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -48,7 +50,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Ingest upstream changes
|
- name: Ingest upstream changes
|
||||||
if: inputs.include-changes
|
if: inputs.include-changes
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
||||||
|
|||||||
@@ -13,4 +13,4 @@ jobs:
|
|||||||
draft:
|
draft:
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
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.
|
description: List of dependencies to reset.
|
||||||
type: string
|
type: string
|
||||||
required: false
|
required: false
|
||||||
|
dir:
|
||||||
|
description: The directory to release
|
||||||
|
type: string
|
||||||
|
default: "."
|
||||||
concurrency: ${{ github.workflow }}
|
concurrency: ${{ github.workflow }}
|
||||||
permissions: {} # Uses ELEMENT_BOT_TOKEN
|
permissions: {} # Uses ELEMENT_BOT_TOKEN
|
||||||
jobs:
|
jobs:
|
||||||
merge:
|
merge:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
with:
|
with:
|
||||||
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
|
# 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 }}
|
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
persist-credentials: true
|
||||||
|
|
||||||
- name: Get actions scripts
|
- name: Get actions scripts
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
with:
|
with:
|
||||||
repository: matrix-org/matrix-js-sdk
|
repository: matrix-org/matrix-js-sdk
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -33,13 +38,14 @@ jobs:
|
|||||||
sparse-checkout: |
|
sparse-checkout: |
|
||||||
scripts/release
|
scripts/release
|
||||||
|
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||||
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "pnpm"
|
||||||
node-version-file: package.json
|
node-version-file: package.json
|
||||||
|
|
||||||
- name: Install Deps
|
- name: Install Deps
|
||||||
run: "yarn install --frozen-lockfile"
|
run: "pnpm install --frozen-lockfile"
|
||||||
|
|
||||||
- name: Set up git
|
- name: Set up git
|
||||||
run: |
|
run: |
|
||||||
@@ -53,6 +59,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Reset dependencies
|
- name: Reset dependencies
|
||||||
if: inputs.dependencies
|
if: inputs.dependencies
|
||||||
|
working-directory: ${{ inputs.dir }}
|
||||||
run: |
|
run: |
|
||||||
while IFS= read -r PACKAGE; do
|
while IFS= read -r PACKAGE; do
|
||||||
[ -z "$PACKAGE" ] && continue
|
[ -z "$PACKAGE" ] && continue
|
||||||
@@ -73,7 +80,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Resetting $PACKAGE to develop branch..."
|
echo "Resetting $PACKAGE to develop branch..."
|
||||||
yarn add "github:matrix-org/$PACKAGE#develop"
|
pnpm add "github:matrix-org/$PACKAGE#develop"
|
||||||
git add -u
|
git add -u
|
||||||
git commit -m "Reset $PACKAGE back to develop branch"
|
git commit -m "Reset $PACKAGE back to develop branch"
|
||||||
done <<< "$DEPENDENCIES"
|
done <<< "$DEPENDENCIES"
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ on:
|
|||||||
secrets:
|
secrets:
|
||||||
ELEMENT_BOT_TOKEN:
|
ELEMENT_BOT_TOKEN:
|
||||||
required: true
|
required: true
|
||||||
NPM_TOKEN:
|
|
||||||
required: false
|
|
||||||
GPG_PASSPHRASE:
|
GPG_PASSPHRASE:
|
||||||
required: false
|
required: false
|
||||||
GPG_PRIVATE_KEY:
|
GPG_PRIVATE_KEY:
|
||||||
@@ -28,12 +26,21 @@ on:
|
|||||||
description: |
|
description: |
|
||||||
The path to the asset you want to upload, if any. You can use glob patterns here.
|
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.
|
Will be GPG signed and an `.asc` file included in the release artifacts if `gpg-fingerprint` is set.
|
||||||
|
Relative to `dir`.
|
||||||
type: string
|
type: string
|
||||||
required: false
|
required: false
|
||||||
expected-asset-count:
|
expected-asset-count:
|
||||||
description: The number of expected assets, including signatures, excluding generated zip & tarball.
|
description: The number of expected assets, including signatures, excluding generated zip & tarball.
|
||||||
type: number
|
type: number
|
||||||
required: false
|
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:
|
outputs:
|
||||||
npm-id:
|
npm-id:
|
||||||
description: "The npm package@version string we published"
|
description: "The npm package@version string we published"
|
||||||
@@ -45,7 +52,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
issues: read
|
issues: read
|
||||||
pull-requests: 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:
|
release:
|
||||||
name: Release
|
name: Release
|
||||||
@@ -58,7 +65,7 @@ jobs:
|
|||||||
- name: Load GPG key
|
- name: Load GPG key
|
||||||
id: gpg
|
id: gpg
|
||||||
if: inputs.gpg-fingerprint
|
if: inputs.gpg-fingerprint
|
||||||
uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6
|
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7
|
||||||
with:
|
with:
|
||||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||||
@@ -66,22 +73,23 @@ jobs:
|
|||||||
|
|
||||||
- name: Get draft release
|
- name: Get draft release
|
||||||
id: draft-release
|
id: draft-release
|
||||||
uses: cardinalby/git-get-release-action@5172c3a026600b1d459b117738c605fabc9e4e44 # v1
|
uses: cardinalby/git-get-release-action@5172c3a026600b1d459b117738c605fabc9e4e44 # 1.2.5
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
latest: true
|
latest: true
|
||||||
|
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
with:
|
with:
|
||||||
ref: staging
|
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
|
# 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 }}
|
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
persist-credentials: true
|
||||||
|
|
||||||
- name: Get actions scripts
|
- name: Get actions scripts
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
with:
|
with:
|
||||||
repository: matrix-org/matrix-js-sdk
|
repository: matrix-org/matrix-js-sdk
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -92,6 +100,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Prepare variables
|
- name: Prepare variables
|
||||||
id: prepare
|
id: prepare
|
||||||
|
working-directory: ${{ inputs.dist-dir }}
|
||||||
run: |
|
run: |
|
||||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||||
|
|
||||||
@@ -106,7 +115,7 @@ jobs:
|
|||||||
run: echo "VERSION=$(echo $VERSION | cut -d- -f1)" >> $GITHUB_ENV
|
run: echo "VERSION=$(echo $VERSION | cut -d- -f1)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Check version number not in use
|
- name: Check version number not in use
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const { VERSION } = process.env;
|
const { VERSION } = process.env;
|
||||||
@@ -125,15 +134,17 @@ jobs:
|
|||||||
git config --global user.email "releases@riot.im"
|
git config --global user.email "releases@riot.im"
|
||||||
git config --global user.name "RiotRobot"
|
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:
|
with:
|
||||||
cache: "yarn"
|
cache: "pnpm"
|
||||||
node-version-file: package.json
|
node-version-file: ${{ inputs.dist-dir }}/package.json
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: "yarn install --frozen-lockfile"
|
run: "pnpm install --frozen-lockfile"
|
||||||
|
|
||||||
- name: Handle develop dependencies
|
- name: Handle develop dependencies
|
||||||
|
working-directory: ${{ inputs.dist-dir }}
|
||||||
run: |
|
run: |
|
||||||
ret=0
|
ret=0
|
||||||
cat package.json | jq -r '.dependencies | to_entries | .[] | "\(.key) \(.value)"' | grep '#develop$' | while read -r dep ; do
|
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]}
|
VERSION=${dep[1]}
|
||||||
|
|
||||||
echo "::warning title=Develop dependency found::$DEPENDENCY will be kept at $VERSION"
|
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 add -u
|
||||||
git commit -m "Keep $PACKAGE at $VERSION"
|
git commit -m "Keep $PACKAGE at $VERSION"
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Bump package.json version
|
- name: Bump package.json versions
|
||||||
run: |
|
run: |
|
||||||
yarn version --no-git-tag-version --new-version "${VERSION#v}"
|
for DIR in $DIRS; do
|
||||||
git add package.json
|
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
|
- name: Add to CHANGELOG.md
|
||||||
if: inputs.final
|
if: inputs.final
|
||||||
@@ -177,7 +192,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Build assets
|
- name: Build assets
|
||||||
if: steps.prepare.outputs.has-dist-script == '1'
|
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
|
- name: Upload release assets & signatures
|
||||||
if: inputs.asset-path
|
if: inputs.asset-path
|
||||||
@@ -185,7 +201,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
gpg-fingerprint: ${{ inputs.gpg-fingerprint }}
|
gpg-fingerprint: ${{ inputs.gpg-fingerprint }}
|
||||||
upload-url: ${{ steps.draft-release.outputs.upload_url }}
|
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
|
- name: Create signed tag
|
||||||
if: inputs.gpg-fingerprint
|
if: inputs.gpg-fingerprint
|
||||||
@@ -218,7 +234,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Validate release has expected assets
|
- name: Validate release has expected assets
|
||||||
if: inputs.expected-asset-count
|
if: inputs.expected-asset-count
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
env:
|
env:
|
||||||
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
||||||
EXPECTED_ASSET_COUNT: ${{ inputs.expected-asset-count }}
|
EXPECTED_ASSET_COUNT: ${{ inputs.expected-asset-count }}
|
||||||
@@ -246,7 +262,7 @@ jobs:
|
|||||||
git push origin master
|
git push origin master
|
||||||
|
|
||||||
- name: Publish release
|
- name: Publish release
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
env:
|
env:
|
||||||
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
||||||
FINAL: ${{ inputs.final }}
|
FINAL: ${{ inputs.final }}
|
||||||
@@ -278,12 +294,12 @@ jobs:
|
|||||||
name: Publish to npm
|
name: Publish to npm
|
||||||
needs: release
|
needs: release
|
||||||
if: inputs.npm
|
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:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
id-token: write
|
id-token: write
|
||||||
secrets:
|
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
|
|
||||||
post-release:
|
post-release:
|
||||||
name: Post release steps
|
name: Post release steps
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
name: Publish to npm
|
name: Publish to npm
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
secrets:
|
inputs:
|
||||||
NPM_TOKEN:
|
dir:
|
||||||
required: true
|
description: The directory to release
|
||||||
|
type: string
|
||||||
|
default: "."
|
||||||
outputs:
|
outputs:
|
||||||
id:
|
id:
|
||||||
description: "The npm package@version string we published"
|
description: "The npm package@version string we published"
|
||||||
@@ -20,32 +22,32 @@ jobs:
|
|||||||
id: ${{ steps.npm-publish.outputs.id }}
|
id: ${{ steps.npm-publish.outputs.id }}
|
||||||
steps:
|
steps:
|
||||||
- name: 🧮 Checkout code
|
- name: 🧮 Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
with:
|
with:
|
||||||
ref: staging
|
ref: staging
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: 🔧 Yarn cache
|
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
- name: 🔧 pnpm cache
|
||||||
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "pnpm"
|
||||||
registry-url: "https://registry.npmjs.org"
|
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
|
- name: 🔨 Install dependencies
|
||||||
run: "yarn install --frozen-lockfile"
|
run: "pnpm install --frozen-lockfile"
|
||||||
|
|
||||||
- name: 🚀 Publish to npm
|
- name: 🚀 Publish to npm
|
||||||
id: npm-publish
|
id: npm-publish
|
||||||
|
working-directory: ${{ inputs.dir }}
|
||||||
run: |
|
run: |
|
||||||
npm publish --provenance --access public --tag next
|
npm publish --provenance --access public --tag "$TAG"
|
||||||
release=$(jq -r '"\(.name)@\(.version)"' package.json)
|
release=$(jq -r '"\(.name)@\(.version)"' package.json)
|
||||||
echo "id=$release" >> $GITHUB_OUTPUT
|
echo "id=$release" >> $GITHUB_OUTPUT
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
TAG: ${{ contains(steps.npm-publish.outputs.id, '-rc.') && 'next' || 'latest' }}
|
||||||
|
|
||||||
- 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 }}
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ concurrency: ${{ github.workflow }}
|
|||||||
permissions: {} # No permissions required
|
permissions: {} # No permissions required
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
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:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
issues: write
|
issues: write
|
||||||
@@ -41,28 +41,32 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
repo:
|
include:
|
||||||
- element-hq/element-web
|
- repo: element-hq/element-web
|
||||||
|
path: apps/web
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
with:
|
with:
|
||||||
repository: ${{ matrix.repo }}
|
repository: ${{ matrix.repo }}
|
||||||
ref: staging
|
ref: staging
|
||||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
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:
|
with:
|
||||||
cache: "yarn"
|
cache: "pnpm"
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
|
|
||||||
- name: Bump dependency
|
- name: Bump dependency
|
||||||
env:
|
env:
|
||||||
DEPENDENCY: ${{ needs.release.outputs.npm-id }}
|
DEPENDENCY: ${{ needs.release.outputs.npm-id }}
|
||||||
|
DIR: ${{ matrix.path }}
|
||||||
run: |
|
run: |
|
||||||
git config --global user.email "releases@riot.im"
|
git config --global user.email "releases@riot.im"
|
||||||
git config --global user.name "RiotRobot"
|
git config --global user.name "RiotRobot"
|
||||||
yarn upgrade "$DEPENDENCY" --exact
|
pnpm add -C "$DIR" "$DEPENDENCY" --save-exact
|
||||||
git add package.json yarn.lock
|
git add "$DIR"/package.json pnpm-lock.yaml
|
||||||
git commit -am"Upgrade dependency to $DEPENDENCY"
|
git commit -am"Upgrade dependency to $DEPENDENCY"
|
||||||
git push origin staging
|
git push origin staging
|
||||||
|
|
||||||
@@ -73,22 +77,25 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: 🧮 Checkout code
|
- name: 🧮 Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: 🔧 Yarn cache
|
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
||||||
with:
|
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
|
node-version-file: package.json
|
||||||
|
|
||||||
- name: 🔨 Install dependencies
|
- name: 🔨 Install dependencies
|
||||||
run: "yarn install --frozen-lockfile"
|
run: "pnpm install --frozen-lockfile"
|
||||||
|
|
||||||
- name: 📖 Generate docs
|
- name: 📖 Generate docs
|
||||||
run: yarn gendoc
|
run: pnpm gendoc
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
|
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
|
||||||
with:
|
with:
|
||||||
path: _docs
|
path: _docs
|
||||||
|
|
||||||
@@ -106,4 +113,4 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deployment
|
id: deployment
|
||||||
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
|
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ on:
|
|||||||
sharded:
|
sharded:
|
||||||
type: boolean
|
type: boolean
|
||||||
required: false
|
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: {}
|
permissions: {}
|
||||||
jobs:
|
jobs:
|
||||||
sonarqube:
|
sonarqube:
|
||||||
@@ -27,7 +31,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# We create the status here and then update it to success/failure in the `report` stage
|
# 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.
|
# This provides an easy link to this workflow_run from the PR before Sonarcloud is done.
|
||||||
- uses: guibranco/github-status-action-v2@741ea90ba6c3ca76fe0d43ba11a90cda97d5e685
|
- uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
|
||||||
with:
|
with:
|
||||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
state: pending
|
state: pending
|
||||||
@@ -36,14 +40,15 @@ jobs:
|
|||||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||||
|
|
||||||
- name: "🧮 Checkout code"
|
- name: "🧮 Checkout code"
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
with:
|
with:
|
||||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||||
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow
|
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
|
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: 📥 Download artifact
|
- name: 📥 Download artifact
|
||||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||||
if: ${{ !inputs.sharded }}
|
if: ${{ !inputs.sharded }}
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -51,14 +56,13 @@ jobs:
|
|||||||
name: coverage
|
name: coverage
|
||||||
path: coverage
|
path: coverage
|
||||||
- name: 📥 Download sharded artifacts
|
- name: 📥 Download sharded artifacts
|
||||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||||
if: inputs.sharded
|
if: inputs.sharded
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
path: coverage
|
path: coverage
|
||||||
merge-multiple: true
|
|
||||||
- name: Check coverage artifact
|
- name: Check coverage artifact
|
||||||
run: |
|
run: |
|
||||||
if [ ! -d coverage ]; then
|
if [ ! -d coverage ]; then
|
||||||
@@ -75,19 +79,20 @@ jobs:
|
|||||||
|
|
||||||
- name: "🩻 SonarCloud Scan"
|
- name: "🩻 SonarCloud Scan"
|
||||||
id: sonarcloud
|
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
|
# 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' }}
|
continue-on-error: ${{ github.event.workflow_run.head_branch != 'develop' }}
|
||||||
with:
|
with:
|
||||||
skip_checkout: true
|
skip_checkout: true
|
||||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||||
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
|
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 }}
|
branch: ${{ github.event.workflow_run.head_branch }}
|
||||||
revision: ${{ github.event.workflow_run.head_sha }}
|
revision: ${{ github.event.workflow_run.head_sha }}
|
||||||
token: ${{ secrets.SONAR_TOKEN }}
|
token: ${{ secrets.SONAR_TOKEN }}
|
||||||
|
|
||||||
- uses: guibranco/github-status-action-v2@741ea90ba6c3ca76fe0d43ba11a90cda97d5e685
|
- uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
name: SonarQube
|
name: SonarQube
|
||||||
on:
|
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"]
|
workflows: ["Tests"]
|
||||||
types:
|
types:
|
||||||
- completed
|
- completed
|
||||||
@@ -16,7 +18,7 @@ jobs:
|
|||||||
actions: read
|
actions: read
|
||||||
statuses: write
|
statuses: write
|
||||||
id-token: write # sonar
|
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:
|
secrets:
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
|||||||
@@ -14,58 +14,61 @@ jobs:
|
|||||||
name: "Typescript Syntax Check"
|
name: "Typescript Syntax Check"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
||||||
with:
|
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
|
node-version-file: package.json
|
||||||
|
|
||||||
- name: Install Deps
|
- name: Install Deps
|
||||||
run: "yarn install"
|
run: "pnpm install"
|
||||||
|
|
||||||
- name: Typecheck
|
- name: Typecheck
|
||||||
run: "yarn run lint:types"
|
run: "pnpm run lint:types"
|
||||||
|
|
||||||
js_lint:
|
js_lint:
|
||||||
name: "ESLint"
|
name: "ESLint"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
||||||
with:
|
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
|
node-version-file: package.json
|
||||||
|
|
||||||
- name: Install Deps
|
- name: Install Deps
|
||||||
run: "yarn install"
|
run: "pnpm install"
|
||||||
|
|
||||||
- name: Run Linter
|
- name: Run Linter
|
||||||
run: "yarn run lint:js"
|
run: "pnpm run lint:js"
|
||||||
|
|
||||||
node_example_lint:
|
node_example_lint:
|
||||||
name: "Node.js example"
|
name: "Node.js example"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
||||||
with:
|
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
|
node-version-file: package.json
|
||||||
|
|
||||||
- name: Install Deps
|
- name: Install Deps
|
||||||
run: "yarn install"
|
run: "pnpm install"
|
||||||
|
|
||||||
- name: Build Types
|
- name: Build Types
|
||||||
run: "yarn build:types"
|
run: "pnpm build:types"
|
||||||
|
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
||||||
with:
|
|
||||||
cache: "npm"
|
|
||||||
node-version-file: "examples/node/package.json"
|
|
||||||
# cache-dependency-path: '**/package-lock.json'
|
|
||||||
|
|
||||||
- name: Install Example Deps
|
- name: Install Example Deps
|
||||||
run: "npm install"
|
run: "npm install"
|
||||||
@@ -82,39 +85,50 @@ jobs:
|
|||||||
workflow_lint:
|
workflow_lint:
|
||||||
name: "Workflow Lint"
|
name: "Workflow Lint"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
security-events: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
||||||
with:
|
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
|
node-version-file: package.json
|
||||||
|
|
||||||
- name: Install Deps
|
- name: Install Deps
|
||||||
run: "yarn install --frozen-lockfile"
|
run: "pnpm install --frozen-lockfile"
|
||||||
|
|
||||||
- name: Run Linter
|
- name: Run Linter
|
||||||
run: "yarn lint:workflows"
|
run: "pnpm lint:workflows"
|
||||||
|
|
||||||
|
- name: Run zizmor
|
||||||
|
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
name: "JSDoc Checker"
|
name: "JSDoc Checker"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
||||||
with:
|
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
|
node-version-file: package.json
|
||||||
|
|
||||||
- name: Install Deps
|
- name: Install Deps
|
||||||
run: "yarn install"
|
run: "pnpm install"
|
||||||
|
|
||||||
- name: Generate Docs
|
- name: Generate Docs
|
||||||
run: "yarn run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
|
run: "pnpm run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||||
with:
|
with:
|
||||||
name: docs
|
name: docs
|
||||||
path: _docs
|
path: _docs
|
||||||
@@ -125,31 +139,36 @@ jobs:
|
|||||||
name: "Analyse Dead Code"
|
name: "Analyse Dead Code"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
||||||
with:
|
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
|
node-version-file: package.json
|
||||||
|
|
||||||
- name: Install Deps
|
- name: Install Deps
|
||||||
run: "yarn install --frozen-lockfile"
|
run: "pnpm install --frozen-lockfile"
|
||||||
|
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: "yarn run lint:knip"
|
run: "pnpm run lint:knip"
|
||||||
|
|
||||||
element-web:
|
element-web:
|
||||||
name: Downstream tsc element-web
|
name: Downstream tsc element-web
|
||||||
if: github.event_name == 'merge_group'
|
if: github.event_name == 'merge_group'
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
with:
|
with:
|
||||||
repository: element-hq/element-web
|
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:
|
with:
|
||||||
cache: "yarn"
|
cache: "pnpm"
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
@@ -159,15 +178,22 @@ jobs:
|
|||||||
JS_SDK_GITHUB_BASE_REF: ${{ github.sha }}
|
JS_SDK_GITHUB_BASE_REF: ${{ github.sha }}
|
||||||
|
|
||||||
- name: Typecheck
|
- 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
|
# Workflow consolidation job
|
||||||
downstream:
|
done:
|
||||||
name: Downstream Typescript Syntax Check
|
needs:
|
||||||
|
- ts_lint
|
||||||
|
- js_lint
|
||||||
|
- node_example_lint
|
||||||
|
- workflow_lint
|
||||||
|
- docs
|
||||||
|
- analyse_dead_code
|
||||||
|
- element-web
|
||||||
|
name: Static Analysis
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
if: always()
|
if: always()
|
||||||
needs:
|
|
||||||
- element-web
|
|
||||||
steps:
|
steps:
|
||||||
- if: needs.element-web.result != 'skipped' && needs.element-web.result != 'success'
|
- if: contains(needs.*.result , 'failure') || contains(needs.*.result, 'cancelled')
|
||||||
run: exit 1
|
run: exit 1
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ on:
|
|||||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||||
jobs:
|
jobs:
|
||||||
sync-labels:
|
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:
|
with:
|
||||||
LABELS: |
|
LABELS: |
|
||||||
element-hq/element-meta
|
element-hq/element-meta
|
||||||
|
|||||||
+26
-24
@@ -12,8 +12,8 @@ env:
|
|||||||
ENABLE_COVERAGE: ${{ github.event_name != 'merge_group' }}
|
ENABLE_COVERAGE: ${{ github.event_name != 'merge_group' }}
|
||||||
permissions: {} # No permissions required
|
permissions: {} # No permissions required
|
||||||
jobs:
|
jobs:
|
||||||
jest:
|
test:
|
||||||
name: "Jest [${{ matrix.specs }}] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"
|
name: "Vitest [${{ matrix.specs }}] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
strategy:
|
strategy:
|
||||||
@@ -22,17 +22,20 @@ jobs:
|
|||||||
node: ["lts/*", 22]
|
node: ["lts/*", 22]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- 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
|
- name: Setup Node
|
||||||
id: setupNode
|
id: setupNode
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "pnpm"
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ matrix.node }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: "yarn install"
|
run: "pnpm install"
|
||||||
|
|
||||||
- name: Get number of CPU cores
|
- name: Get number of CPU cores
|
||||||
id: cpu-cores
|
id: cpu-cores
|
||||||
@@ -40,24 +43,23 @@ jobs:
|
|||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: |
|
run: |
|
||||||
yarn test \
|
pnpm test \
|
||||||
--coverage=${{ env.ENABLE_COVERAGE }} \
|
--coverage=${ENABLE_COVERAGE} \
|
||||||
--ci \
|
--maxWorkers ${NUM_WORKERS} \
|
||||||
--max-workers ${{ steps.cpu-cores.outputs.count }} \
|
|
||||||
./spec/${{ matrix.specs }}
|
./spec/${{ matrix.specs }}
|
||||||
env:
|
env:
|
||||||
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
|
SHARD: ${{ matrix.specs }}
|
||||||
|
NUM_WORKERS: ${{ steps.cpu-cores.outputs.count }}
|
||||||
# tell jest to use coloured output
|
|
||||||
FORCE_COLOR: true
|
|
||||||
|
|
||||||
- name: Move coverage files into place
|
- name: Move coverage files into place
|
||||||
if: env.ENABLE_COVERAGE == 'true'
|
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
|
- name: Upload Artifact
|
||||||
if: env.ENABLE_COVERAGE == 'true'
|
if: env.ENABLE_COVERAGE == 'true'
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.specs }}-${{ matrix.node == 'lts/*' && 'lts' || matrix.node }}
|
name: coverage-${{ matrix.specs }}-${{ matrix.node == 'lts/*' && 'lts' || matrix.node }}
|
||||||
path: |
|
path: |
|
||||||
@@ -65,19 +67,19 @@ jobs:
|
|||||||
!coverage/lcov-report
|
!coverage/lcov-report
|
||||||
|
|
||||||
# Dummy completion job to simplify branch protections
|
# Dummy completion job to simplify branch protections
|
||||||
jest-complete:
|
complete:
|
||||||
name: Jest tests
|
name: Tests
|
||||||
needs: jest
|
needs: test
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- if: needs.jest.result != 'skipped' && needs.jest.result != 'success'
|
- if: needs.test.result != 'skipped' && needs.test.result != 'success'
|
||||||
run: exit 1
|
run: exit 1
|
||||||
|
|
||||||
element-web:
|
element-web:
|
||||||
name: Downstream test element-web
|
name: Downstream test element-web
|
||||||
if: github.event_name == 'merge_group'
|
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:
|
permissions:
|
||||||
statuses: write
|
statuses: write
|
||||||
with:
|
with:
|
||||||
@@ -87,8 +89,8 @@ jobs:
|
|||||||
complement-crypto:
|
complement-crypto:
|
||||||
name: "Run Complement Crypto tests"
|
name: "Run Complement Crypto tests"
|
||||||
if: github.event_name == 'merge_group'
|
if: github.event_name == 'merge_group'
|
||||||
permissions: read-all
|
permissions: read-all # zizmor: ignore[excessive-permissions]
|
||||||
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main
|
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
use_js_sdk: "."
|
use_js_sdk: "."
|
||||||
|
|
||||||
@@ -116,7 +118,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Skip SonarCloud on merge queues
|
- name: Skip SonarCloud on merge queues
|
||||||
if: env.ENABLE_COVERAGE == 'false'
|
if: env.ENABLE_COVERAGE == 'false'
|
||||||
uses: guibranco/github-status-action-v2@741ea90ba6c3ca76fe0d43ba11a90cda97d5e685
|
uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
|
||||||
with:
|
with:
|
||||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
state: success
|
state: success
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ jobs:
|
|||||||
automate-project-columns-next:
|
automate-project-columns-next:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/add-to-project@main
|
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
||||||
with:
|
with:
|
||||||
project-url: https://github.com/orgs/element-hq/projects/120
|
project-url: https://github.com/orgs/element-hq/projects/120
|
||||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ on:
|
|||||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||||
jobs:
|
jobs:
|
||||||
call-triage-labelled:
|
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:
|
secrets:
|
||||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
|
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10
|
||||||
with:
|
with:
|
||||||
operations-per-run: 250
|
operations-per-run: 250
|
||||||
days-before-issue-stale: -1
|
days-before-issue-stale: -1
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
/.npmrc
|
/.npmrc
|
||||||
/*.log
|
/*.log
|
||||||
|
pnpm-lock.yaml
|
||||||
package-lock.json
|
package-lock.json
|
||||||
.lock-wscript
|
.lock-wscript
|
||||||
build/Release
|
build/Release
|
||||||
|
|||||||
+249
@@ -1,3 +1,252 @@
|
|||||||
|
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)
|
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.
|
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.
|
||||||
|
|||||||
+1
-1
@@ -117,7 +117,7 @@ checks, so please check back after a few minutes.
|
|||||||
Your PR should include tests.
|
Your PR should include tests.
|
||||||
|
|
||||||
For new user facing features in `matrix-js-sdk`, you
|
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`
|
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
|
It's good practice to write tests alongside the code as it ensures the code is testable from
|
||||||
|
|||||||
@@ -41,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
|
> 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.
|
> [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)
|
Using `pnpm` instead of `npm` is recommended. Please see the pnpm [install
|
||||||
if you do not have it already.
|
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
|
```javascript
|
||||||
import * as sdk from "matrix-js-sdk";
|
import * as sdk from "matrix-js-sdk";
|
||||||
@@ -310,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:
|
host the API reference from the source files like this:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ yarn gendoc
|
$ pnpm gendoc
|
||||||
$ cd docs
|
$ cd docs
|
||||||
$ python -m http.server 8005
|
$ python -m http.server 8005
|
||||||
```
|
```
|
||||||
@@ -453,7 +453,7 @@ want to use this SDK, skip this section._
|
|||||||
First, you need to pull in the right build tools:
|
First, you need to pull in the right build tools:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ yarn install
|
$ pnpm install
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
@@ -461,17 +461,17 @@ First, you need to pull in the right build tools:
|
|||||||
To build a browser version from scratch when developing:
|
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:
|
To run linting:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ yarn lint
|
$ pnpm lint
|
||||||
```
|
```
|
||||||
|
|||||||
+2
-10
@@ -7,21 +7,13 @@ module.exports = {
|
|||||||
targets: {
|
targets: {
|
||||||
esmodules: true,
|
esmodules: true,
|
||||||
},
|
},
|
||||||
// We want to output ES modules for the final build (mostly to ensure that
|
modules: false,
|
||||||
// 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,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"@babel/preset-typescript",
|
"@babel/preset-typescript",
|
||||||
{
|
{
|
||||||
// When using the transpiled javascript in `lib`, Node.js requires `.js` extensions on any `import`
|
rewriteImportExtensions: true,
|
||||||
// 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",
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
+4
-5
@@ -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.
|
11. If a variable is not receiving a value on declaration, its type must be defined.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
let errorMessage: Optional<string>;
|
let errorMessage: string;
|
||||||
```
|
```
|
||||||
|
|
||||||
12. Objects can use shorthand declarations, including mixing of types.
|
12. Objects can use shorthand declarations, including mixing of types.
|
||||||
@@ -150,8 +150,7 @@ Unless otherwise specified, the following applies to all code:
|
|||||||
1. When using `any`, a comment explaining why must be present.
|
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.
|
27. `import` should be used instead of `require`, as `require` does not have types.
|
||||||
28. Export only what can be reused.
|
28. Export only what can be reused.
|
||||||
29. Prefer a type like `Optional<X>` (`type Optional<T> = T | null | undefined`) instead
|
29. Prefer a type like `X | null` instead of truly optional parameters.
|
||||||
of truly optional parameters.
|
|
||||||
1. A notable exception is when the likelihood of a bug is minimal, such as when a function
|
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
|
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
|
`?` operator is inappropriate is when taking a room ID: typically the caller should
|
||||||
@@ -161,7 +160,7 @@ Unless otherwise specified, the following applies to all code:
|
|||||||
```typescript
|
```typescript
|
||||||
function doThingWithRoom(
|
function doThingWithRoom(
|
||||||
thing: string,
|
thing: string,
|
||||||
room: Optional<string>, // require the caller to specify
|
room: string | null, // require the caller to specify
|
||||||
) {
|
) {
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
@@ -214,7 +213,7 @@ Unless otherwise specified, the following applies to all code:
|
|||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
1. Tests must be written in TypeScript.
|
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:
|
3. Use the following convention template:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
|||||||
@@ -21,4 +21,4 @@ export PATH="$rootdir/node_modules/.bin:$PATH"
|
|||||||
|
|
||||||
# now run our checks
|
# now run our checks
|
||||||
cd "$tmpdir"
|
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";
|
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 {
|
export default {
|
||||||
entry: [
|
entry: [
|
||||||
"src/index.ts",
|
"src/index.ts",
|
||||||
@@ -12,10 +15,6 @@ export default {
|
|||||||
"src/utils.ts", // not really an entrypoint but we have deprecated `defer` there
|
"src/utils.ts", // not really an entrypoint but we have deprecated `defer` there
|
||||||
"scripts/**",
|
"scripts/**",
|
||||||
"spec/**",
|
"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
|
// XXX: these should be re-exported by one of the supported exports
|
||||||
"src/matrixrtc/index.ts",
|
"src/matrixrtc/index.ts",
|
||||||
"src/sliding-sync.ts",
|
"src/sliding-sync.ts",
|
||||||
@@ -32,12 +31,6 @@ export default {
|
|||||||
"husky",
|
"husky",
|
||||||
// Used in script which only runs in environment with `@octokit/rest` installed
|
// Used in script which only runs in environment with `@octokit/rest` installed
|
||||||
"@octokit/rest",
|
"@octokit/rest",
|
||||||
// Used by jest
|
|
||||||
"jest-environment-jsdom",
|
|
||||||
"babel-jest",
|
|
||||||
"ts-node",
|
|
||||||
// Used by `@babel/plugin-transform-runtime`
|
|
||||||
"@babel/runtime",
|
|
||||||
],
|
],
|
||||||
ignoreBinaries: [
|
ignoreBinaries: [
|
||||||
// Used when available by reusable workflow `.github/workflows/release-make.yml`
|
// Used when available by reusable workflow `.github/workflows/release-make.yml`
|
||||||
|
|||||||
+47
-41
@@ -1,27 +1,26 @@
|
|||||||
{
|
{
|
||||||
"name": "matrix-js-sdk",
|
"name": "matrix-js-sdk",
|
||||||
"version": "37.13.0",
|
"version": "41.3.0",
|
||||||
"description": "Matrix Client-Server SDK for Javascript",
|
"description": "Matrix Client-Server SDK for Javascript",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.0.0"
|
"node": ">=22.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "yarn build",
|
"prepare": "pnpm build",
|
||||||
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel --delete-dir-on-start src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||||
"clean": "rimraf lib",
|
"build": "pnpm build:compile && pnpm build:types",
|
||||||
"build": "yarn clean && yarn build:compile && yarn build:types",
|
|
||||||
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
|
"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",
|
"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": "eslint --max-warnings 0 src spec && prettier --check .",
|
||||||
"lint:js-fix": "prettier --log-level=warn --write . && eslint --fix src spec",
|
"lint:js-fix": "prettier --log-level=warn --write . && eslint --fix src spec",
|
||||||
"lint:types": "tsc --noEmit",
|
"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:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'",
|
||||||
"lint:knip": "knip",
|
"lint:knip": "knip",
|
||||||
"test": "jest",
|
"test": "vitest run",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "vitest watch",
|
||||||
"coverage": "yarn test --coverage"
|
"coverage": "pnpm test --coverage"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -49,19 +48,18 @@
|
|||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@matrix-org/matrix-sdk-crypto-wasm": "^15.1.0",
|
"@matrix-org/matrix-sdk-crypto-wasm": "^18.1.0",
|
||||||
"another-json": "^0.2.0",
|
"another-json": "^0.2.0",
|
||||||
"bs58": "^6.0.0",
|
"bs58": "^6.0.0",
|
||||||
"content-type": "^1.0.4",
|
"content-type": "^1.0.4",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"loglevel": "^1.9.2",
|
"loglevel": "^1.9.2",
|
||||||
"matrix-events-sdk": "0.0.1",
|
"matrix-events-sdk": "0.0.1",
|
||||||
"matrix-widget-api": "^1.10.0",
|
"matrix-widget-api": "^1.16.1",
|
||||||
"oidc-client-ts": "^3.0.1",
|
"oidc-client-ts": "^3.0.1",
|
||||||
"p-retry": "4",
|
"p-retry": "8",
|
||||||
"sdp-transform": "^2.14.1",
|
"sdp-transform": "^3.0.0",
|
||||||
"unhomoglyph": "^1.0.6",
|
"unhomoglyph": "^1.0.6"
|
||||||
"uuid": "11"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@action-validator/cli": "^0.6.0",
|
"@action-validator/cli": "^0.6.0",
|
||||||
@@ -78,18 +76,19 @@
|
|||||||
"@babel/plugin-transform-runtime": "^7.12.10",
|
"@babel/plugin-transform-runtime": "^7.12.10",
|
||||||
"@babel/preset-env": "^7.12.11",
|
"@babel/preset-env": "^7.12.11",
|
||||||
"@babel/preset-typescript": "^7.12.7",
|
"@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",
|
"@matrix-org/olm": "3.2.15",
|
||||||
"@peculiar/webcrypto": "^1.4.5",
|
"@peculiar/webcrypto": "^1.4.5",
|
||||||
"@stylistic/eslint-plugin": "^5.0.0",
|
"@stylistic/eslint-plugin": "^5.0.0",
|
||||||
"@types/content-type": "^1.1.5",
|
"@types/content-type": "^1.1.5",
|
||||||
"@types/debug": "^4.1.7",
|
"@types/debug": "^4.1.7",
|
||||||
"@types/jest": "^29.0.0",
|
"@types/node": "22",
|
||||||
"@types/node": "18",
|
|
||||||
"@types/sdp-transform": "^2.4.5",
|
"@types/sdp-transform": "^2.4.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||||
"@typescript-eslint/parser": "^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",
|
"babel-plugin-search-and-replace": "^1.1.1",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"eslint": "8.57.1",
|
"eslint": "8.57.1",
|
||||||
@@ -97,36 +96,43 @@
|
|||||||
"eslint-config-prettier": "^10.0.0",
|
"eslint-config-prettier": "^10.0.0",
|
||||||
"eslint-import-resolver-typescript": "^4.0.0",
|
"eslint-import-resolver-typescript": "^4.0.0",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-jest": "^28.0.0",
|
"eslint-plugin-jsdoc": "^62.0.0",
|
||||||
"eslint-plugin-jsdoc": "^50.0.0",
|
"eslint-plugin-matrix-org": "^3.0.0",
|
||||||
"eslint-plugin-matrix-org": "2.1.0",
|
|
||||||
"eslint-plugin-n": "^14.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",
|
"eslint-plugin-unicorn": "^56.0.0",
|
||||||
"fake-indexeddb": "^5.0.2",
|
"fake-indexeddb": "^5.0.2",
|
||||||
"fetch-mock": "11.1.5",
|
"fetch-mock": "^12.6.0",
|
||||||
"fetch-mock-jest": "^1.5.1",
|
"happy-dom": "^20.1.0",
|
||||||
"husky": "^9.0.0",
|
"husky": "^9.0.0",
|
||||||
"jest": "^29.0.0",
|
"knip": "^6.0.0",
|
||||||
"jest-environment-jsdom": "^29.0.0",
|
|
||||||
"jest-localstorage-mock": "^2.4.6",
|
|
||||||
"jest-mock": "^29.0.0",
|
|
||||||
"knip": "^5.0.0",
|
|
||||||
"lint-staged": "^16.0.0",
|
"lint-staged": "^16.0.0",
|
||||||
"matrix-mock-request": "^2.5.0",
|
"matrix-mock-request": "^2.5.0",
|
||||||
"node-fetch": "^2.7.0",
|
"prettier": "3.8.3",
|
||||||
"prettier": "3.6.2",
|
|
||||||
"rimraf": "^6.0.0",
|
|
||||||
"ts-node": "^10.9.2",
|
|
||||||
"typedoc": "^0.28.1",
|
"typedoc": "^0.28.1",
|
||||||
"typedoc-plugin-coverage": "^4.0.0",
|
"typedoc-plugin-coverage": "^4.0.0",
|
||||||
"typedoc-plugin-mdn-links": "^5.0.0",
|
"typedoc-plugin-mdn-links": "^5.0.0",
|
||||||
"typedoc-plugin-missing-exports": "^4.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": {
|
"pnpm": {
|
||||||
"outputDirectory": "coverage",
|
"peerDependencyRules": {
|
||||||
"outputName": "jest-sonar-report.xml",
|
"allowedVersions": {
|
||||||
"relativePaths": true
|
"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.typescript.tsconfigPath=./tsconfig.json
|
||||||
sonar.javascript.lcov.reportPaths=coverage/lcov.info
|
sonar.javascript.lcov.reportPaths=coverage/lcov.info
|
||||||
sonar.coverage.exclusions=spec/**/*
|
sonar.coverage.exclusions=spec/**/*
|
||||||
sonar.testExecutionReportPaths=coverage/jest-sonar-report.xml
|
sonar.testExecutionReportPaths=coverage/sonar-report.xml
|
||||||
|
|
||||||
sonar.lang.patterns.ts=**/*.ts,**/*.tsx
|
sonar.lang.patterns.ts=**/*.ts,**/*.tsx
|
||||||
|
|||||||
+2
-2
@@ -17,7 +17,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// `expect` is allowed in helper functions which are called within `test`/`it` blocks
|
// `expect` is allowed in helper functions which are called within `test`/`it` blocks
|
||||||
/* eslint-disable jest/no-standalone-expect */
|
/* eslint-disable @vitest/no-standalone-expect */
|
||||||
|
|
||||||
import MockHttpBackend from "matrix-mock-request";
|
import MockHttpBackend from "matrix-mock-request";
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ import { type ISyncResponder } from "./test-utils/SyncResponder";
|
|||||||
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
|
* 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.
|
* @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.
|
* use things like {@link E2EKeyReceiver} and {@link SyncResponder} to manage the requests.
|
||||||
*/
|
*/
|
||||||
export class TestClient implements IE2EKeyReceiver, ISyncResponder {
|
export class TestClient implements IE2EKeyReceiver, ISyncResponder {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fetchMock from "fetch-mock-jest";
|
import fetchMock from "@fetch-mock/vitest";
|
||||||
import "fake-indexeddb/auto";
|
import "fake-indexeddb/auto";
|
||||||
import { IDBFactory } from "fake-indexeddb";
|
import { IDBFactory } from "fake-indexeddb";
|
||||||
import debug from "debug";
|
import debug from "debug";
|
||||||
@@ -74,7 +74,7 @@ describe("cross-signing", () => {
|
|||||||
function createCryptoCallbacks(): CryptoCallbacks {
|
function createCryptoCallbacks(): CryptoCallbacks {
|
||||||
return {
|
return {
|
||||||
getSecretStorageKey: (keys, name) => {
|
getSecretStorageKey: (keys, name) => {
|
||||||
return Promise.resolve<[string, Uint8Array]>(["key_id", encryptionKey]);
|
return Promise.resolve<[string, Uint8Array<ArrayBuffer>]>(["key_id", encryptionKey]);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,6 @@ describe("cross-signing", () => {
|
|||||||
async () => {
|
async () => {
|
||||||
// anything that we don't have a specific matcher for silently returns a 404
|
// anything that we don't have a specific matcher for silently returns a 404
|
||||||
fetchMock.catch(404);
|
fetchMock.catch(404);
|
||||||
fetchMock.config.warnOnFallback = false;
|
|
||||||
|
|
||||||
const homeserverUrl = "https://alice-server.com";
|
const homeserverUrl = "https://alice-server.com";
|
||||||
aliceClient = createClient({
|
aliceClient = createClient({
|
||||||
@@ -113,8 +112,7 @@ describe("cross-signing", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await aliceClient.stopClient();
|
aliceClient.stopClient();
|
||||||
fetchMock.mockReset();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -138,27 +136,25 @@ describe("cross-signing", () => {
|
|||||||
await bootstrapCrossSigning(authDict);
|
await bootstrapCrossSigning(authDict);
|
||||||
|
|
||||||
// check that the cross-signing keys have been uploaded
|
// check that the cross-signing keys have been uploaded
|
||||||
expect(fetchMock.called("upload-cross-signing-keys")).toBeTruthy();
|
expect(fetchMock.callHistory.called("upload-cross-signing-keys")).toBeTruthy();
|
||||||
const [, keysOpts] = fetchMock.lastCall("upload-cross-signing-keys")!;
|
const keysOpts = fetchMock.callHistory.lastCall("upload-cross-signing-keys")!.options;
|
||||||
const keysBody = JSON.parse(keysOpts!.body as string);
|
const keysBody = JSON.parse(keysOpts!.body as string);
|
||||||
expect(keysBody.auth).toEqual(authDict); // check uia dict was passed
|
expect(keysBody.auth).toEqual(authDict); // check uia dict was passed
|
||||||
// there should be a key of each type
|
// there should be a key of each type
|
||||||
// master key is signed by the device
|
// 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];
|
const masterKeyId = Object.keys(keysBody.master_key.keys)[0];
|
||||||
// ssk and usk are signed by the master key
|
// ssk and usk are signed by the master key
|
||||||
expect(keysBody).toHaveProperty(`self_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}]`);
|
expect(keysBody).toHaveProperty(["user_signing_key", "signatures", TEST_USER_ID, masterKeyId]);
|
||||||
const sskId = Object.keys(keysBody.self_signing_key.keys)[0];
|
const sskId = Object.keys(keysBody.self_signing_key.keys)[0];
|
||||||
|
|
||||||
// check the publish call
|
// check the publish call
|
||||||
expect(fetchMock.called("upload-sigs")).toBeTruthy();
|
expect(fetchMock.callHistory.called("upload-sigs")).toBeTruthy();
|
||||||
const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!;
|
const sigsOpts = fetchMock.callHistory.lastCall("upload-sigs")!.options;
|
||||||
const body = JSON.parse(sigsOpts!.body as string);
|
const body = JSON.parse(sigsOpts!.body as string);
|
||||||
// there should be a signature for our device, by our self-signing key.
|
// there should be a signature for our device, by our self-signing key.
|
||||||
expect(body).toHaveProperty(
|
expect(body).toHaveProperty([TEST_USER_ID, TEST_DEVICE_ID, "signatures", TEST_USER_ID, sskId]);
|
||||||
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[${sskId}]`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("get cross signing keys from secret storage and import them", async () => {
|
it("get cross signing keys from secret storage and import them", async () => {
|
||||||
@@ -237,13 +233,17 @@ describe("cross-signing", () => {
|
|||||||
expect(await userTrustStatusChangedPromise).toBe(aliceClient.getUserId());
|
expect(await userTrustStatusChangedPromise).toBe(aliceClient.getUserId());
|
||||||
|
|
||||||
// Expect the signature to be uploaded
|
// Expect the signature to be uploaded
|
||||||
expect(fetchMock.called("upload-sigs")).toBeTruthy();
|
expect(fetchMock.callHistory.called("upload-sigs")).toBeTruthy();
|
||||||
const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!;
|
const sigsOpts = fetchMock.callHistory.lastCall("upload-sigs")!.options;
|
||||||
const body = JSON.parse(sigsOpts!.body as string);
|
const body = JSON.parse(sigsOpts!.body as string);
|
||||||
// the device should have a signature with the public self cross signing keys.
|
// the device should have a signature with the public self cross signing keys.
|
||||||
expect(body).toHaveProperty(
|
expect(body).toHaveProperty([
|
||||||
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[ed25519:${SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64}]`,
|
TEST_USER_ID,
|
||||||
);
|
TEST_DEVICE_ID,
|
||||||
|
"signatures",
|
||||||
|
TEST_USER_ID,
|
||||||
|
`ed25519:${SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64}`,
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can bootstrapCrossSigning twice", async () => {
|
it("can bootstrapCrossSigning twice", async () => {
|
||||||
@@ -255,8 +255,7 @@ describe("cross-signing", () => {
|
|||||||
// a second call should do nothing except GET requests
|
// a second call should do nothing except GET requests
|
||||||
fetchMock.mockClear();
|
fetchMock.mockClear();
|
||||||
await bootstrapCrossSigning(authDict);
|
await bootstrapCrossSigning(authDict);
|
||||||
const calls = fetchMock.calls((url, opts) => opts.method != "GET");
|
expect(fetchMock).toHaveFetchedTimes(0, "unmatched");
|
||||||
expect(calls.length).toEqual(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("will upload existing cross-signing keys to an established secret storage", async () => {
|
it("will upload existing cross-signing keys to an established secret storage", async () => {
|
||||||
@@ -267,7 +266,6 @@ describe("cross-signing", () => {
|
|||||||
// To arrange that, we call `bootstrapCrossSigning` on our main device, and then (pretend to) set up 4S from
|
// 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.
|
// a *different* device. Then, when we call `bootstrapCrossSigning` again, it should do the honours.
|
||||||
|
|
||||||
mockSetupCrossSigningRequests();
|
|
||||||
const accountDataAccumulator = new AccountDataAccumulator(syncResponder);
|
const accountDataAccumulator = new AccountDataAccumulator(syncResponder);
|
||||||
accountDataAccumulator.interceptGetAccountData();
|
accountDataAccumulator.interceptGetAccountData();
|
||||||
|
|
||||||
@@ -282,7 +280,7 @@ describe("cross-signing", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Prepare for the cross-signing keys
|
// 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 bootstrapCrossSigning(authDict);
|
||||||
await p;
|
await p;
|
||||||
@@ -403,7 +401,7 @@ describe("cross-signing", () => {
|
|||||||
const isCrossSigningReady = await aliceClient.getCrypto()!.isCrossSigningReady();
|
const isCrossSigningReady = await aliceClient.getCrypto()!.isCrossSigningReady();
|
||||||
|
|
||||||
expect(isCrossSigningReady).toBeFalsy();
|
expect(isCrossSigningReady).toBeFalsy();
|
||||||
});
|
}, 10000);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getCrossSigningKeyId", () => {
|
describe("getCrossSigningKeyId", () => {
|
||||||
@@ -415,22 +413,13 @@ describe("cross-signing", () => {
|
|||||||
*/
|
*/
|
||||||
function awaitCrossSigningKeysUpload() {
|
function awaitCrossSigningKeysUpload() {
|
||||||
return new Promise<any>((resolve) => {
|
return new Promise<any>((resolve) => {
|
||||||
fetchMock.post(
|
fetchMock.modifyRoute("upload-cross-signing-keys", {
|
||||||
{
|
response: (callLog) => {
|
||||||
url: new URL(
|
const content = JSON.parse(callLog.options.body as string);
|
||||||
"/_matrix/client/v3/keys/device_signing/upload",
|
|
||||||
aliceClient.getHomeserverUrl(),
|
|
||||||
).toString(),
|
|
||||||
name: "upload-cross-signing-keys",
|
|
||||||
},
|
|
||||||
(url, options) => {
|
|
||||||
const content = JSON.parse(options.body as string);
|
|
||||||
resolve(content);
|
resolve(content);
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
// Override the route defined in E2EKeyReceiver
|
});
|
||||||
{ overwriteRoutes: true },
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,9 +450,6 @@ describe("cross-signing", () => {
|
|||||||
|
|
||||||
describe("crossSignDevice", () => {
|
describe("crossSignDevice", () => {
|
||||||
beforeEach(async () => {
|
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
|
// make sure that there is another device which we can sign
|
||||||
e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
|
e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
|
||||||
|
|
||||||
@@ -477,10 +463,6 @@ describe("cross-signing", () => {
|
|||||||
expect(devices.get(aliceClient.getSafeUserId())!.has(testData.TEST_DEVICE_ID)).toBeTruthy();
|
expect(devices.get(aliceClient.getSafeUserId())!.has(testData.TEST_DEVICE_ID)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
jest.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("fails for an unknown device", async () => {
|
it("fails for an unknown device", async () => {
|
||||||
await expect(aliceClient.getCrypto()!.crossSignDevice("unknown")).rejects.toThrow("Unknown device");
|
await expect(aliceClient.getCrypto()!.crossSignDevice("unknown")).rejects.toThrow("Unknown device");
|
||||||
});
|
});
|
||||||
@@ -493,9 +475,9 @@ describe("cross-signing", () => {
|
|||||||
await aliceClient.getCrypto()!.crossSignDevice(testData.TEST_DEVICE_ID);
|
await aliceClient.getCrypto()!.crossSignDevice(testData.TEST_DEVICE_ID);
|
||||||
|
|
||||||
// check that a sig for the device was uploaded
|
// 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);
|
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];
|
const deviceSig = body[aliceClient.getSafeUserId()][testData.TEST_DEVICE_ID];
|
||||||
expect(deviceSig).toHaveProperty("signatures");
|
expect(deviceSig).toHaveProperty("signatures");
|
||||||
});
|
});
|
||||||
|
|||||||
+224
-246
@@ -16,12 +16,12 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import anotherjson from "another-json";
|
import anotherjson from "another-json";
|
||||||
import fetchMock from "fetch-mock-jest";
|
import fetchMock from "@fetch-mock/vitest";
|
||||||
import "fake-indexeddb/auto";
|
import "fake-indexeddb/auto";
|
||||||
import { IDBFactory } from "fake-indexeddb";
|
import { IDBFactory } from "fake-indexeddb";
|
||||||
import Olm from "@matrix-org/olm";
|
import Olm from "@matrix-org/olm";
|
||||||
|
import { type RouteResponse } from "fetch-mock";
|
||||||
|
|
||||||
import type FetchMock from "fetch-mock";
|
|
||||||
import * as testUtils from "../../test-utils/test-utils";
|
import * as testUtils from "../../test-utils/test-utils";
|
||||||
import {
|
import {
|
||||||
emitPromise,
|
emitPromise,
|
||||||
@@ -86,9 +86,13 @@ import {
|
|||||||
encryptGroupSessionKey,
|
encryptGroupSessionKey,
|
||||||
encryptMegolmEvent,
|
encryptMegolmEvent,
|
||||||
encryptMegolmEventRawPlainText,
|
encryptMegolmEventRawPlainText,
|
||||||
|
encryptOlmEvent,
|
||||||
establishOlmSession,
|
establishOlmSession,
|
||||||
getTestOlmAccountKeys,
|
getTestOlmAccountKeys,
|
||||||
} from "./olm-utils";
|
expectSendRoomKey,
|
||||||
|
expectSendMegolmMessageEvent,
|
||||||
|
expectEncryptedSendMessageEvent,
|
||||||
|
} from "./olm-utils.ts";
|
||||||
import { AccountDataAccumulator } from "../../test-utils/AccountDataAccumulator";
|
import { AccountDataAccumulator } from "../../test-utils/AccountDataAccumulator";
|
||||||
import { UNSIGNED_MEMBERSHIP_FIELD } from "../../../src/@types/event";
|
import { UNSIGNED_MEMBERSHIP_FIELD } from "../../../src/@types/event";
|
||||||
import { KnownMembership } from "../../../src/@types/membership";
|
import { KnownMembership } from "../../../src/@types/membership";
|
||||||
@@ -101,110 +105,9 @@ afterEach(() => {
|
|||||||
// eslint-disable-next-line no-global-assign
|
// eslint-disable-next-line no-global-assign
|
||||||
indexedDB = new IDBFactory();
|
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 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", () => {
|
describe("crypto", () => {
|
||||||
let testOlmAccount = {} as unknown as Olm.Account;
|
let testOlmAccount = {} as unknown as Olm.Account;
|
||||||
let testSenderKey = "";
|
let testSenderKey = "";
|
||||||
@@ -252,13 +155,8 @@ describe("crypto", () => {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
const rootRegexp = escapeRegExp(new URL("/_matrix/client/", aliceClient.getHomeserverUrl()).toString());
|
const rootRegexp = escapeRegExp(new URL("/_matrix/client/", aliceClient.getHomeserverUrl()).toString());
|
||||||
fetchMock.postOnce(
|
fetchMock.postOnce(new RegExp(rootRegexp + "(r0|v3)/keys/query"), (callLog) =>
|
||||||
new RegExp(rootRegexp + "(r0|v3)/keys/query"),
|
onQueryRequest(JSON.parse(callLog.options.body as string)),
|
||||||
(url: string, opts: RequestInit) => onQueryRequest(JSON.parse(opts.body as string)),
|
|
||||||
{
|
|
||||||
// append to the list of intercepts on this path
|
|
||||||
overwriteRoutes: false,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,7 +166,7 @@ describe("crypto", () => {
|
|||||||
* @param response - the response to return from the request. Normally an {@link IClaimOTKsResult}
|
* @param response - the response to return from the request. Normally an {@link IClaimOTKsResult}
|
||||||
* (or a function that returns one).
|
* (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());
|
const rootRegexp = escapeRegExp(new URL("/_matrix/client/", aliceClient.getHomeserverUrl()).toString());
|
||||||
fetchMock.postOnce(new RegExp(rootRegexp + "(r0|v3)/keys/claim"), response);
|
fetchMock.postOnce(new RegExp(rootRegexp + "(r0|v3)/keys/claim"), response);
|
||||||
}
|
}
|
||||||
@@ -321,15 +219,20 @@ describe("crypto", () => {
|
|||||||
*/
|
*/
|
||||||
function createCryptoCallbacks(): CryptoCallbacks {
|
function createCryptoCallbacks(): CryptoCallbacks {
|
||||||
// Store the cached secret storage key and return it when `getSecretStorageKey` is called
|
// Store the cached secret storage key and return it when `getSecretStorageKey` is called
|
||||||
let cachedKey: { keyId: string; key: Uint8Array };
|
let cachedKey: { keyId: string; key: Uint8Array<ArrayBuffer> };
|
||||||
const cacheSecretStorageKey = (keyId: string, keyInfo: SecretStorageKeyDescription, key: Uint8Array) => {
|
const cacheSecretStorageKey = (
|
||||||
|
keyId: string,
|
||||||
|
keyInfo: SecretStorageKeyDescription,
|
||||||
|
key: Uint8Array<ArrayBuffer>,
|
||||||
|
) => {
|
||||||
cachedKey = {
|
cachedKey = {
|
||||||
keyId,
|
keyId,
|
||||||
key,
|
key,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSecretStorageKey = () => Promise.resolve<[string, Uint8Array]>([cachedKey.keyId, cachedKey.key]);
|
const getSecretStorageKey = () =>
|
||||||
|
Promise.resolve<[string, Uint8Array<ArrayBuffer>]>([cachedKey.keyId, cachedKey.key]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cacheSecretStorageKey,
|
cacheSecretStorageKey,
|
||||||
@@ -341,7 +244,6 @@ describe("crypto", () => {
|
|||||||
async () => {
|
async () => {
|
||||||
// anything that we don't have a specific matcher for silently returns a 404
|
// anything that we don't have a specific matcher for silently returns a 404
|
||||||
fetchMock.catch(404);
|
fetchMock.catch(404);
|
||||||
fetchMock.config.warnOnFallback = false;
|
|
||||||
|
|
||||||
const homeserverUrl = "https://alice-server.com";
|
const homeserverUrl = "https://alice-server.com";
|
||||||
aliceClient = createClient({
|
aliceClient = createClient({
|
||||||
@@ -363,18 +265,20 @@ describe("crypto", () => {
|
|||||||
testOlmAccount = await createOlmAccount();
|
testOlmAccount = await createOlmAccount();
|
||||||
const testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
|
const testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
|
||||||
testSenderKey = testE2eKeys.curve25519;
|
testSenderKey = testE2eKeys.curve25519;
|
||||||
|
|
||||||
|
vi.useRealTimers();
|
||||||
},
|
},
|
||||||
/* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */
|
/* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */
|
||||||
10000,
|
10000,
|
||||||
);
|
);
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await aliceClient.stopClient();
|
aliceClient.stopClient();
|
||||||
|
|
||||||
// Allow in-flight things to complete before we tear down the test
|
// Allow in-flight things to complete before we tear down the test
|
||||||
await jest.runAllTimersAsync();
|
if (vi.isFakeTimers()) {
|
||||||
|
await vi.runAllTimersAsync();
|
||||||
fetchMock.mockReset();
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("MatrixClient.getCrypto returns a CryptoApi", () => {
|
it("MatrixClient.getCrypto returns a CryptoApi", () => {
|
||||||
@@ -441,7 +345,7 @@ describe("crypto", () => {
|
|||||||
|
|
||||||
describe("Unable to decrypt error codes", function () {
|
describe("Unable to decrypt error codes", function () {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
vi.useFakeTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Decryption fails with UISI error", async () => {
|
it("Decryption fails with UISI error", async () => {
|
||||||
@@ -817,7 +721,7 @@ describe("crypto", () => {
|
|||||||
syncResponder.sendOrQueueSyncResponse(syncResponse);
|
syncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||||
await syncPromise(aliceClient);
|
await syncPromise(aliceClient);
|
||||||
|
|
||||||
await awaitDecryptionError;
|
await expect(awaitDecryptionError).resolves.toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -968,6 +872,7 @@ describe("crypto", () => {
|
|||||||
await expectSendRoomKey("@bob:xyz", testOlmAccount);
|
await expectSendRoomKey("@bob:xyz", testOlmAccount);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @vitest/expect-expect
|
||||||
it("Alice sends a megolm message", async () => {
|
it("Alice sends a megolm message", async () => {
|
||||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||||
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||||
@@ -991,10 +896,11 @@ describe("crypto", () => {
|
|||||||
// Finally, send the message, and expect to get an `m.room.encrypted` event that we can decrypt.
|
// Finally, send the message, and expect to get an `m.room.encrypted` event that we can decrypt.
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
aliceClient.sendTextMessage(ROOM_ID, "test"),
|
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 () => {
|
it("We should start a new megolm session after forceDiscardSession", async () => {
|
||||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||||
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||||
@@ -1018,7 +924,7 @@ describe("crypto", () => {
|
|||||||
// Send the first message, and check we can decrypt it.
|
// Send the first message, and check we can decrypt it.
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
aliceClient.sendTextMessage(ROOM_ID, "test"),
|
aliceClient.sendTextMessage(ROOM_ID, "test"),
|
||||||
expectSendMegolmMessage(inboundGroupSessionPromise),
|
expectSendMegolmMessageEvent(inboundGroupSessionPromise),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Finally the interesting part: discard the session.
|
// Finally the interesting part: discard the session.
|
||||||
@@ -1026,7 +932,7 @@ describe("crypto", () => {
|
|||||||
|
|
||||||
// Now when we send the next message, we should get a *new* megolm session.
|
// Now when we send the next message, we should get a *new* megolm session.
|
||||||
const inboundGroupSessionPromise2 = expectSendRoomKey("@bob:xyz", testOlmAccount);
|
const inboundGroupSessionPromise2 = expectSendRoomKey("@bob:xyz", testOlmAccount);
|
||||||
const p2 = expectSendMegolmMessage(inboundGroupSessionPromise2);
|
const p2 = expectSendMegolmMessageEvent(inboundGroupSessionPromise2);
|
||||||
await Promise.all([aliceClient.sendTextMessage(ROOM_ID, "test2"), p2]);
|
await Promise.all([aliceClient.sendTextMessage(ROOM_ID, "test2"), p2]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1037,7 +943,7 @@ describe("crypto", () => {
|
|||||||
*/
|
*/
|
||||||
async function sendEncryptedMessage(): Promise<IContent> {
|
async function sendEncryptedMessage(): Promise<IContent> {
|
||||||
const [encryptedMessage] = await Promise.all([
|
const [encryptedMessage] = await Promise.all([
|
||||||
expectEncryptedSendMessage(),
|
expectEncryptedSendMessageEvent(),
|
||||||
aliceClient.sendTextMessage(ROOM_ID, "test"),
|
aliceClient.sendTextMessage(ROOM_ID, "test"),
|
||||||
]);
|
]);
|
||||||
return encryptedMessage;
|
return encryptedMessage;
|
||||||
@@ -1095,9 +1001,7 @@ describe("crypto", () => {
|
|||||||
await startClientAndAwaitFirstSync();
|
await startClientAndAwaitFirstSync();
|
||||||
const p2pSession = await establishOlmSession(aliceClient, keyReceiver, syncResponder, testOlmAccount);
|
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
|
vi.useFakeTimers();
|
||||||
// working `queueMicrotask`
|
|
||||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
|
||||||
|
|
||||||
const syncResponse = getSyncResponse(["@bob:xyz"]);
|
const syncResponse = getSyncResponse(["@bob:xyz"]);
|
||||||
|
|
||||||
@@ -1129,7 +1033,7 @@ describe("crypto", () => {
|
|||||||
expect(sessionId).toBeDefined();
|
expect(sessionId).toBeDefined();
|
||||||
|
|
||||||
// Advance the time by 1h
|
// Advance the time by 1h
|
||||||
jest.advanceTimersByTime(oneHourInMs);
|
vi.advanceTimersByTime(oneHourInMs);
|
||||||
|
|
||||||
// Send a second message to bob and get the encrypted message
|
// Send a second message to bob and get the encrypted message
|
||||||
const [secondEncryptedMessage] = await Promise.all([
|
const [secondEncryptedMessage] = await Promise.all([
|
||||||
@@ -1159,7 +1063,7 @@ describe("crypto", () => {
|
|||||||
let [, , encryptedMessage] = await Promise.all([
|
let [, , encryptedMessage] = await Promise.all([
|
||||||
aliceClient.sendTextMessage(ROOM_ID, "test"),
|
aliceClient.sendTextMessage(ROOM_ID, "test"),
|
||||||
expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession),
|
expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession),
|
||||||
expectEncryptedSendMessage(),
|
expectEncryptedSendMessageEvent(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Check that the session id exists
|
// Check that the session id exists
|
||||||
@@ -1187,7 +1091,7 @@ describe("crypto", () => {
|
|||||||
[, , encryptedMessage] = await Promise.all([
|
[, , encryptedMessage] = await Promise.all([
|
||||||
aliceClient.sendTextMessage(ROOM_ID, "test"),
|
aliceClient.sendTextMessage(ROOM_ID, "test"),
|
||||||
expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession),
|
expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession),
|
||||||
expectEncryptedSendMessage(),
|
expectEncryptedSendMessageEvent(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Check that the new session id exists
|
// Check that the new session id exists
|
||||||
@@ -1250,7 +1154,7 @@ describe("crypto", () => {
|
|||||||
// it probably won't be decrypted yet, because it takes a while to process the olm keys
|
// 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 });
|
const decryptedEvent = await testUtils.awaitDecryption(event, { waitOnDecryptionFailure: true });
|
||||||
expect(decryptedEvent.getRoomId()).toEqual(ROOM_ID);
|
expect(decryptedEvent.getRoomId()).toEqual(ROOM_ID);
|
||||||
expect(decryptedEvent.getContent()).toEqual({});
|
expect(decryptedEvent.getContent<IContent>()).toEqual({});
|
||||||
expect(decryptedEvent.getClearContent()).toBeUndefined();
|
expect(decryptedEvent.getClearContent()).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1274,20 +1178,12 @@ describe("crypto", () => {
|
|||||||
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount);
|
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount);
|
||||||
|
|
||||||
// ... and finally, send the room key. We block the response until `sendRoomMessageDefer` completes.
|
// ... 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) => {
|
const reqProm = new Promise<IContent>((resolve) => {
|
||||||
fetchMock.putOnce(
|
fetchMock.putOnce(new RegExp("/send/m.room.encrypted/"), async (callLog): Promise<RouteResponse> => {
|
||||||
new RegExp("/send/m.room.encrypted/"),
|
resolve(JSON.parse(callLog.options.body as string));
|
||||||
async (url: string, opts: RequestInit): Promise<FetchMock.MockResponse> => {
|
return await sendRoomMessageResolvers.promise;
|
||||||
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,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Now we start to send the message
|
// Now we start to send the message
|
||||||
@@ -1370,6 +1266,7 @@ describe("crypto", () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @vitest/expect-expect
|
||||||
it("Sending an event initiates a member list sync", async () => {
|
it("Sending an event initiates a member list sync", async () => {
|
||||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||||
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||||
@@ -1385,7 +1282,7 @@ describe("crypto", () => {
|
|||||||
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession);
|
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession);
|
||||||
|
|
||||||
// and finally the megolm message
|
// and finally the megolm message
|
||||||
const megolmMessagePromise = expectSendMegolmMessage(inboundGroupSessionPromise);
|
const megolmMessagePromise = expectSendMegolmMessageEvent(inboundGroupSessionPromise);
|
||||||
|
|
||||||
// kick it off
|
// kick it off
|
||||||
const sendPromise = aliceClient.sendTextMessage(ROOM_ID, "test");
|
const sendPromise = aliceClient.sendTextMessage(ROOM_ID, "test");
|
||||||
@@ -1393,6 +1290,7 @@ describe("crypto", () => {
|
|||||||
await Promise.all([sendPromise, megolmMessagePromise, memberListPromise]);
|
await Promise.all([sendPromise, megolmMessagePromise, memberListPromise]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @vitest/expect-expect
|
||||||
it("loading the membership list inhibits a later load", async () => {
|
it("loading the membership list inhibits a later load", async () => {
|
||||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||||
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||||
@@ -1408,7 +1306,7 @@ describe("crypto", () => {
|
|||||||
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession);
|
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession);
|
||||||
|
|
||||||
// and finally the megolm message
|
// and finally the megolm message
|
||||||
const megolmMessagePromise = expectSendMegolmMessage(inboundGroupSessionPromise);
|
const megolmMessagePromise = expectSendMegolmMessageEvent(inboundGroupSessionPromise);
|
||||||
|
|
||||||
// kick it off
|
// kick it off
|
||||||
const sendPromise = aliceClient.sendTextMessage(ROOM_ID, "test");
|
const sendPromise = aliceClient.sendTextMessage(ROOM_ID, "test");
|
||||||
@@ -1491,8 +1389,10 @@ describe("crypto", () => {
|
|||||||
|
|
||||||
expect(ev.decryptionFailureReason).toEqual(expectedErrorCode);
|
expect(ev.decryptionFailureReason).toEqual(expectedErrorCode);
|
||||||
|
|
||||||
// `isEncryptedDisabledForUnverifiedDevices` should be true for `m.unverified` and false for other errors.
|
// `decryptionFailureReason` should be `MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE` for `m.unverified`
|
||||||
expect(ev.isEncryptedDisabledForUnverifiedDevices).toEqual(withheldCode === "m.unverified");
|
expect(
|
||||||
|
ev.decryptionFailureReason === DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE,
|
||||||
|
).toEqual(withheldCode === "m.unverified");
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -1500,33 +1400,26 @@ describe("crypto", () => {
|
|||||||
|
|
||||||
describe("key upload request", () => {
|
describe("key upload request", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
vi.useFakeTimers();
|
||||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function awaitKeyUploadRequest(): Promise<{ keysCount: number; fallbackKeysCount: number }> {
|
function awaitKeyUploadRequest(): Promise<{ keysCount: number; fallbackKeysCount: number }> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const listener = (url: string, options: RequestInit) => {
|
fetchMock.modifyRoute("keys-upload", {
|
||||||
const content = JSON.parse(options.body as string);
|
response: (callLog) => {
|
||||||
const keysCount = Object.keys(content?.one_time_keys || {}).length;
|
const content = JSON.parse(callLog.options.body as string);
|
||||||
const fallbackKeysCount = Object.keys(content?.fallback_keys || {}).length;
|
const keysCount = Object.keys(content?.one_time_keys || {}).length;
|
||||||
if (keysCount) resolve({ keysCount, fallbackKeysCount });
|
const fallbackKeysCount = Object.keys(content?.fallback_keys || {}).length;
|
||||||
return {
|
if (keysCount) resolve({ keysCount, fallbackKeysCount });
|
||||||
one_time_key_counts: {
|
return {
|
||||||
// The matrix client does `/upload` requests until 50 keys are uploaded
|
one_time_key_counts: {
|
||||||
// We return here 60 to avoid the `/upload` request loop
|
// The matrix client does `/upload` requests until 50 keys are uploaded
|
||||||
signed_curve25519: keysCount ? 60 : keysCount,
|
// 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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1539,7 +1432,7 @@ describe("crypto", () => {
|
|||||||
|
|
||||||
await syncPromise(aliceClient);
|
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;
|
const { keysCount, fallbackKeysCount } = await uploadPromise;
|
||||||
expect(keysCount).toBeGreaterThan(0);
|
expect(keysCount).toBeGreaterThan(0);
|
||||||
expect(fallbackKeysCount).toBe(0);
|
expect(fallbackKeysCount).toBe(0);
|
||||||
@@ -1553,7 +1446,7 @@ describe("crypto", () => {
|
|||||||
|
|
||||||
// Advance local date to 2 minutes
|
// Advance local date to 2 minutes
|
||||||
// The old crypto only runs the upload every 60 seconds
|
// 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);
|
await syncPromise(aliceClient);
|
||||||
|
|
||||||
@@ -1644,18 +1537,16 @@ describe("crypto", () => {
|
|||||||
|
|
||||||
function awaitKeyQueryRequest(): Promise<Record<string, []>> {
|
function awaitKeyQueryRequest(): Promise<Record<string, []>> {
|
||||||
return new Promise((resolve) => {
|
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(
|
fetchMock.post(
|
||||||
new URL("/_matrix/client/v3/keys/query", aliceClient.getHomeserverUrl()).toString(),
|
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;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1694,8 +1585,7 @@ describe("crypto", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("Get devices from tracked users", async () => {
|
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`.
|
vi.useFakeTimers();
|
||||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
|
||||||
|
|
||||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||||
await startClientAndAwaitFirstSync();
|
await startClientAndAwaitFirstSync();
|
||||||
@@ -1707,12 +1597,12 @@ describe("crypto", () => {
|
|||||||
|
|
||||||
// Advance local date to 2 minutes
|
// Advance local date to 2 minutes
|
||||||
// The old crypto only runs the upload every 60 seconds
|
// 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);
|
await syncPromise(aliceClient);
|
||||||
|
|
||||||
// Old crypto: for alice: run over the `sleep(5)` in `doQueuedQueries` of `DeviceList`
|
// 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`
|
// Old crypto: for alice: run the `processQueryResponseForUser` in `doQueuedQueries` of `DeviceList`
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
@@ -1720,7 +1610,7 @@ describe("crypto", () => {
|
|||||||
await queryPromise;
|
await queryPromise;
|
||||||
|
|
||||||
// Old crypto: for `user`: run over the `sleep(5)` in `doQueuedQueries` of `DeviceList`
|
// 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`
|
// Old crypto: for `user`: run the `processQueryResponseForUser` in `doQueuedQueries` of `DeviceList`
|
||||||
// It will add `@testing_florian1:matrix.org` devices to the DeviceList
|
// It will add `@testing_florian1:matrix.org` devices to the DeviceList
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
@@ -1744,7 +1634,7 @@ describe("crypto", () => {
|
|||||||
* Create a fake secret storage key
|
* Create a fake secret storage key
|
||||||
* Async because `bootstrapSecretStorage` expect an async method
|
* 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
|
keyInfo: {}, // Returning undefined here used to cause a crash
|
||||||
privateKey: Uint8Array.of(32, 33),
|
privateKey: Uint8Array.of(32, 33),
|
||||||
});
|
});
|
||||||
@@ -1762,7 +1652,7 @@ describe("crypto", () => {
|
|||||||
* https://spec.matrix.org/v1.6/client-server-api/#put_matrixclientv3useruseridaccount_datatype
|
* https://spec.matrix.org/v1.6/client-server-api/#put_matrixclientv3useruseridaccount_datatype
|
||||||
*/
|
*/
|
||||||
async function awaitCrossSigningKeyUpload(key: string): Promise<Record<string, {}>> {
|
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;
|
return content.encrypted;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1774,10 +1664,7 @@ describe("crypto", () => {
|
|||||||
async function awaitSecretStorageKeyStoredInAccountData(): Promise<string> {
|
async function awaitSecretStorageKeyStoredInAccountData(): Promise<string> {
|
||||||
// eslint-disable-next-line no-constant-condition
|
// eslint-disable-next-line no-constant-condition
|
||||||
while (true) {
|
while (true) {
|
||||||
const content = await accountDataAccumulator.interceptSetAccountData(":type(m.secret_storage.*)", {
|
const content = await accountDataAccumulator.waitForAccountData("m.secret_storage.*");
|
||||||
repeat: 1,
|
|
||||||
overwriteRoutes: true,
|
|
||||||
});
|
|
||||||
if (content.key) {
|
if (content.key) {
|
||||||
return content.key;
|
return content.key;
|
||||||
}
|
}
|
||||||
@@ -1785,10 +1672,7 @@ describe("crypto", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function awaitMegolmBackupKeyUpload(): Promise<Record<string, {}>> {
|
async function awaitMegolmBackupKeyUpload(): Promise<Record<string, {}>> {
|
||||||
const content = await accountDataAccumulator.interceptSetAccountData("m.megolm_backup.v1", {
|
const content = await accountDataAccumulator.waitForAccountData("m.megolm_backup.v1");
|
||||||
repeat: 1,
|
|
||||||
overwriteRoutes: true,
|
|
||||||
});
|
|
||||||
return content.encrypted;
|
return content.encrypted;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1809,7 +1693,6 @@ describe("crypto", () => {
|
|||||||
* @param backupVersion - The version of the created backup
|
* @param backupVersion - The version of the created backup
|
||||||
*/
|
*/
|
||||||
async function bootstrapSecurity(backupVersion: string): Promise<void> {
|
async function bootstrapSecurity(backupVersion: string): Promise<void> {
|
||||||
mockSetupCrossSigningRequests();
|
|
||||||
mockSetupMegolmBackupRequests(backupVersion);
|
mockSetupMegolmBackupRequests(backupVersion);
|
||||||
|
|
||||||
// promise which will resolve when a `KeyBackupStatus` event is emitted with `enabled: true`
|
// promise which will resolve when a `KeyBackupStatus` event is emitted with `enabled: true`
|
||||||
@@ -1888,6 +1771,7 @@ describe("crypto", () => {
|
|||||||
|
|
||||||
it("Should create a 4S key", async () => {
|
it("Should create a 4S key", async () => {
|
||||||
accountDataAccumulator.interceptGetAccountData();
|
accountDataAccumulator.interceptGetAccountData();
|
||||||
|
accountDataAccumulator.interceptSetAccountData();
|
||||||
|
|
||||||
const awaitAccountData = awaitAccountDataUpdate("m.secret_storage.default_key");
|
const awaitAccountData = awaitAccountDataUpdate("m.secret_storage.default_key");
|
||||||
|
|
||||||
@@ -2039,8 +1923,7 @@ describe("crypto", () => {
|
|||||||
|
|
||||||
describe("Manage Key Backup", () => {
|
describe("Manage Key Backup", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
vi.useFakeTimers();
|
||||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should be able to restore from 4S after bootstrap", async () => {
|
it("Should be able to restore from 4S after bootstrap", async () => {
|
||||||
@@ -2057,29 +1940,23 @@ describe("crypto", () => {
|
|||||||
const newKey = testData.MEGOLM_SESSION_DATA;
|
const newKey = testData.MEGOLM_SESSION_DATA;
|
||||||
|
|
||||||
const awaitKeyUploaded = new Promise<KeyBackup>((resolve) => {
|
const awaitKeyUploaded = new Promise<KeyBackup>((resolve) => {
|
||||||
fetchMock.put(
|
fetchMock.put("path:/_matrix/client/v3/room_keys/keys", (callLog) => {
|
||||||
"path:/_matrix/client/v3/room_keys/keys",
|
const uploadPayload: KeyBackup = JSON.parse((callLog.options.body as string) ?? "{}");
|
||||||
(url, request) => {
|
resolve(uploadPayload);
|
||||||
const uploadPayload: KeyBackup = JSON.parse((request.body as string) ?? "{}");
|
return {
|
||||||
resolve(uploadPayload);
|
status: 200,
|
||||||
return {
|
body: {
|
||||||
status: 200,
|
count: 1,
|
||||||
body: {
|
etag: "abcdefg",
|
||||||
count: 1,
|
},
|
||||||
etag: "abcdefg",
|
};
|
||||||
},
|
});
|
||||||
};
|
|
||||||
},
|
|
||||||
{
|
|
||||||
overwriteRoutes: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await aliceClient.getCrypto()!.importRoomKeys([newKey]);
|
await aliceClient.getCrypto()!.importRoomKeys([newKey]);
|
||||||
|
|
||||||
// The backup loop waits a random amount of time to avoid different clients firing at the same time.
|
// 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;
|
const keyBackupData = await awaitKeyUploaded;
|
||||||
|
|
||||||
@@ -2107,40 +1984,33 @@ describe("crypto", () => {
|
|||||||
fetchMock.delete(
|
fetchMock.delete(
|
||||||
"express:/_matrix/client/v3/room_keys/version/:version",
|
"express:/_matrix/client/v3/room_keys/version/:version",
|
||||||
(url: string, options: RequestInit) => {
|
(url: string, options: RequestInit) => {
|
||||||
fetchMock.get(
|
fetchMock.modifyRoute("room-keys-version", {
|
||||||
"path:/_matrix/client/v3/room_keys/version",
|
response: {
|
||||||
{
|
|
||||||
status: 404,
|
status: 404,
|
||||||
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
|
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
|
||||||
},
|
},
|
||||||
{ overwriteRoutes: true },
|
});
|
||||||
);
|
|
||||||
resolve();
|
resolve();
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
{ overwriteRoutes: true },
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const newVersion = "2";
|
const newVersion = "2";
|
||||||
fetchMock.post(
|
fetchMock.modifyRoute("post-room-keys-version", {
|
||||||
"path:/_matrix/client/v3/room_keys/version",
|
response: (callLog) => {
|
||||||
(url, request) => {
|
const backupData: KeyBackupInfo = JSON.parse((callLog.options.body as string) ?? "{}");
|
||||||
const backupData: KeyBackupInfo = JSON.parse((request.body as string) ?? "{}");
|
|
||||||
backupData.version = newVersion;
|
backupData.version = newVersion;
|
||||||
backupData.count = 0;
|
backupData.count = 0;
|
||||||
backupData.etag = "zer";
|
backupData.etag = "zer";
|
||||||
|
|
||||||
// update get call with new version
|
// update get call with new version
|
||||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData, {
|
fetchMock.modifyRoute("room-keys-version", { response: backupData });
|
||||||
overwriteRoutes: true,
|
|
||||||
});
|
|
||||||
return {
|
return {
|
||||||
version: backupVersion,
|
version: backupVersion,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{ overwriteRoutes: true },
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const newBackupStatusUpdate = new Promise<void>((resolve) => {
|
const newBackupStatusUpdate = new Promise<void>((resolve) => {
|
||||||
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
|
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
|
||||||
@@ -2195,6 +2065,7 @@ describe("crypto", () => {
|
|||||||
expect(hasCrossSigningKeysForUser).toBe(true);
|
expect(hasCrossSigningKeysForUser).toBe(true);
|
||||||
|
|
||||||
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
|
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.isVerified()).toBe(false);
|
||||||
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
||||||
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
|
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
|
||||||
@@ -2202,15 +2073,15 @@ describe("crypto", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("Cross signing keys are available for a tracked user", async () => {
|
it("Cross signing keys are available for a tracked user", async () => {
|
||||||
// Process Alice keys, old crypto has a sleep(5ms) during the process
|
// Process Alice keys
|
||||||
await jest.advanceTimersByTimeAsync(5);
|
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
// Alice is the local user and should be tracked !
|
// Alice is the local user and should be tracked !
|
||||||
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys(TEST_USER_ID);
|
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys(TEST_USER_ID);
|
||||||
expect(hasCrossSigningKeysForUser).toBe(true);
|
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.isVerified()).toBe(false);
|
||||||
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
||||||
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
|
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
|
||||||
@@ -2221,7 +2092,8 @@ describe("crypto", () => {
|
|||||||
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys("@unknown:xyz");
|
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys("@unknown:xyz");
|
||||||
expect(hasCrossSigningKeysForUser).toBe(false);
|
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.isVerified()).toBe(false);
|
||||||
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
||||||
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
|
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
|
||||||
@@ -2251,6 +2123,7 @@ describe("crypto", () => {
|
|||||||
|
|
||||||
{
|
{
|
||||||
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
|
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
|
||||||
|
expect(verificationStatus.known).toBe(true);
|
||||||
expect(verificationStatus.isVerified()).toBe(false);
|
expect(verificationStatus.isVerified()).toBe(false);
|
||||||
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
||||||
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
|
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
|
||||||
@@ -2291,6 +2164,7 @@ describe("crypto", () => {
|
|||||||
client2?.stopClient();
|
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 () => {
|
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
|
// Alice is in an encrypted room
|
||||||
const encryptionState = mkEncryptionEvent({ algorithm: "m.megolm.v1.aes-sha2" });
|
const encryptionState = mkEncryptionEvent({ algorithm: "m.megolm.v1.aes-sha2" });
|
||||||
@@ -2298,7 +2172,7 @@ describe("crypto", () => {
|
|||||||
await syncPromise(client1);
|
await syncPromise(client1);
|
||||||
|
|
||||||
// Send a message, and expect to get an `m.room.encrypted` event.
|
// 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.
|
// We now replace the client, and allow the new one to resync, *without* the encryption event.
|
||||||
client2 = await replaceClient(client1);
|
client2 = await replaceClient(client1);
|
||||||
@@ -2319,7 +2193,7 @@ describe("crypto", () => {
|
|||||||
// Send a message, and expect to get an `m.room.encrypted` event.
|
// Send a message, and expect to get an `m.room.encrypted` event.
|
||||||
const [, msg1Content] = await Promise.all([
|
const [, msg1Content] = await Promise.all([
|
||||||
client1.sendTextMessage(ROOM_ID, "test1"),
|
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
|
// Replace the state with one which bumps the rotation period. This should be ignored, though it's not
|
||||||
@@ -2338,16 +2212,17 @@ describe("crypto", () => {
|
|||||||
// use a different one.
|
// use a different one.
|
||||||
const [, msg2Content] = await Promise.all([
|
const [, msg2Content] = await Promise.all([
|
||||||
client1.sendTextMessage(ROOM_ID, "test2"),
|
client1.sendTextMessage(ROOM_ID, "test2"),
|
||||||
expectEncryptedSendMessage(),
|
expectEncryptedSendMessageEvent(),
|
||||||
]);
|
]);
|
||||||
expect(msg2Content.session_id).toEqual(msg1Content.session_id);
|
expect(msg2Content.session_id).toEqual(msg1Content.session_id);
|
||||||
const [, msg3Content] = await Promise.all([
|
const [, msg3Content] = await Promise.all([
|
||||||
client1.sendTextMessage(ROOM_ID, "test3"),
|
client1.sendTextMessage(ROOM_ID, "test3"),
|
||||||
expectEncryptedSendMessage(),
|
expectEncryptedSendMessageEvent(),
|
||||||
]);
|
]);
|
||||||
expect(msg3Content.session_id).not.toEqual(msg1Content.session_id);
|
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 () => {
|
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
|
// 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 });
|
const encryptionState = mkEncryptionEvent({ algorithm: "m.megolm.v1.aes-sha2", rotation_period_msgs: 2 });
|
||||||
@@ -2355,7 +2230,7 @@ describe("crypto", () => {
|
|||||||
await syncPromise(client1);
|
await syncPromise(client1);
|
||||||
|
|
||||||
// Send a message, and expect to get an `m.room.encrypted` event.
|
// 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.
|
// We now replace the client, and allow the new one to resync with a *different* encryption event.
|
||||||
client2 = await replaceClient(client1);
|
client2 = await replaceClient(client1);
|
||||||
@@ -2441,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,7 +15,8 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import "fake-indexeddb/auto";
|
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 debug from "debug";
|
||||||
|
|
||||||
import { ClientEvent, createClient, DebugLogger, type MatrixClient, MatrixEvent } from "../../../src";
|
import { ClientEvent, createClient, DebugLogger, type MatrixClient, MatrixEvent } from "../../../src";
|
||||||
@@ -28,7 +29,7 @@ import { emitPromise, EventCounter } from "../../test-utils/test-utils";
|
|||||||
|
|
||||||
describe("Device dehydration", () => {
|
describe("Device dehydration", () => {
|
||||||
it("should rehydrate and dehydrate a device", async () => {
|
it("should rehydrate and dehydrate a device", async () => {
|
||||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
vi.useFakeTimers();
|
||||||
|
|
||||||
const matrixClient = createClient({
|
const matrixClient = createClient({
|
||||||
baseUrl: "http://test.server",
|
baseUrl: "http://test.server",
|
||||||
@@ -59,28 +60,35 @@ describe("Device dehydration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const crypto = matrixClient.getCrypto()!;
|
const crypto = matrixClient.getCrypto()!;
|
||||||
fetchMock.config.overwriteRoutes = true;
|
|
||||||
|
|
||||||
// start dehydration -- we start with no dehydrated device, and we
|
// start dehydration -- we start with no dehydrated device, and we
|
||||||
// store the dehydrated device that we create
|
// store the dehydrated device that we create
|
||||||
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
|
fetchMock.get(
|
||||||
status: 404,
|
"path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",
|
||||||
body: {
|
{
|
||||||
errcode: "M_NOT_FOUND",
|
status: 404,
|
||||||
error: "Not found",
|
body: {
|
||||||
|
errcode: "M_NOT_FOUND",
|
||||||
|
error: "Not found",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
{ name: "get-dehydrated-device" },
|
||||||
|
);
|
||||||
let dehydratedDeviceBody: any;
|
let dehydratedDeviceBody: any;
|
||||||
let dehydrationCount = 0;
|
let dehydrationCount = 0;
|
||||||
let resolveDehydrationPromise: () => void;
|
let resolveDehydrationPromise: () => void;
|
||||||
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", (_, opts) => {
|
fetchMock.put(
|
||||||
dehydratedDeviceBody = JSON.parse(opts.body as string);
|
"path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",
|
||||||
dehydrationCount++;
|
(callLog) => {
|
||||||
if (resolveDehydrationPromise) {
|
dehydratedDeviceBody = JSON.parse(callLog.options.body as string);
|
||||||
resolveDehydrationPromise();
|
dehydrationCount++;
|
||||||
}
|
if (resolveDehydrationPromise) {
|
||||||
return {};
|
resolveDehydrationPromise();
|
||||||
});
|
}
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
{ name: "put-dehydrated-device" },
|
||||||
|
);
|
||||||
await crypto.startDehydration();
|
await crypto.startDehydration();
|
||||||
|
|
||||||
expect(dehydrationCount).toEqual(1);
|
expect(dehydrationCount).toEqual(1);
|
||||||
@@ -91,7 +99,7 @@ describe("Device dehydration", () => {
|
|||||||
const dehydrationPromise = new Promise<void>((resolve, reject) => {
|
const dehydrationPromise = new Promise<void>((resolve, reject) => {
|
||||||
resolveDehydrationPromise = resolve;
|
resolveDehydrationPromise = resolve;
|
||||||
});
|
});
|
||||||
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
|
vi.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
|
||||||
await dehydrationPromise;
|
await dehydrationPromise;
|
||||||
|
|
||||||
expect(dehydrationKeyCachedEventCounter.counter).toEqual(1);
|
expect(dehydrationKeyCachedEventCounter.counter).toEqual(1);
|
||||||
@@ -101,16 +109,18 @@ describe("Device dehydration", () => {
|
|||||||
// restart dehydration -- rehydrate the device that we created above,
|
// restart dehydration -- rehydrate the device that we created above,
|
||||||
// and create a new dehydrated device. We also set `createNewKey`, so
|
// and create a new dehydrated device. We also set `createNewKey`, so
|
||||||
// a new dehydration key will be set
|
// a new dehydration key will be set
|
||||||
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
|
fetchMock.modifyRoute("get-dehydrated-device", {
|
||||||
device_id: dehydratedDeviceBody.device_id,
|
response: {
|
||||||
device_data: dehydratedDeviceBody.device_data,
|
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.
|
// rehydrating should make two calls to the /events endpoint.
|
||||||
// The first time will return a single event, and the second
|
// The first time will return a single event, and the second
|
||||||
// time will return no events (which will signal to the
|
// time will return no events (which will signal to the
|
||||||
// rehydration function that it can stop)
|
// 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 nextBatch = body.next_batch ?? "0";
|
||||||
const events = nextBatch === "0" ? [{ sender: "@alice:localhost", type: "m.dummy", content: {} }] : [];
|
const events = nextBatch === "0" ? [{ sender: "@alice:localhost", type: "m.dummy", content: {} }] : [];
|
||||||
return {
|
return {
|
||||||
@@ -135,28 +145,30 @@ describe("Device dehydration", () => {
|
|||||||
expect(dehydrationKeyCachedEventCounter.counter).toEqual(2);
|
expect(dehydrationKeyCachedEventCounter.counter).toEqual(2);
|
||||||
|
|
||||||
// test that if we get an error when we try to rotate, it emits an event
|
// 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", {
|
fetchMock.modifyRoute("put-dehydrated-device", {
|
||||||
status: 500,
|
response: {
|
||||||
body: {
|
status: 500,
|
||||||
errcode: "M_UNKNOWN",
|
body: {
|
||||||
error: "Unknown error",
|
errcode: "M_UNKNOWN",
|
||||||
|
error: "Unknown error",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const rotationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.DehydratedDeviceRotationError);
|
const rotationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.DehydratedDeviceRotationError);
|
||||||
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
|
vi.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
|
||||||
await rotationErrorEventPromise;
|
await rotationErrorEventPromise;
|
||||||
|
|
||||||
// Restart dehydration, but return an error for GET /dehydrated_device so that rehydration fails.
|
// 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", {
|
fetchMock.modifyRoute("get-dehydrated-device", {
|
||||||
status: 500,
|
response: {
|
||||||
body: {
|
status: 500,
|
||||||
errcode: "M_UNKNOWN",
|
body: {
|
||||||
error: "Unknown error",
|
errcode: "M_UNKNOWN",
|
||||||
|
error: "Unknown error",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", (_, opts) => {
|
fetchMock.modifyRoute("put-dehydrated-device", { response: { body: {} } });
|
||||||
return {};
|
|
||||||
});
|
|
||||||
const rehydrationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.RehydrationError);
|
const rehydrationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.RehydrationError);
|
||||||
await crypto.startDehydration(true);
|
await crypto.startDehydration(true);
|
||||||
await rehydrationErrorEventPromise;
|
await rehydrationErrorEventPromise;
|
||||||
@@ -182,8 +194,8 @@ async function initializeSecretStorage(
|
|||||||
const e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
|
const e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
|
||||||
e2eKeyResponder.addKeyReceiver(userId, e2eKeyReceiver);
|
e2eKeyResponder.addKeyReceiver(userId, e2eKeyReceiver);
|
||||||
const accountData: Map<string, object> = new Map();
|
const accountData: Map<string, object> = new Map();
|
||||||
fetchMock.get("glob:http://*/_matrix/client/v3/user/*/account_data/*", (url, opts) => {
|
fetchMock.get("glob:http://*/_matrix/client/v3/user/*/account_data/*", (callLog) => {
|
||||||
const name = url.split("/").pop()!;
|
const name = callLog.url.split("/").pop()!;
|
||||||
const value = accountData.get(name);
|
const value = accountData.get(name);
|
||||||
if (value) {
|
if (value) {
|
||||||
return value;
|
return value;
|
||||||
@@ -197,9 +209,9 @@ async function initializeSecretStorage(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
fetchMock.put("glob:http://*/_matrix/client/v3/user/*/account_data/*", (url, opts) => {
|
fetchMock.put("glob:http://*/_matrix/client/v3/user/*/account_data/*", (callLog) => {
|
||||||
const name = url.split("/").pop()!;
|
const name = callLog.url.split("/").pop()!;
|
||||||
const value = JSON.parse(opts.body as string);
|
const value = JSON.parse(callLog.options.body as string);
|
||||||
accountData.set(name, value);
|
accountData.set(name, value);
|
||||||
matrixClient.emit(ClientEvent.AccountData, new MatrixEvent({ type: name, content: value }));
|
matrixClient.emit(ClientEvent.AccountData, new MatrixEvent({ type: name, content: value }));
|
||||||
return {};
|
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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fetchMock from "fetch-mock-jest";
|
import fetchMock from "@fetch-mock/vitest";
|
||||||
import "fake-indexeddb/auto";
|
import "fake-indexeddb/auto";
|
||||||
import { IDBFactory } from "fake-indexeddb";
|
import { IDBFactory } from "fake-indexeddb";
|
||||||
import { type Mocked } from "jest-mock";
|
import { type Mocked } from "vitest";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createClient,
|
createClient,
|
||||||
encodeBase64,
|
encodeBase64,
|
||||||
|
type IContent,
|
||||||
type ICreateClientOpts,
|
type ICreateClientOpts,
|
||||||
type IEvent,
|
type IEvent,
|
||||||
type IMegolmSessionData,
|
type IMegolmSessionData,
|
||||||
@@ -36,7 +37,13 @@ import { advanceTimersUntil, awaitDecryption, syncPromise } from "../../test-uti
|
|||||||
import * as testData from "../../test-utils/test-data";
|
import * as testData from "../../test-utils/test-data";
|
||||||
import { type KeyBackupInfo, type KeyBackupSession } from "../../../src/crypto-api/keybackup";
|
import { type KeyBackupInfo, type KeyBackupSession } from "../../../src/crypto-api/keybackup";
|
||||||
import { flushPromises } from "../../test-utils/flushPromises";
|
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";
|
import { type KeyBackup } from "../../../src/rust-crypto/backup.ts";
|
||||||
|
|
||||||
const ROOM_ID = testData.TEST_ROOM_ID;
|
const ROOM_ID = testData.TEST_ROOM_ID;
|
||||||
@@ -69,10 +76,11 @@ function mockUploadEmitter(
|
|||||||
expectedVersion: string,
|
expectedVersion: string,
|
||||||
): TypedEventEmitter<MockKeyUploadEvent, MockKeyUploadEventHandlerMap> {
|
): TypedEventEmitter<MockKeyUploadEvent, MockKeyUploadEventHandlerMap> {
|
||||||
const emitter = new TypedEventEmitter();
|
const emitter = new TypedEventEmitter();
|
||||||
|
fetchMock.removeRoute("mock-upload-emitter");
|
||||||
fetchMock.put(
|
fetchMock.put(
|
||||||
"path:/_matrix/client/v3/room_keys/keys",
|
"path:/_matrix/client/v3/room_keys/keys",
|
||||||
(url, request) => {
|
(callLog) => {
|
||||||
const version = new URLSearchParams(new URL(url).search).get("version");
|
const version = new URLSearchParams(new URL(callLog.url).search).get("version");
|
||||||
if (version != expectedVersion) {
|
if (version != expectedVersion) {
|
||||||
return {
|
return {
|
||||||
status: 403,
|
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;
|
let count = 0;
|
||||||
for (const [roomId, value] of Object.entries(uploadPayload.rooms)) {
|
for (const [roomId, value] of Object.entries(uploadPayload.rooms)) {
|
||||||
for (const sessionId of Object.keys(value.sessions)) {
|
for (const sessionId of Object.keys(value.sessions)) {
|
||||||
@@ -99,9 +107,7 @@ function mockUploadEmitter(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{ name: "mock-upload-emitter" },
|
||||||
overwriteRoutes: true,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
return emitter;
|
return emitter;
|
||||||
}
|
}
|
||||||
@@ -117,12 +123,10 @@ describe("megolm-keys backup", () => {
|
|||||||
let e2eKeyResponder: E2EKeyResponder;
|
let e2eKeyResponder: E2EKeyResponder;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
vi.useFakeTimers();
|
||||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
|
||||||
|
|
||||||
// anything that we don't have a specific matcher for silently returns a 404
|
// anything that we don't have a specific matcher for silently returns a 404
|
||||||
fetchMock.catch(404);
|
fetchMock.catch(404);
|
||||||
fetchMock.config.warnOnFallback = false;
|
|
||||||
|
|
||||||
mockInitialApiRequests(TEST_HOMESERVER_URL);
|
mockInitialApiRequests(TEST_HOMESERVER_URL);
|
||||||
syncResponder = new SyncResponder(TEST_HOMESERVER_URL);
|
syncResponder = new SyncResponder(TEST_HOMESERVER_URL);
|
||||||
@@ -133,15 +137,12 @@ describe("megolm-keys backup", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
if (aliceClient !== undefined) {
|
aliceClient?.stopClient();
|
||||||
await aliceClient.stopClient();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow in-flight things to complete before we tear down the test
|
// Allow in-flight things to complete before we tear down the test
|
||||||
await jest.runAllTimersAsync();
|
await vi.runAllTimersAsync();
|
||||||
|
|
||||||
fetchMock.mockReset();
|
vi.restoreAllMocks();
|
||||||
jest.restoreAllMocks();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
async function initTestClient(opts: Partial<ICreateClientOpts> = {}): Promise<MatrixClient> {
|
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 () => {
|
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
|
// 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") {
|
if (version == "1") {
|
||||||
return testData.CURVE25519_KEY_BACKUP_DATA;
|
return testData.CURVE25519_KEY_BACKUP_DATA;
|
||||||
} else {
|
} else {
|
||||||
@@ -237,11 +238,11 @@ describe("megolm-keys backup", () => {
|
|||||||
|
|
||||||
// Eventually, decryption succeeds.
|
// Eventually, decryption succeeds.
|
||||||
await awaitDecryption(event, { waitOnDecryptionFailure: true });
|
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 () => {
|
it("handles error on backup query gracefully", async () => {
|
||||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
|
||||||
fetchMock.get(
|
fetchMock.get(
|
||||||
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
|
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
|
||||||
@@ -253,9 +254,9 @@ describe("megolm-keys backup", () => {
|
|||||||
syncResponder.sendOrQueueSyncResponse(SYNC_RESPONSE);
|
syncResponder.sendOrQueueSyncResponse(SYNC_RESPONSE);
|
||||||
await flushBackupRequest();
|
await flushBackupRequest();
|
||||||
|
|
||||||
const calls = fetchMock.calls("getKey");
|
const calls = fetchMock.callHistory.calls("getKey");
|
||||||
expect(calls.length).toEqual(1);
|
expect(calls.length).toEqual(1);
|
||||||
expect(calls[0][0]).toEqual(EXPECTED_URL);
|
expect(calls[0].url).toEqual(EXPECTED_URL);
|
||||||
|
|
||||||
await flushBackupRequest();
|
await flushBackupRequest();
|
||||||
|
|
||||||
@@ -274,11 +275,11 @@ describe("megolm-keys backup", () => {
|
|||||||
// Send Alice a message that she won't be able to decrypt
|
// Send Alice a message that she won't be able to decrypt
|
||||||
syncResponder.sendOrQueueSyncResponse(SYNC_RESPONSE);
|
syncResponder.sendOrQueueSyncResponse(SYNC_RESPONSE);
|
||||||
await flushBackupRequest();
|
await flushBackupRequest();
|
||||||
const calls = fetchMock.calls("getKey");
|
const calls = fetchMock.callHistory.calls("getKey");
|
||||||
expect(calls.length).toEqual(1);
|
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
|
// another message
|
||||||
const event2 = { ...testData.ENCRYPTED_EVENT, event_id: "$event2" };
|
const event2 = { ...testData.ENCRYPTED_EVENT, event_id: "$event2" };
|
||||||
@@ -288,7 +289,7 @@ describe("megolm-keys backup", () => {
|
|||||||
};
|
};
|
||||||
syncResponder.sendOrQueueSyncResponse(syncResponse2);
|
syncResponder.sendOrQueueSyncResponse(syncResponse2);
|
||||||
await flushBackupRequest();
|
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 () {
|
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
|
// @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
|
// We need several rooms with several sessions to test chunking
|
||||||
const { response, expectedTotal } = createBackupDownloadResponse([45, 300, 345, 12, 130]);
|
const { response, expectedTotal } = createBackupDownloadResponse([45, 300, 345, 12, 130]);
|
||||||
@@ -380,7 +381,7 @@ describe("megolm-keys backup", () => {
|
|||||||
check!.backupInfo!.version!,
|
check!.backupInfo!.version!,
|
||||||
);
|
);
|
||||||
|
|
||||||
const progressCallback = jest.fn();
|
const progressCallback = vi.fn();
|
||||||
const result = await aliceCrypto.restoreKeyBackup({
|
const result = await aliceCrypto.restoreKeyBackup({
|
||||||
progressCallback,
|
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 () {
|
it("Should continue to process backup if a chunk import fails and report failures", async function () {
|
||||||
const importMockImpl = jest
|
const importMockImpl = vi
|
||||||
.fn()
|
.fn()
|
||||||
.mockImplementationOnce(() => {
|
.mockImplementationOnce(() => {
|
||||||
// Fail to import first chunk
|
// Fail to import first chunk
|
||||||
@@ -427,7 +428,7 @@ describe("megolm-keys backup", () => {
|
|||||||
.mockResolvedValue(undefined);
|
.mockResolvedValue(undefined);
|
||||||
|
|
||||||
// @ts-ignore - mock a private method for testing purpose
|
// @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]);
|
const { response, expectedTotal } = createBackupDownloadResponse([100, 300]);
|
||||||
|
|
||||||
@@ -439,7 +440,7 @@ describe("megolm-keys backup", () => {
|
|||||||
check!.backupInfo!.version!,
|
check!.backupInfo!.version!,
|
||||||
);
|
);
|
||||||
|
|
||||||
const progressCallback = jest.fn();
|
const progressCallback = vi.fn();
|
||||||
const result = await aliceCrypto.restoreKeyBackup({ progressCallback });
|
const result = await aliceCrypto.restoreKeyBackup({ progressCallback });
|
||||||
|
|
||||||
expect(result.total).toStrictEqual(expectedTotal);
|
expect(result.total).toStrictEqual(expectedTotal);
|
||||||
@@ -463,13 +464,13 @@ describe("megolm-keys backup", () => {
|
|||||||
|
|
||||||
it("Should continue if some keys fails to decrypt", async function () {
|
it("Should continue if some keys fails to decrypt", async function () {
|
||||||
// @ts-ignore - mock a private method for testing purpose
|
// @ts-ignore - mock a private method for testing purpose
|
||||||
aliceCrypto.importBackedUpRoomKeys = jest.fn();
|
aliceCrypto.importBackedUpRoomKeys = vi.fn();
|
||||||
|
|
||||||
const decryptionFailureCount = 2;
|
const decryptionFailureCount = 2;
|
||||||
|
|
||||||
const mockDecryptor = {
|
const mockDecryptor = {
|
||||||
// DecryptSessions does not reject on decryption failure, but just skip the key
|
// 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
|
// simulate fail to decrypt 2 keys out of all
|
||||||
const decrypted = [];
|
const decrypted = [];
|
||||||
const keys = Object.keys(sessions);
|
const keys = Object.keys(sessions);
|
||||||
@@ -480,11 +481,11 @@ describe("megolm-keys backup", () => {
|
|||||||
}
|
}
|
||||||
return decrypted;
|
return decrypted;
|
||||||
}),
|
}),
|
||||||
free: jest.fn(),
|
free: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// @ts-ignore - mock a private method for testing purpose
|
// @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]);
|
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 () {
|
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
|
// @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 = {
|
const fullBackup = createFullBackup(
|
||||||
rooms: {
|
testData.MEGOLM_SESSION_DATA.session_id,
|
||||||
[ROOM_ID]: {
|
testData.CURVE25519_KEY_BACKUP_DATA,
|
||||||
sessions: {
|
);
|
||||||
[testData.MEGOLM_SESSION_DATA.session_id]: testData.CURVE25519_KEY_BACKUP_DATA,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
|
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
|
||||||
|
|
||||||
await aliceCrypto.loadSessionBackupPrivateKeyFromSecretStorage();
|
await aliceCrypto.loadSessionBackupPrivateKeyFromSecretStorage();
|
||||||
@@ -526,15 +522,44 @@ describe("megolm-keys backup", () => {
|
|||||||
expect(result.imported).toStrictEqual(1);
|
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 () => {
|
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");
|
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", () => {
|
describe("backupLoop", () => {
|
||||||
it("Alice should upload known keys when backup is enabled", async function () {
|
it("Alice should upload known keys when backup is enabled", async function () {
|
||||||
// 404 means that there is no active backup
|
// 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();
|
aliceClient = await initTestClient();
|
||||||
const aliceCrypto = aliceClient.getCrypto()!;
|
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, {
|
fetchMock.modifyRoute("room-keys-version", {
|
||||||
overwriteRoutes: true,
|
response: { status: 200, body: testData.SIGNED_BACKUP_DATA },
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||||
@@ -581,7 +606,7 @@ describe("megolm-keys backup", () => {
|
|||||||
await aliceCrypto.importRoomKeys(someRoomKeys);
|
await aliceCrypto.importRoomKeys(someRoomKeys);
|
||||||
|
|
||||||
// The backup loop is waiting a random amount of time to avoid different clients firing at the same time.
|
// 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);
|
await Promise.all(uploadPromises);
|
||||||
|
|
||||||
@@ -605,7 +630,7 @@ describe("megolm-keys backup", () => {
|
|||||||
|
|
||||||
await aliceCrypto.importRoomKeys([newKey]);
|
await aliceCrypto.importRoomKeys([newKey]);
|
||||||
|
|
||||||
jest.runAllTimers();
|
vi.runAllTimers();
|
||||||
await newKeyUploadPromise;
|
await newKeyUploadPromise;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -630,7 +655,7 @@ describe("megolm-keys backup", () => {
|
|||||||
const someRoomKeys = testData.MEGOLM_SESSION_DATA_ARRAY;
|
const someRoomKeys = testData.MEGOLM_SESSION_DATA_ARRAY;
|
||||||
|
|
||||||
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, {
|
||||||
overwriteRoutes: true,
|
name: "room-keys-version",
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||||
@@ -640,7 +665,7 @@ describe("megolm-keys backup", () => {
|
|||||||
await aliceCrypto.importRoomKeys(someRoomKeys);
|
await aliceCrypto.importRoomKeys(someRoomKeys);
|
||||||
|
|
||||||
// The backup loop is waiting a random amount of time to avoid different clients firing at the same time.
|
// 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
|
// wait for all keys to be backed up
|
||||||
await remainingZeroPromise;
|
await remainingZeroPromise;
|
||||||
@@ -651,10 +676,7 @@ describe("megolm-keys backup", () => {
|
|||||||
newBackup.version = newBackupVersion;
|
newBackup.version = newBackupVersion;
|
||||||
|
|
||||||
// Let's simulate that a new backup is available by returning error code on key upload
|
// Let's simulate that a new backup is available by returning error code on key upload
|
||||||
|
fetchMock.modifyRoute("room-keys-version", { response: newBackup });
|
||||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, {
|
|
||||||
overwriteRoutes: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// If we import a new key the loop will try to upload to old version, it will
|
// 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
|
// fail then check the current version and switch if trusted
|
||||||
@@ -697,12 +719,12 @@ describe("megolm-keys backup", () => {
|
|||||||
|
|
||||||
await aliceCrypto.importRoomKeys([newKey]);
|
await aliceCrypto.importRoomKeys([newKey]);
|
||||||
|
|
||||||
jest.runAllTimers();
|
vi.runAllTimers();
|
||||||
|
|
||||||
await disableOldBackup;
|
await disableOldBackup;
|
||||||
await enableNewBackup;
|
await enableNewBackup;
|
||||||
|
|
||||||
jest.runAllTimers();
|
vi.runAllTimers();
|
||||||
|
|
||||||
await Promise.all(uploadPromises);
|
await Promise.all(uploadPromises);
|
||||||
await newKeyUploadPromise;
|
await newKeyUploadPromise;
|
||||||
@@ -717,22 +739,14 @@ describe("megolm-keys backup", () => {
|
|||||||
await waitForDeviceList();
|
await waitForDeviceList();
|
||||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
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.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||||
overwriteRoutes: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// on the first key upload attempt, simulate a network failure
|
// on the first key upload attempt, simulate a network failure
|
||||||
const failurePromise = new Promise((resolve) => {
|
const failurePromise = new Promise((resolve) => {
|
||||||
fetchMock.put(
|
fetchMock.putOnce("path:/_matrix/client/v3/room_keys/keys", () => {
|
||||||
"path:/_matrix/client/v3/room_keys/keys",
|
resolve(undefined);
|
||||||
() => {
|
throw new TypeError(`Failed to fetch`);
|
||||||
resolve(undefined);
|
});
|
||||||
throw new TypeError(`Failed to fetch`);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
overwriteRoutes: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// kick the import loop off and wait for the failed request
|
// kick the import loop off and wait for the failed request
|
||||||
@@ -741,27 +755,21 @@ describe("megolm-keys backup", () => {
|
|||||||
|
|
||||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||||
expect(result).toBeTruthy();
|
expect(result).toBeTruthy();
|
||||||
jest.advanceTimersByTime(10 * 60 * 1000);
|
vi.advanceTimersByTime(10 * 60 * 1000);
|
||||||
await failurePromise;
|
await failurePromise;
|
||||||
|
|
||||||
// Fix the endpoint to do successful uploads
|
// Fix the endpoint to do successful uploads
|
||||||
const successPromise = new Promise((resolve) => {
|
const successPromise = new Promise((resolve) => {
|
||||||
fetchMock.put(
|
fetchMock.putOnce("path:/_matrix/client/v3/room_keys/keys", () => {
|
||||||
"path:/_matrix/client/v3/room_keys/keys",
|
resolve(undefined);
|
||||||
() => {
|
return {
|
||||||
resolve(undefined);
|
status: 200,
|
||||||
return {
|
body: {
|
||||||
status: 200,
|
count: 2,
|
||||||
body: {
|
etag: "abcdefg",
|
||||||
count: 2,
|
},
|
||||||
etag: "abcdefg",
|
};
|
||||||
},
|
});
|
||||||
};
|
|
||||||
},
|
|
||||||
{
|
|
||||||
overwriteRoutes: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// check that a `KeyBackupSessionsRemaining` event is emitted with `remaining == 0`
|
// 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
|
// 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 successPromise;
|
||||||
await allKeysUploadedPromise;
|
await allKeysUploadedPromise;
|
||||||
});
|
});
|
||||||
@@ -782,7 +790,7 @@ describe("megolm-keys backup", () => {
|
|||||||
|
|
||||||
it("getActiveSessionBackupVersion() should give correct result", async function () {
|
it("getActiveSessionBackupVersion() should give correct result", async function () {
|
||||||
// 404 means that there is no active backup
|
// 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();
|
aliceClient = await initTestClient();
|
||||||
const aliceCrypto = aliceClient.getCrypto()!;
|
const aliceCrypto = aliceClient.getCrypto()!;
|
||||||
@@ -801,9 +809,7 @@ describe("megolm-keys backup", () => {
|
|||||||
// Serve a backup with no trusted signature
|
// Serve a backup with no trusted signature
|
||||||
const unsignedBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
|
const unsignedBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
|
||||||
delete unsignedBackup.auth_data.signatures;
|
delete unsignedBackup.auth_data.signatures;
|
||||||
fetchMock.get("express:/_matrix/client/v3/room_keys/version", unsignedBackup, {
|
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", unsignedBackup);
|
||||||
overwriteRoutes: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const checked = await aliceCrypto.checkKeyBackupAndEnable();
|
const checked = await aliceCrypto.checkKeyBackupAndEnable();
|
||||||
expect(checked?.backupInfo?.version).toStrictEqual(unsignedBackup.version);
|
expect(checked?.backupInfo?.version).toStrictEqual(unsignedBackup.version);
|
||||||
@@ -813,9 +819,7 @@ describe("megolm-keys backup", () => {
|
|||||||
expect(backupStatus).toBeNull();
|
expect(backupStatus).toBeNull();
|
||||||
|
|
||||||
// Add a valid signature to the backup
|
// Add a valid signature to the backup
|
||||||
fetchMock.get("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
|
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||||
overwriteRoutes: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// check that signalling is working
|
// check that signalling is working
|
||||||
const backupPromise = new Promise<void>((resolve, reject) => {
|
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 () => {
|
it("getKeyBackupInfo() should not return a backup if the active backup has been deleted", async () => {
|
||||||
// 404 means that there is no active backup
|
// 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}`, {});
|
fetchMock.delete(`express:/_matrix/client/v3/room_keys/version/${testData.SIGNED_BACKUP_DATA.version}`, {});
|
||||||
|
|
||||||
aliceClient = await initTestClient();
|
aliceClient = await initTestClient();
|
||||||
@@ -853,11 +857,9 @@ describe("megolm-keys backup", () => {
|
|||||||
expect(await aliceCrypto.getKeyBackupInfo()).toBeNull();
|
expect(await aliceCrypto.getKeyBackupInfo()).toBeNull();
|
||||||
|
|
||||||
// Return now the backup
|
// Return now the backup
|
||||||
fetchMock.get("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
|
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||||
overwriteRoutes: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
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
|
// Delete the backup and we are expecting the key backup to be disabled
|
||||||
const keyBackupStatus = Promise.withResolvers<boolean>();
|
const keyBackupStatus = Promise.withResolvers<boolean>();
|
||||||
@@ -991,7 +993,7 @@ describe("megolm-keys backup", () => {
|
|||||||
await waitForDeviceList();
|
await waitForDeviceList();
|
||||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
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();
|
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||||
expect(result).toBeTruthy();
|
expect(result).toBeTruthy();
|
||||||
@@ -1001,9 +1003,7 @@ describe("megolm-keys backup", () => {
|
|||||||
delete unsignedBackup.auth_data.signatures;
|
delete unsignedBackup.auth_data.signatures;
|
||||||
unsignedBackup.version = "2";
|
unsignedBackup.version = "2";
|
||||||
|
|
||||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", unsignedBackup, {
|
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", unsignedBackup);
|
||||||
overwriteRoutes: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
await aliceCrypto.checkKeyBackupAndEnable();
|
await aliceCrypto.checkKeyBackupAndEnable();
|
||||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
|
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
|
||||||
@@ -1018,7 +1018,7 @@ describe("megolm-keys backup", () => {
|
|||||||
await waitForDeviceList();
|
await waitForDeviceList();
|
||||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
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();
|
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||||
expect(result).toBeTruthy();
|
expect(result).toBeTruthy();
|
||||||
@@ -1028,9 +1028,7 @@ describe("megolm-keys backup", () => {
|
|||||||
const newBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
|
const newBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
|
||||||
newBackup.version = newBackupVersion;
|
newBackup.version = newBackupVersion;
|
||||||
|
|
||||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, {
|
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", newBackup);
|
||||||
overwriteRoutes: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
await aliceCrypto.checkKeyBackupAndEnable();
|
await aliceCrypto.checkKeyBackupAndEnable();
|
||||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(newBackupVersion);
|
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(newBackupVersion);
|
||||||
@@ -1045,25 +1043,19 @@ describe("megolm-keys backup", () => {
|
|||||||
await waitForDeviceList();
|
await waitForDeviceList();
|
||||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
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();
|
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||||
expect(result).toBeTruthy();
|
expect(result).toBeTruthy();
|
||||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
|
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
|
||||||
|
|
||||||
fetchMock.get(
|
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", {
|
||||||
"path:/_matrix/client/v3/room_keys/version",
|
status: 404,
|
||||||
{
|
body: {
|
||||||
status: 404,
|
errcode: "M_NOT_FOUND",
|
||||||
body: {
|
error: "No backup found",
|
||||||
errcode: "M_NOT_FOUND",
|
|
||||||
error: "No backup found",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
});
|
||||||
overwriteRoutes: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const noResult = await aliceCrypto.checkKeyBackupAndEnable();
|
const noResult = await aliceCrypto.checkKeyBackupAndEnable();
|
||||||
expect(noResult).toBeNull();
|
expect(noResult).toBeNull();
|
||||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
|
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
|
||||||
@@ -1072,10 +1064,12 @@ describe("megolm-keys backup", () => {
|
|||||||
|
|
||||||
describe("Backup Changed from other sessions", () => {
|
describe("Backup Changed from other sessions", () => {
|
||||||
beforeEach(async () => {
|
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
|
// 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();
|
aliceClient = await initTestClient();
|
||||||
const aliceCrypto = aliceClient.getCrypto()!;
|
const aliceCrypto = aliceClient.getCrypto()!;
|
||||||
@@ -1108,9 +1102,9 @@ describe("megolm-keys backup", () => {
|
|||||||
|
|
||||||
fetchMock.get(
|
fetchMock.get(
|
||||||
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
|
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
|
||||||
(url, request) => {
|
(callLog) => {
|
||||||
// check that the version is correct
|
// 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") {
|
if (version == "1") {
|
||||||
return testData.CURVE25519_KEY_BACKUP_DATA;
|
return testData.CURVE25519_KEY_BACKUP_DATA;
|
||||||
} else {
|
} 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.
|
// 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];
|
const event = room.getLiveTimeline().getEvents()[0];
|
||||||
await advanceTimersUntil(awaitDecryption(event, { waitOnDecryptionFailure: true }));
|
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
|
// Second suppose now that the backup has changed to version 2
|
||||||
@@ -1146,7 +1140,7 @@ describe("megolm-keys backup", () => {
|
|||||||
version: "2",
|
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
|
// suppose the new key is now known
|
||||||
const aliceCrypto = aliceClient.getCrypto()!;
|
const aliceCrypto = aliceClient.getCrypto()!;
|
||||||
await aliceCrypto.storeSessionBackupPrivateKey(
|
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||||
@@ -1159,11 +1153,10 @@ describe("megolm-keys backup", () => {
|
|||||||
|
|
||||||
const awaitHasQueriedNewBackup: PromiseWithResolvers<void> = Promise.withResolvers<void>();
|
const awaitHasQueriedNewBackup: PromiseWithResolvers<void> = Promise.withResolvers<void>();
|
||||||
|
|
||||||
fetchMock.get(
|
fetchMock.modifyRoute("room-keys", {
|
||||||
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
|
response: (callLog) => {
|
||||||
(url, request) => {
|
|
||||||
// check that the version is correct
|
// 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) {
|
if (version == newBackup.version) {
|
||||||
awaitHasQueriedNewBackup.resolve();
|
awaitHasQueriedNewBackup.resolve();
|
||||||
return testData.CURVE25519_KEY_BACKUP_DATA;
|
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.
|
// 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> = {
|
const newMessage: Partial<IEvent> = {
|
||||||
@@ -1216,7 +1208,7 @@ describe("megolm-keys backup", () => {
|
|||||||
// user will be one).
|
// user will be one).
|
||||||
syncResponder.sendOrQueueSyncResponse({});
|
syncResponder.sendOrQueueSyncResponse({});
|
||||||
// DeviceList has a sleep(5) which we need to make happen
|
// 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
|
// The client should now know about the dummy device
|
||||||
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
|
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ limitations under the License.
|
|||||||
|
|
||||||
import Olm from "@matrix-org/olm";
|
import Olm from "@matrix-org/olm";
|
||||||
import anotherjson from "another-json";
|
import anotherjson from "another-json";
|
||||||
|
import fetchMock from "@fetch-mock/vitest";
|
||||||
|
import { type RouteResponse } from "fetch-mock";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type IContent,
|
type IContent,
|
||||||
@@ -30,6 +32,7 @@ import { type IE2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
|||||||
import { type ISyncResponder } from "../../test-utils/SyncResponder";
|
import { type ISyncResponder } from "../../test-utils/SyncResponder";
|
||||||
import { syncPromise } from "../../test-utils/test-utils";
|
import { syncPromise } from "../../test-utils/test-utils";
|
||||||
import { type KeyBackupInfo } from "../../../src/crypto-api";
|
import { type KeyBackupInfo } from "../../../src/crypto-api";
|
||||||
|
import { logger } from "../../../src/logger";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module
|
* @module
|
||||||
@@ -302,6 +305,9 @@ export function encryptMegolmEventRawPlainText(opts: {
|
|||||||
},
|
},
|
||||||
type: "m.room.encrypted",
|
type: "m.room.encrypted",
|
||||||
unsigned: {},
|
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);
|
await syncPromise(testClient);
|
||||||
return p2pSession;
|
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 "fake-indexeddb/auto";
|
||||||
import { IDBFactory } from "fake-indexeddb";
|
import { IDBFactory } from "fake-indexeddb";
|
||||||
import fetchMock from "fetch-mock-jest";
|
import fetchMock from "@fetch-mock/vitest";
|
||||||
|
|
||||||
import { createClient, IndexedDBCryptoStore } from "../../../src";
|
import { createClient, IndexedDBCryptoStore } from "../../../src";
|
||||||
import { populateStore } from "../../test-utils/test_indexeddb_cryptostore_dump";
|
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 { EMPTY_ACCOUNT_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/empty_account";
|
||||||
import { CryptoEvent } from "../../../src/crypto-api";
|
import { CryptoEvent } from "../../../src/crypto-api";
|
||||||
|
|
||||||
jest.setTimeout(15000);
|
vi.setConfig({ testTimeout: 15000 });
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
// 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 () => {
|
it("should ignore a second call", async () => {
|
||||||
const matrixClient = createClient({
|
const matrixClient = createClient({
|
||||||
baseUrl: "http://test.server",
|
baseUrl: "http://test.server",
|
||||||
@@ -134,10 +135,6 @@ describe("MatrixClient.initRustCrypto", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Libolm Migration", () => {
|
describe("Libolm Migration", () => {
|
||||||
beforeEach(() => {
|
|
||||||
fetchMock.reset();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should migrate from libolm", async () => {
|
it("should migrate from libolm", async () => {
|
||||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", FULL_ACCOUNT_DATASET.backupResponse);
|
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,
|
pickleKey: FULL_ACCOUNT_DATASET.pickleKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
const progressListener = jest.fn();
|
const progressListener = vi.fn();
|
||||||
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
|
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
|
||||||
|
|
||||||
await matrixClient.initRustCrypto();
|
await matrixClient.initRustCrypto();
|
||||||
@@ -326,7 +323,7 @@ describe("MatrixClient.initRustCrypto", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// When we start Rust crypto, potentially triggering an upgrade
|
// When we start Rust crypto, potentially triggering an upgrade
|
||||||
const progressListener = jest.fn();
|
const progressListener = vi.fn();
|
||||||
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
|
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
|
||||||
|
|
||||||
await matrixClient.initRustCrypto();
|
await matrixClient.initRustCrypto();
|
||||||
@@ -478,6 +475,7 @@ describe("MatrixClient.clearStores", () => {
|
|||||||
expect(await indexedDB.databases()).toHaveLength(0);
|
expect(await indexedDB.databases()).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @vitest/expect-expect
|
||||||
it("should not fail in environments without indexedDB", async () => {
|
it("should not fail in environments without indexedDB", async () => {
|
||||||
// eslint-disable-next-line no-global-assign
|
// eslint-disable-next-line no-global-assign
|
||||||
indexedDB = undefined!;
|
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,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fetchMock from "fetch-mock-jest";
|
import fetchMock from "@fetch-mock/vitest";
|
||||||
import "fake-indexeddb/auto";
|
import "fake-indexeddb/auto";
|
||||||
import { IDBFactory } from "fake-indexeddb";
|
import { IDBFactory } from "fake-indexeddb";
|
||||||
import Olm from "@matrix-org/olm";
|
import Olm from "@matrix-org/olm";
|
||||||
@@ -59,7 +59,6 @@ describe("to-device-messages", () => {
|
|||||||
async () => {
|
async () => {
|
||||||
// anything that we don't have a specific matcher for silently returns a 404
|
// anything that we don't have a specific matcher for silently returns a 404
|
||||||
fetchMock.catch(404);
|
fetchMock.catch(404);
|
||||||
fetchMock.config.warnOnFallback = false;
|
|
||||||
|
|
||||||
const homeserverUrl = "https://server.com";
|
const homeserverUrl = "https://server.com";
|
||||||
aliceClient = createClient({
|
aliceClient = createClient({
|
||||||
@@ -100,7 +99,6 @@ describe("to-device-messages", () => {
|
|||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
aliceClient.stopClient();
|
aliceClient.stopClient();
|
||||||
fetchMock.mockReset();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("encryptToDeviceMessages", () => {
|
describe("encryptToDeviceMessages", () => {
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ import "fake-indexeddb/auto";
|
|||||||
|
|
||||||
import anotherjson from "another-json";
|
import anotherjson from "another-json";
|
||||||
import debug from "debug";
|
import debug from "debug";
|
||||||
import fetchMock from "fetch-mock-jest";
|
import fetchMock from "@fetch-mock/vitest";
|
||||||
|
import { type RouteResponse } from "fetch-mock";
|
||||||
import { IDBFactory } from "fake-indexeddb";
|
import { IDBFactory } from "fake-indexeddb";
|
||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
import Olm from "@matrix-org/olm";
|
import Olm from "@matrix-org/olm";
|
||||||
|
|
||||||
import type FetchMock from "fetch-mock";
|
|
||||||
import {
|
import {
|
||||||
createClient,
|
createClient,
|
||||||
DebugLogger,
|
DebugLogger,
|
||||||
@@ -46,8 +46,8 @@ import {
|
|||||||
type Verifier,
|
type Verifier,
|
||||||
VerifierEvent,
|
VerifierEvent,
|
||||||
} from "../../../src/crypto-api/verification";
|
} from "../../../src/crypto-api/verification";
|
||||||
import { escapeRegExp } from "../../../src/utils";
|
import { escapeRegExp, sleep } from "../../../src/utils";
|
||||||
import { awaitDecryption, emitPromise, getSyncResponse, syncPromise } from "../../test-utils/test-utils";
|
import { awaitDecryption, emitPromise, getSyncResponse, syncPromise, waitFor } from "../../test-utils/test-utils";
|
||||||
import { SyncResponder } from "../../test-utils/SyncResponder";
|
import { SyncResponder } from "../../test-utils/SyncResponder";
|
||||||
import {
|
import {
|
||||||
BACKUP_DECRYPTION_KEY_BASE64,
|
BACKUP_DECRYPTION_KEY_BASE64,
|
||||||
@@ -79,11 +79,6 @@ import {
|
|||||||
import { type KeyBackupInfo, CryptoEvent } from "../../../src/crypto-api";
|
import { type KeyBackupInfo, CryptoEvent } from "../../../src/crypto-api";
|
||||||
import { encodeBase64 } from "../../../src/base64";
|
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 () => {
|
beforeAll(async () => {
|
||||||
// we use the libolm primitives in the test, so init the Olm library
|
// we use the libolm primitives in the test, so init the Olm library
|
||||||
await Olm.init();
|
await Olm.init();
|
||||||
@@ -96,6 +91,10 @@ beforeAll(async () => {
|
|||||||
await RustSdkCryptoJs.initAsync();
|
await RustSdkCryptoJs.initAsync();
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
// 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
|
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
|
||||||
@@ -128,7 +127,6 @@ describe("verification", () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// anything that we don't have a specific matcher for silently returns a 404
|
// anything that we don't have a specific matcher for silently returns a 404
|
||||||
fetchMock.catch(404);
|
fetchMock.catch(404);
|
||||||
fetchMock.config.warnOnFallback = false;
|
|
||||||
|
|
||||||
e2eKeyReceiver = new E2EKeyReceiver(TEST_HOMESERVER_URL);
|
e2eKeyReceiver = new E2EKeyReceiver(TEST_HOMESERVER_URL);
|
||||||
e2eKeyResponder = new E2EKeyResponder(TEST_HOMESERVER_URL);
|
e2eKeyResponder = new E2EKeyResponder(TEST_HOMESERVER_URL);
|
||||||
@@ -139,14 +137,12 @@ describe("verification", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
if (aliceClient !== undefined) {
|
aliceClient?.stopClient();
|
||||||
await aliceClient.stopClient();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow in-flight things to complete before we tear down the test
|
// Allow in-flight things to complete before we tear down the test
|
||||||
await jest.runAllTimersAsync();
|
if (vi.isFakeTimers()) {
|
||||||
|
await vi.runAllTimersAsync();
|
||||||
fetchMock.mockReset();
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Outgoing verification requests for another device", () => {
|
describe("Outgoing verification requests for another device", () => {
|
||||||
@@ -154,11 +150,10 @@ describe("verification", () => {
|
|||||||
// pretend that we have another device, which we will verify
|
// pretend that we have another device, which we will verify
|
||||||
e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
|
e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
|
||||||
|
|
||||||
fetchMock.put(
|
fetchMock.put(new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp("m.secret.request")}`), {
|
||||||
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp("m.secret.request")}`),
|
ok: false,
|
||||||
{ ok: false, status: 404 },
|
status: 404,
|
||||||
{ overwriteRoutes: true },
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// test with (1) the default verification method list, (2) a custom verification method list.
|
// test with (1) the default verification method list, (2) a custom verification method list.
|
||||||
@@ -210,7 +205,7 @@ describe("verification", () => {
|
|||||||
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
|
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
|
||||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||||
if (methods !== undefined) {
|
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));
|
expect(new Set(toDeviceMessage.methods)).toEqual(new Set(methods));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,7 +238,7 @@ describe("verification", () => {
|
|||||||
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.accept");
|
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.accept");
|
||||||
const verificationPromise = verifier.verify();
|
const verificationPromise = verifier.verify();
|
||||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||||
jest.advanceTimersByTime(10);
|
vi.advanceTimersByTime(10);
|
||||||
|
|
||||||
requestBody = await sendToDevicePromise;
|
requestBody = await sendToDevicePromise;
|
||||||
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||||
@@ -321,7 +316,7 @@ describe("verification", () => {
|
|||||||
expect(request.otherPartySupportsMethod("m.sas.v1")).toBe(true);
|
expect(request.otherPartySupportsMethod("m.sas.v1")).toBe(true);
|
||||||
|
|
||||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
// 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
|
// And now Alice starts a SAS verification
|
||||||
let sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start");
|
let sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start");
|
||||||
@@ -514,7 +509,7 @@ describe("verification", () => {
|
|||||||
// Rust crypto waits for the 'done' to arrive from the other side.
|
// Rust crypto waits for the 'done' to arrive from the other side.
|
||||||
if (request.phase === VerificationPhase.Done) {
|
if (request.phase === VerificationPhase.Done) {
|
||||||
const userVerificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(TEST_USER_ID);
|
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();
|
expect(userVerificationStatus.isCrossSigningVerified()).toBeTruthy();
|
||||||
await verificationPromise;
|
await verificationPromise;
|
||||||
}
|
}
|
||||||
@@ -637,7 +632,7 @@ describe("verification", () => {
|
|||||||
expect(request.verifier).toBeUndefined();
|
expect(request.verifier).toBeUndefined();
|
||||||
|
|
||||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
// 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
|
// ... but Alice wants to do an SAS verification
|
||||||
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start");
|
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start");
|
||||||
@@ -682,7 +677,7 @@ describe("verification", () => {
|
|||||||
expect(request.verifier).toBeUndefined();
|
expect(request.verifier).toBeUndefined();
|
||||||
|
|
||||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
// 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
|
// ... but the dummy device wants to do an SAS verification
|
||||||
returnToDeviceMessageFromSync(buildSasStartMessage(transactionId));
|
returnToDeviceMessageFromSync(buildSasStartMessage(transactionId));
|
||||||
@@ -735,6 +730,35 @@ describe("verification", () => {
|
|||||||
expect(request.cancellingUserId).toEqual("@alice:localhost");
|
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 () => {
|
it("can cancel during the SAS phase", async () => {
|
||||||
// have alice initiate a verification. She should send a m.key.verification.request
|
// have alice initiate a verification. She should send a m.key.verification.request
|
||||||
const [, request] = await Promise.all([
|
const [, request] = await Promise.all([
|
||||||
@@ -761,7 +785,7 @@ describe("verification", () => {
|
|||||||
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.accept");
|
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.accept");
|
||||||
const verificationPromise = verifier.verify();
|
const verificationPromise = verifier.verify();
|
||||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||||
jest.advanceTimersByTime(10);
|
vi.advanceTimersByTime(10);
|
||||||
await sendToDevicePromise;
|
await sendToDevicePromise;
|
||||||
|
|
||||||
// now we unceremoniously cancel. We expect the verificatationPromise to reject.
|
// now we unceremoniously cancel. We expect the verificatationPromise to reject.
|
||||||
@@ -906,19 +930,16 @@ describe("verification", () => {
|
|||||||
function awaitRoomMessageRequest(): Promise<IContent> {
|
function awaitRoomMessageRequest(): Promise<IContent> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
// Case of unencrypted message of the new crypto
|
// Case of unencrypted message of the new crypto
|
||||||
fetchMock.put(
|
fetchMock.put("express:/_matrix/client/v3/rooms/:roomId/send/m.room.message/:txId", (callLog) => {
|
||||||
"express:/_matrix/client/v3/rooms/:roomId/send/m.room.message/:txId",
|
resolve(JSON.parse(callLog.options.body as string));
|
||||||
(url: string, options: RequestInit) => {
|
return { event_id: "$YUwRidLecu:example.com" };
|
||||||
resolve(JSON.parse(options.body as string));
|
});
|
||||||
return { event_id: "$YUwRidLecu:example.com" };
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Case of encrypted message of the old crypto
|
// Case of encrypted message of the old crypto
|
||||||
fetchMock.put(
|
fetchMock.put(
|
||||||
"express:/_matrix/client/v3/rooms/:roomId/send/m.room.encrypted/:txId",
|
"express:/_matrix/client/v3/rooms/:roomId/send/m.room.encrypted/:txId",
|
||||||
async (url: string, options: RequestInit) => {
|
async (callLog) => {
|
||||||
const encryptedMessage = JSON.parse(options.body as string);
|
const encryptedMessage = JSON.parse(callLog.options.body as string);
|
||||||
const event = new MatrixEvent({
|
const event = new MatrixEvent({
|
||||||
content: encryptedMessage,
|
content: encryptedMessage,
|
||||||
type: "m.room.encrypted",
|
type: "m.room.encrypted",
|
||||||
@@ -941,7 +962,7 @@ describe("verification", () => {
|
|||||||
|
|
||||||
// In `DeviceList#doQueuedQueries`, the key download response is processed every 5ms
|
// In `DeviceList#doQueuedQueries`, the key download response is processed every 5ms
|
||||||
// 5ms by users, ie Bob and Alice
|
// 5ms by users, ie Bob and Alice
|
||||||
await jest.advanceTimersByTimeAsync(10);
|
await vi.advanceTimersByTimeAsync(10);
|
||||||
|
|
||||||
const messageRequestPromise = awaitRoomMessageRequest();
|
const messageRequestPromise = awaitRoomMessageRequest();
|
||||||
const verificationRequest = await aliceClient
|
const verificationRequest = await aliceClient
|
||||||
@@ -1051,7 +1072,14 @@ describe("verification", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("ignores old verification requests", async () => {
|
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);
|
aliceClient.on(CryptoEvent.VerificationRequestReceived, eventHandler);
|
||||||
|
|
||||||
const verificationRequestEvent = createVerificationRequestEvent();
|
const verificationRequestEvent = createVerificationRequestEvent();
|
||||||
@@ -1065,6 +1093,16 @@ describe("verification", () => {
|
|||||||
const matrixEvent = room.getLiveTimeline().getEvents()[0];
|
const matrixEvent = room.getLiveTimeline().getEvents()[0];
|
||||||
expect(matrixEvent.getId()).toEqual(verificationRequestEvent.event_id);
|
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
|
// check that an event has not been raised, and that the request is not found
|
||||||
expect(eventHandler).not.toHaveBeenCalled();
|
expect(eventHandler).not.toHaveBeenCalled();
|
||||||
expect(
|
expect(
|
||||||
@@ -1072,6 +1110,29 @@ describe("verification", () => {
|
|||||||
).not.toBeDefined();
|
).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 () => {
|
it("Plaintext verification request from Bob to Alice", async () => {
|
||||||
// Add verification request from Bob to Alice in the DM between them
|
// Add verification request from Bob to Alice in the DM between them
|
||||||
returnRoomMessageFromSync(TEST_ROOM_ID, createVerificationRequestEvent());
|
returnRoomMessageFromSync(TEST_ROOM_ID, createVerificationRequestEvent());
|
||||||
@@ -1116,7 +1177,7 @@ describe("verification", () => {
|
|||||||
returnToDeviceMessageFromSync(toDeviceEvent);
|
returnToDeviceMessageFromSync(toDeviceEvent);
|
||||||
|
|
||||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
// 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
|
// Wait for the request to be decrypted
|
||||||
const request1 = await requestEventPromise;
|
const request1 = await requestEventPromise;
|
||||||
@@ -1153,7 +1214,7 @@ describe("verification", () => {
|
|||||||
expect(matrixEvent.getContent().msgtype).toEqual("m.bad.encrypted");
|
expect(matrixEvent.getContent().msgtype).toEqual("m.bad.encrypted");
|
||||||
|
|
||||||
// Advance time by 5mins, the verification request should be ignored after that
|
// 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
|
// Send Bob the room keys
|
||||||
returnToDeviceMessageFromSync(toDeviceEvent);
|
returnToDeviceMessageFromSync(toDeviceEvent);
|
||||||
@@ -1219,7 +1280,7 @@ describe("verification", () => {
|
|||||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse([TEST_USER_ID]));
|
syncResponder.sendOrQueueSyncResponse(getSyncResponse([TEST_USER_ID]));
|
||||||
await syncPromise(aliceClient);
|
await syncPromise(aliceClient);
|
||||||
// DeviceList has a sleep(5) which we need to make happen
|
// 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
|
// The client should now know about the olm device
|
||||||
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
|
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
|
||||||
@@ -1231,11 +1292,10 @@ describe("verification", () => {
|
|||||||
testOlmAccount?.free();
|
testOlmAccount?.free();
|
||||||
|
|
||||||
// Allow in-flight things to complete before we tear down the test
|
// Allow in-flight things to complete before we tear down the test
|
||||||
await jest.runAllTimersAsync();
|
await vi.runAllTimersAsync();
|
||||||
|
|
||||||
fetchMock.mockReset();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @vitest/expect-expect
|
||||||
it("Should request cross signing keys after verification", async () => {
|
it("Should request cross signing keys after verification", async () => {
|
||||||
const requestPromises = mockSecretRequestAndGetPromises();
|
const requestPromises = mockSecretRequestAndGetPromises();
|
||||||
|
|
||||||
@@ -1353,11 +1413,11 @@ describe("verification", () => {
|
|||||||
*/
|
*/
|
||||||
async function retrieveBackupPrivateKeyWithDelay(): Promise<Uint8Array | null> {
|
async function retrieveBackupPrivateKeyWithDelay(): Promise<Uint8Array | null> {
|
||||||
// We are lacking a way to signal that the secret has been received, so we wait a bit..
|
// 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) => {
|
await new Promise((resolve) => {
|
||||||
setTimeout(resolve, 500);
|
setTimeout(resolve, 500);
|
||||||
});
|
});
|
||||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
vi.useFakeTimers();
|
||||||
|
|
||||||
return aliceClient.getCrypto()!.getSessionBackupPrivateKey();
|
return aliceClient.getCrypto()!.getSessionBackupPrivateKey();
|
||||||
}
|
}
|
||||||
@@ -1390,27 +1450,21 @@ describe("verification", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const expectBackupCheck = new Promise((resolve) => {
|
const expectBackupCheck = new Promise((resolve) => {
|
||||||
fetchMock.get(
|
fetchMock.get("express:/_matrix/client/v3/room_keys/version", (callLog) => {
|
||||||
"express:/_matrix/client/v3/room_keys/version",
|
resolve(undefined);
|
||||||
(url, request) => {
|
if (expectBackup instanceof MatrixError) {
|
||||||
resolve(undefined);
|
return {
|
||||||
if (expectBackup instanceof MatrixError) {
|
status: expectBackup.httpStatus,
|
||||||
return {
|
body: expectBackup.data,
|
||||||
status: expectBackup.httpStatus,
|
};
|
||||||
body: expectBackup.data,
|
}
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expectBackup instanceof Error) {
|
if (expectBackup instanceof Error) {
|
||||||
return Promise.reject(expectBackup);
|
return Promise.reject(expectBackup);
|
||||||
}
|
}
|
||||||
|
|
||||||
return expectBackup;
|
return expectBackup;
|
||||||
},
|
});
|
||||||
{
|
|
||||||
overwriteRoutes: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", CURVE25519_KEY_BACKUP_DATA);
|
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", CURVE25519_KEY_BACKUP_DATA);
|
||||||
@@ -1491,7 +1545,7 @@ describe("verification", () => {
|
|||||||
// user will be one).
|
// user will be one).
|
||||||
syncResponder.sendOrQueueSyncResponse({});
|
syncResponder.sendOrQueueSyncResponse({});
|
||||||
// DeviceList has a sleep(5) which we need to make happen
|
// 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
|
// The client should now know about the dummy device
|
||||||
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
|
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
|
||||||
@@ -1525,8 +1579,8 @@ function expectSendToDeviceMessage(msgtype: string): Promise<{ messages: any }>
|
|||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
fetchMock.putOnce(
|
fetchMock.putOnce(
|
||||||
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp(msgtype)}`),
|
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp(msgtype)}`),
|
||||||
(url: string, opts: RequestInit): FetchMock.MockResponse => {
|
(callLog): RouteResponse => {
|
||||||
resolve(JSON.parse(opts.body as string));
|
resolve(JSON.parse(callLog.options.body as string));
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -1547,29 +1601,25 @@ function mockSecretRequestAndGetPromises(): Map<string, Promise<string>> {
|
|||||||
const uskRequestResolvers = Promise.withResolvers<string>();
|
const uskRequestResolvers = Promise.withResolvers<string>();
|
||||||
const backupKeyRequestResolvers = Promise.withResolvers<string>();
|
const backupKeyRequestResolvers = Promise.withResolvers<string>();
|
||||||
|
|
||||||
fetchMock.put(
|
fetchMock.put(new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/m.secret.request`), (callLog): RouteResponse => {
|
||||||
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/m.secret.request`),
|
const messages = JSON.parse(callLog.options.body as string).messages[TEST_USER_ID];
|
||||||
(url: string, opts: RequestInit): FetchMock.MockResponse => {
|
// rust crypto broadcasts to all devices, old crypto to a specific device, take the first one
|
||||||
const messages = JSON.parse(opts.body as string).messages[TEST_USER_ID];
|
const content = Object.values(messages)[0] as any;
|
||||||
// rust crypto broadcasts to all devices, old crypto to a specific device, take the first one
|
if (content.action == "request") {
|
||||||
const content = Object.values(messages)[0] as any;
|
const name = content.name;
|
||||||
if (content.action == "request") {
|
const requestId = content.request_id;
|
||||||
const name = content.name;
|
if (name == "m.cross_signing.user_signing") {
|
||||||
const requestId = content.request_id;
|
uskRequestResolvers.resolve(requestId);
|
||||||
if (name == "m.cross_signing.user_signing") {
|
} else if (name == "m.cross_signing.master") {
|
||||||
uskRequestResolvers.resolve(requestId);
|
mskRequestResolvers.resolve(requestId);
|
||||||
} else if (name == "m.cross_signing.master") {
|
} else if (name == "m.cross_signing.self_signing") {
|
||||||
mskRequestResolvers.resolve(requestId);
|
sskRequestResolvers.resolve(requestId);
|
||||||
} else if (name == "m.cross_signing.self_signing") {
|
} else if (name == "m.megolm_backup.v1") {
|
||||||
sskRequestResolvers.resolve(requestId);
|
backupKeyRequestResolvers.resolve(requestId);
|
||||||
} else if (name == "m.megolm_backup.v1") {
|
|
||||||
backupKeyRequestResolvers.resolve(requestId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {};
|
}
|
||||||
},
|
return {};
|
||||||
{ overwriteRoutes: true },
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const promiseMap = new Map<string, Promise<string>>();
|
const promiseMap = new Map<string, Promise<string>>();
|
||||||
promiseMap.set("m.cross_signing.master", mskRequestResolvers.promise);
|
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();
|
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
|
// @ts-ignore
|
||||||
client.clientOpts.threadSupport = true;
|
client.clientOpts.threadSupport = true;
|
||||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||||
@@ -696,12 +696,12 @@ describe("MatrixClient event timelines", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all([
|
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(),
|
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
|
// @ts-ignore
|
||||||
client.clientOpts.threadSupport = true;
|
client.clientOpts.threadSupport = true;
|
||||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||||
@@ -723,7 +723,7 @@ describe("MatrixClient event timelines", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all([
|
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(),
|
httpBackend.flushAllExpected(),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -2044,6 +2044,7 @@ describe("MatrixClient event timelines", function () {
|
|||||||
expect(timeline!.getEvents()[1]!.event).toEqual(THREAD_REPLY);
|
expect(timeline!.getEvents()[1]!.event).toEqual(THREAD_REPLY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @vitest/expect-expect
|
||||||
it("in stable mode", async () => {
|
it("in stable mode", async () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
client.clientOpts.threadSupport = true;
|
client.clientOpts.threadSupport = true;
|
||||||
|
|||||||
@@ -347,6 +347,7 @@ describe("MatrixClient", function () {
|
|||||||
expect((await prom).room_id).toBe(roomId);
|
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 () {
|
it("should no-op if you've already knocked a room", function () {
|
||||||
const room = new Room(roomId, client, userId);
|
const room = new Room(roomId, client, userId);
|
||||||
|
|
||||||
@@ -380,23 +381,16 @@ describe("MatrixClient", function () {
|
|||||||
[
|
[
|
||||||
403,
|
403,
|
||||||
{ errcode: "M_FORBIDDEN", error: "You don't have permission to knock" },
|
{ errcode: "M_FORBIDDEN", error: "You don't have permission to knock" },
|
||||||
"[M_FORBIDDEN: MatrixError: [403] You don't have permission to knock]",
|
"MatrixError: [403] You don't have permission to knock",
|
||||||
],
|
|
||||||
[
|
|
||||||
500,
|
|
||||||
{ errcode: "INTERNAL_SERVER_ERROR" },
|
|
||||||
"[INTERNAL_SERVER_ERROR: MatrixError: [500] Unknown message]",
|
|
||||||
],
|
],
|
||||||
|
[500, { errcode: "INTERNAL_SERVER_ERROR" }, "MatrixError: [500] Unknown message"],
|
||||||
];
|
];
|
||||||
|
|
||||||
it.each(testCases)("should handle %s error", async (code, { errcode, error }, snapshot) => {
|
it.each(testCases)("should handle %s error", async (code, { errcode, error }, snapshot) => {
|
||||||
httpBackend.when("POST", "/knock/" + encodeURIComponent(roomId)).respond(code, { errcode, error });
|
httpBackend.when("POST", "/knock/" + encodeURIComponent(roomId)).respond(code, { errcode, error });
|
||||||
|
|
||||||
const prom = client.knockRoom(roomId);
|
const prom = client.knockRoom(roomId);
|
||||||
await Promise.all([
|
await Promise.all([httpBackend.flushAllExpected(), expect(prom).rejects.toThrow(snapshot)]);
|
||||||
httpBackend.flushAllExpected(),
|
|
||||||
expect(prom).rejects.toMatchInlineSnapshot(snapshot),
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1198,7 +1192,7 @@ describe("MatrixClient", function () {
|
|||||||
describe("logout", () => {
|
describe("logout", () => {
|
||||||
it("should abort pending requests when called with stopClient=true", async () => {
|
it("should abort pending requests when called with stopClient=true", async () => {
|
||||||
httpBackend.when("POST", "/logout").respond(200, {});
|
httpBackend.when("POST", "/logout").respond(200, {});
|
||||||
const fn = jest.fn();
|
const fn = vi.fn();
|
||||||
client.http.request(Method.Get, "/test").catch(fn);
|
client.http.request(Method.Get, "/test").catch(fn);
|
||||||
client.logout(true);
|
client.logout(true);
|
||||||
await httpBackend.flush(undefined);
|
await httpBackend.flush(undefined);
|
||||||
@@ -1326,7 +1320,7 @@ describe("MatrixClient", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should always fetch capabilities and then cache", async () => {
|
it("should always fetch capabilities and then cache", async () => {
|
||||||
@@ -1397,6 +1391,7 @@ describe("MatrixClient", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("publicRooms", () => {
|
describe("publicRooms", () => {
|
||||||
|
// eslint-disable-next-line @vitest/expect-expect
|
||||||
it("should use GET request if no server or filter is specified", () => {
|
it("should use GET request if no server or filter is specified", () => {
|
||||||
httpBackend.when("GET", "/publicRooms").respond(200, {});
|
httpBackend.when("GET", "/publicRooms").respond(200, {});
|
||||||
client.publicRooms({});
|
client.publicRooms({});
|
||||||
@@ -1585,7 +1580,7 @@ describe("MatrixClient", function () {
|
|||||||
|
|
||||||
describe("setSyncPresence", () => {
|
describe("setSyncPresence", () => {
|
||||||
it("should pass calls through to the underlying sync api", () => {
|
it("should pass calls through to the underlying sync api", () => {
|
||||||
const setPresence = jest.fn();
|
const setPresence = vi.fn();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
client.syncApi = { setPresence };
|
client.syncApi = { setPresence };
|
||||||
client.setSyncPresence(SetPresence.Unavailable);
|
client.setSyncPresence(SetPresence.Unavailable);
|
||||||
@@ -1594,6 +1589,7 @@ describe("MatrixClient", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("sendTyping", () => {
|
describe("sendTyping", () => {
|
||||||
|
// eslint-disable-next-line @vitest/expect-expect
|
||||||
it("should bail early for guests", async () => {
|
it("should bail early for guests", async () => {
|
||||||
client.setGuest(true);
|
client.setGuest(true);
|
||||||
await client.sendTyping("!room:server", true, 100);
|
await client.sendTyping("!room:server", true, 100);
|
||||||
@@ -1851,6 +1847,7 @@ describe("MatrixClient", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("setRoomMutePushRule", () => {
|
describe("setRoomMutePushRule", () => {
|
||||||
|
// eslint-disable-next-line @vitest/expect-expect
|
||||||
it("should set room push rule to muted", async () => {
|
it("should set room push rule to muted", async () => {
|
||||||
const roomId = "!roomId:server";
|
const roomId = "!roomId:server";
|
||||||
const client = new MatrixClient({
|
const client = new MatrixClient({
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ describe("MatrixClient opts", function () {
|
|||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
Promise.all([client.sendTextMessage("!foo:bar", "a body", "txn1"), httpBackend.flush("/txn1", 1)]),
|
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 () => {
|
it("shouldn't queue events", async () => {
|
||||||
|
|||||||
@@ -720,7 +720,7 @@ describe("MatrixClient room timelines", function () {
|
|||||||
} else {
|
} else {
|
||||||
reject(new Error("TestError: Timed out while waiting for `RoomEvent.TimelineReset` to fire."));
|
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 () => {
|
room.on(RoomEvent.TimelineReset, async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import "fake-indexeddb/auto";
|
import "fake-indexeddb/auto";
|
||||||
import fetchMock from "fetch-mock-jest";
|
import fetchMock from "@fetch-mock/vitest";
|
||||||
|
|
||||||
import { type MatrixClient, ClientEvent, createClient, SyncState } from "../../src";
|
import { type MatrixClient, ClientEvent, createClient, SyncState } from "../../src";
|
||||||
|
|
||||||
@@ -83,8 +83,7 @@ describe("MatrixClient syncing errors", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should retry, until errors are solved.", async () => {
|
it("should retry, until errors are solved.", async () => {
|
||||||
jest.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
fetchMock.config.overwriteRoutes = false;
|
|
||||||
fetchMock
|
fetchMock
|
||||||
.getOnce("end:versions", {}) // first version check without credentials needs to succeed
|
.getOnce("end:versions", {}) // first version check without credentials needs to succeed
|
||||||
.getOnce("end:versions", 429) // second version check fails with 429 triggering another retry
|
.getOnce("end:versions", 429) // second version check fails with 429 triggering another retry
|
||||||
@@ -105,19 +104,18 @@ describe("MatrixClient syncing errors", () => {
|
|||||||
|
|
||||||
await client!.startClient();
|
await client!.startClient();
|
||||||
expect(await syncEvents[0].promise).toBe(SyncState.Error);
|
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);
|
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);
|
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);
|
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);
|
expect(await syncEvents[4].promise).toBe(SyncState.Syncing);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should stop sync keep alive when client is stopped.", async () => {
|
it("should stop sync keep alive when client is stopped.", async () => {
|
||||||
jest.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
fetchMock.config.overwriteRoutes = false;
|
|
||||||
fetchMock
|
fetchMock
|
||||||
.get("end:capabilities", {})
|
.get("end:capabilities", {})
|
||||||
.getOnce("end:versions", {}) // first version check without credentials needs to succeed
|
.getOnce("end:versions", {}) // first version check without credentials needs to succeed
|
||||||
@@ -146,9 +144,9 @@ describe("MatrixClient syncing errors", () => {
|
|||||||
|
|
||||||
const syntState = await firstSyncEvent.promise;
|
const syntState = await firstSyncEvent.promise;
|
||||||
expect(syntState).toBe(SyncState.Error);
|
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)));
|
const timeoutPromise = makeQueryablePromise(new Promise<void>((res) => setTimeout(res, 1)));
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ describe("MatrixClient syncing", () => {
|
|||||||
presence: {},
|
presence: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @vitest/expect-expect
|
||||||
it("should /sync after /pushrules and /filter.", async () => {
|
it("should /sync after /pushrules and /filter.", async () => {
|
||||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||||
|
|
||||||
@@ -501,7 +502,7 @@ describe("MatrixClient syncing", () => {
|
|||||||
})
|
})
|
||||||
.respond(200, syncData);
|
.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 });
|
client!.startClient({ initialSyncLimit: 1 });
|
||||||
|
|
||||||
return httpBackend!.flushAllExpected();
|
return httpBackend!.flushAllExpected();
|
||||||
@@ -994,7 +995,7 @@ describe("MatrixClient syncing", () => {
|
|||||||
roomVersion: "org.matrix.msc2716v3",
|
roomVersion: "org.matrix.msc2716v3",
|
||||||
},
|
},
|
||||||
].forEach((testMeta) => {
|
].forEach((testMeta) => {
|
||||||
// eslint-disable-next-line jest/valid-title
|
// eslint-disable-next-line @vitest/valid-title
|
||||||
describe(testMeta.label, () => {
|
describe(testMeta.label, () => {
|
||||||
const roomCreateEvent = utils.mkEvent({
|
const roomCreateEvent = utils.mkEvent({
|
||||||
type: "m.room.create",
|
type: "m.room.create",
|
||||||
@@ -1835,7 +1836,7 @@ describe("MatrixClient syncing", () => {
|
|||||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||||
|
|
||||||
const room = client!.getRoom(roomOne);
|
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);
|
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" } });
|
const eventB2 = new MatrixEvent({ type: "b", content: { body: "2" } });
|
||||||
|
|
||||||
client!.store.storeAccountDataEvents([eventA1, eventB1]);
|
client!.store.storeAccountDataEvents([eventA1, eventB1]);
|
||||||
const fn = jest.fn();
|
const fn = vi.fn();
|
||||||
client!.on(ClientEvent.AccountData, fn);
|
client!.on(ClientEvent.AccountData, fn);
|
||||||
|
|
||||||
httpBackend!.when("GET", "/sync").respond(200, {
|
httpBackend!.when("GET", "/sync").respond(200, {
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ describe("Notification count fixing", () => {
|
|||||||
|
|
||||||
client!.startClient({ threadSupport: true });
|
client!.startClient({ threadSupport: true });
|
||||||
const room = new Room(roomId, client!, selfUserId);
|
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({
|
const event = new MatrixEvent({
|
||||||
room_id: roomId,
|
room_id: roomId,
|
||||||
@@ -77,7 +77,7 @@ describe("Notification count fixing", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.spyOn(event, "getPushActions").mockReturnValue({
|
vi.spyOn(event, "getPushActions").mockReturnValue({
|
||||||
notify: true,
|
notify: true,
|
||||||
tweaks: {},
|
tweaks: {},
|
||||||
});
|
});
|
||||||
@@ -123,7 +123,7 @@ describe("MatrixClient syncing", () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const room = new Room(roomId, client!, selfUserId);
|
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 thread = mkThread({ room, client: client!, authorId: selfUserId, participantUserIds: [selfUserId] });
|
||||||
const threadReply = thread.events.at(-1)!;
|
const threadReply = thread.events.at(-1)!;
|
||||||
@@ -143,7 +143,7 @@ describe("MatrixClient syncing", () => {
|
|||||||
|
|
||||||
const reactionEventId = `$9-${Math.random()}-${Math.random()}`;
|
const reactionEventId = `$9-${Math.random()}-${Math.random()}`;
|
||||||
let lastEvent: MatrixEvent | null = null;
|
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;
|
lastEvent = event as MatrixEvent;
|
||||||
return { event_id: reactionEventId };
|
return { event_id: reactionEventId };
|
||||||
});
|
});
|
||||||
@@ -195,7 +195,7 @@ describe("MatrixClient syncing", () => {
|
|||||||
})
|
})
|
||||||
.respond(200, syncData);
|
.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 });
|
client!.startClient({ initialSyncLimit: 1 });
|
||||||
|
|
||||||
await httpBackend!.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
|
|||||||
@@ -14,9 +14,8 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { QrCodeData, QrCodeMode } from "@matrix-org/matrix-sdk-crypto-wasm";
|
import { QrCodeData, QrCodeIntent } from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||||
import { mocked } from "jest-mock";
|
import fetchMock from "@fetch-mock/vitest";
|
||||||
import fetchMock from "fetch-mock-jest";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MSC4108FailureReason,
|
MSC4108FailureReason,
|
||||||
@@ -40,7 +39,7 @@ import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
|
|||||||
function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled: boolean }): MatrixClient {
|
function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled: boolean }): MatrixClient {
|
||||||
const baseUrl = "https://example.com";
|
const baseUrl = "https://example.com";
|
||||||
const crypto = {
|
const crypto = {
|
||||||
exportSecretsForQrLogin: jest.fn(),
|
exportSecretsForQrLogin: vi.fn(),
|
||||||
};
|
};
|
||||||
const client = {
|
const client = {
|
||||||
doesServerSupportUnstableFeature(feature: string) {
|
doesServerSupportUnstableFeature(feature: string) {
|
||||||
@@ -54,9 +53,9 @@ function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled
|
|||||||
},
|
},
|
||||||
baseUrl,
|
baseUrl,
|
||||||
getDomain: () => "example.com",
|
getDomain: () => "example.com",
|
||||||
getDevice: jest.fn(),
|
getDevice: vi.fn(),
|
||||||
getCrypto: jest.fn(() => crypto),
|
getCrypto: vi.fn(() => crypto),
|
||||||
getAuthMetadata: jest.fn().mockResolvedValue(makeDelegatedAuthConfig("https://issuer/", [DEVICE_CODE_SCOPE])),
|
getAuthMetadata: vi.fn().mockResolvedValue(makeDelegatedAuthConfig("https://issuer/", [DEVICE_CODE_SCOPE])),
|
||||||
} as unknown as MatrixClient;
|
} as unknown as MatrixClient;
|
||||||
client.http = new MatrixHttpApi<IHttpOpts & { onlyData: true }>(client, {
|
client.http = new MatrixHttpApi<IHttpOpts & { onlyData: true }>(client, {
|
||||||
baseUrl: client.baseUrl,
|
baseUrl: client.baseUrl,
|
||||||
@@ -77,10 +76,6 @@ describe("MSC4108SignInWithQR", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
fetchMock.reset();
|
|
||||||
});
|
|
||||||
|
|
||||||
const url = "https://fallbackserver/rz/123";
|
const url = "https://fallbackserver/rz/123";
|
||||||
const deviceId = "DEADB33F";
|
const deviceId = "DEADB33F";
|
||||||
const verificationUri = "https://example.com/verify";
|
const verificationUri = "https://example.com/verify";
|
||||||
@@ -115,10 +110,10 @@ describe("MSC4108SignInWithQR", () => {
|
|||||||
let opponentData = Promise.withResolvers<string>();
|
let opponentData = Promise.withResolvers<string>();
|
||||||
|
|
||||||
const ourMockSession = {
|
const ourMockSession = {
|
||||||
send: jest.fn(async (newData) => {
|
send: vi.fn(async (newData) => {
|
||||||
ourData.resolve(newData);
|
ourData.resolve(newData);
|
||||||
}),
|
}),
|
||||||
receive: jest.fn(() => {
|
receive: vi.fn(() => {
|
||||||
const prom = opponentData.promise;
|
const prom = opponentData.promise;
|
||||||
prom.then(() => {
|
prom.then(() => {
|
||||||
opponentData = Promise.withResolvers();
|
opponentData = Promise.withResolvers();
|
||||||
@@ -134,10 +129,10 @@ describe("MSC4108SignInWithQR", () => {
|
|||||||
},
|
},
|
||||||
} as unknown as MSC4108RendezvousSession;
|
} as unknown as MSC4108RendezvousSession;
|
||||||
const opponentMockSession = {
|
const opponentMockSession = {
|
||||||
send: jest.fn(async (newData) => {
|
send: vi.fn(async (newData) => {
|
||||||
opponentData.resolve(newData);
|
opponentData.resolve(newData);
|
||||||
}),
|
}),
|
||||||
receive: jest.fn(() => {
|
receive: vi.fn(() => {
|
||||||
const prom = ourData.promise;
|
const prom = ourData.promise;
|
||||||
prom.then(() => {
|
prom.then(() => {
|
||||||
ourData = Promise.withResolvers();
|
ourData = Promise.withResolvers();
|
||||||
@@ -151,7 +146,7 @@ describe("MSC4108SignInWithQR", () => {
|
|||||||
|
|
||||||
const ourChannel = new MSC4108SecureChannel(ourMockSession);
|
const ourChannel = new MSC4108SecureChannel(ourMockSession);
|
||||||
const qrCodeData = QrCodeData.fromBytes(
|
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);
|
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 () => {
|
it("should be able to connect with opponent and share verificationUri", async () => {
|
||||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
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([
|
await Promise.all([
|
||||||
expect(ourLogin.deviceAuthorizationGrant()).resolves.toEqual({
|
expect(ourLogin.deviceAuthorizationGrant()).resolves.toEqual({
|
||||||
@@ -194,7 +189,7 @@ describe("MSC4108SignInWithQR", () => {
|
|||||||
it("should abort if device already exists", async () => {
|
it("should abort if device already exists", async () => {
|
||||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||||
|
|
||||||
mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
|
vi.mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
expect(ourLogin.deviceAuthorizationGrant()).rejects.toThrow("Specified device ID already exists"),
|
expect(ourLogin.deviceAuthorizationGrant()).rejects.toThrow("Specified device ID already exists"),
|
||||||
@@ -244,12 +239,12 @@ describe("MSC4108SignInWithQR", () => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
await opponentLogin.receive();
|
await opponentLogin.receive();
|
||||||
|
|
||||||
mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
|
vi.mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
|
||||||
|
|
||||||
const secrets = {
|
const secrets = {
|
||||||
cross_signing: { master_key: "mk", user_signing_key: "usk", self_signing_key: "ssk" },
|
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 = {
|
const payload = {
|
||||||
secrets: expect.objectContaining(secrets),
|
secrets: expect.objectContaining(secrets),
|
||||||
@@ -261,13 +256,13 @@ describe("MSC4108SignInWithQR", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should abort if device doesn't come up by timeout", async () => {
|
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();
|
fn();
|
||||||
// TODO: mock timers properly
|
// TODO: mock timers properly
|
||||||
return -1 as any;
|
return -1 as any;
|
||||||
});
|
});
|
||||||
jest.spyOn(Date, "now").mockImplementation(() => {
|
vi.spyOn(Date, "now").mockImplementation(() => {
|
||||||
return 12345678 + mocked(setTimeout).mock.calls.length * 1000;
|
return 12345678 + vi.mocked(setTimeout).mock.calls.length * 1000;
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||||
@@ -280,7 +275,7 @@ describe("MSC4108SignInWithQR", () => {
|
|||||||
await opponentLogin.send({
|
await opponentLogin.send({
|
||||||
type: PayloadType.Success,
|
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();
|
const ourProm = ourLogin.shareSecrets();
|
||||||
await expect(ourProm).rejects.toThrow("New device not found");
|
await expect(ourProm).rejects.toThrow("New device not found");
|
||||||
@@ -297,7 +292,7 @@ describe("MSC4108SignInWithQR", () => {
|
|||||||
await opponentLogin.send({
|
await opponentLogin.send({
|
||||||
type: PayloadType.Success,
|
type: PayloadType.Success,
|
||||||
});
|
});
|
||||||
mocked(client.getDevice).mockRejectedValue(
|
vi.mocked(client.getDevice).mockRejectedValue(
|
||||||
new MatrixError({ errcode: "M_UNKNOWN", error: "The message" }, 500),
|
new MatrixError({ errcode: "M_UNKNOWN", error: "The message" }, 500),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -314,7 +309,7 @@ describe("MSC4108SignInWithQR", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should not send secrets if user cancels", async () => {
|
it("should not send secrets if user cancels", async () => {
|
||||||
jest.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
|
vi.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
|
||||||
fn();
|
fn();
|
||||||
// TODO: mock timers properly
|
// TODO: mock timers properly
|
||||||
return -1 as any;
|
return -1 as any;
|
||||||
@@ -334,7 +329,7 @@ describe("MSC4108SignInWithQR", () => {
|
|||||||
await opponentLogin.receive();
|
await opponentLogin.receive();
|
||||||
|
|
||||||
const deviceResolvers = Promise.withResolvers<IMyDevice>();
|
const deviceResolvers = Promise.withResolvers<IMyDevice>();
|
||||||
mocked(client.getDevice).mockReturnValue(deviceResolvers.promise);
|
vi.mocked(client.getDevice).mockReturnValue(deviceResolvers.promise);
|
||||||
|
|
||||||
ourLogin.cancel(MSC4108FailureReason.UserCancelled).catch(() => {});
|
ourLogin.cancel(MSC4108FailureReason.UserCancelled).catch(() => {});
|
||||||
deviceResolvers.resolve({} as IMyDevice);
|
deviceResolvers.resolve({} as IMyDevice);
|
||||||
@@ -342,7 +337,7 @@ describe("MSC4108SignInWithQR", () => {
|
|||||||
const secrets = {
|
const secrets = {
|
||||||
cross_signing: { master_key: "mk", user_signing_key: "usk", self_signing_key: "ssk" },
|
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([
|
await Promise.all([
|
||||||
expect(ourProm).rejects.toThrow("User cancelled"),
|
expect(ourProm).rejects.toThrow("User cancelled"),
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
type Extension,
|
type Extension,
|
||||||
} from "../../src/sliding-sync";
|
} from "../../src/sliding-sync";
|
||||||
import { TestClient } from "../TestClient";
|
import { TestClient } from "../TestClient";
|
||||||
import { type IRoomEvent, type IStateEvent } from "../../src";
|
import { type IContent, type IRoomEvent, type IStateEvent } from "../../src";
|
||||||
import {
|
import {
|
||||||
type MatrixClient,
|
type MatrixClient,
|
||||||
type MatrixEvent,
|
type MatrixEvent,
|
||||||
@@ -68,17 +68,17 @@ describe("SlidingSyncSdk", () => {
|
|||||||
const selfAccessToken = "aseukfgwef";
|
const selfAccessToken = "aseukfgwef";
|
||||||
|
|
||||||
const mockifySlidingSync = (s: SlidingSync): SlidingSync => {
|
const mockifySlidingSync = (s: SlidingSync): SlidingSync => {
|
||||||
s.getListParams = jest.fn();
|
s.getListParams = vi.fn();
|
||||||
s.getListData = jest.fn();
|
s.getListData = vi.fn();
|
||||||
s.getRoomSubscriptions = jest.fn();
|
s.getRoomSubscriptions = vi.fn();
|
||||||
s.modifyRoomSubscriptionInfo = jest.fn();
|
s.modifyRoomSubscriptionInfo = vi.fn();
|
||||||
s.modifyRoomSubscriptions = jest.fn();
|
s.modifyRoomSubscriptions = vi.fn();
|
||||||
s.registerExtension = jest.fn();
|
s.registerExtension = vi.fn();
|
||||||
s.setList = jest.fn();
|
s.setList = vi.fn();
|
||||||
s.setListRanges = jest.fn();
|
s.setListRanges = vi.fn();
|
||||||
s.start = jest.fn();
|
s.start = vi.fn();
|
||||||
s.stop = jest.fn();
|
s.stop = vi.fn();
|
||||||
s.resend = jest.fn();
|
s.resend = vi.fn();
|
||||||
return s;
|
return s;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ describe("SlidingSyncSdk", () => {
|
|||||||
expect(m.getType()).toEqual(want[i].type);
|
expect(m.getType()).toEqual(want[i].type);
|
||||||
expect(m.getSender()).toEqual(want[i].sender);
|
expect(m.getSender()).toEqual(want[i].sender);
|
||||||
expect(m.getId()).toEqual(want[i].event_id);
|
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);
|
expect(m.getTs()).toEqual(want[i].origin_server_ts);
|
||||||
if (want[i].unsigned) {
|
if (want[i].unsigned) {
|
||||||
expect(m.getUnsigned()).toEqual(want[i].unsigned);
|
expect(m.getUnsigned()).toEqual(want[i].unsigned);
|
||||||
@@ -150,7 +150,7 @@ describe("SlidingSyncSdk", () => {
|
|||||||
// find an extension on a SlidingSyncSdk instance
|
// find an extension on a SlidingSyncSdk instance
|
||||||
const findExtension = (name: string): Extension<any, any> => {
|
const findExtension = (name: string): Extension<any, any> => {
|
||||||
expect(mockSlidingSync!.registerExtension).toHaveBeenCalled();
|
expect(mockSlidingSync!.registerExtension).toHaveBeenCalled();
|
||||||
const mockFn = mockSlidingSync!.registerExtension as jest.Mock;
|
const mockFn = vi.mocked(mockSlidingSync!.registerExtension);
|
||||||
// find the extension
|
// find the extension
|
||||||
for (let i = 0; i < mockFn.mock.calls.length; i++) {
|
for (let i = 0; i < mockFn.mock.calls.length; i++) {
|
||||||
const calledExtension = mockFn.mock.calls[i][0] as Extension<any, any>;
|
const calledExtension = mockFn.mock.calls[i][0] as Extension<any, any>;
|
||||||
@@ -658,7 +658,7 @@ describe("SlidingSyncSdk", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("can update device lists", () => {
|
it("can update device lists", () => {
|
||||||
syncCryptoCallback!.processDeviceLists = jest.fn();
|
syncCryptoCallback!.processDeviceLists = vi.fn();
|
||||||
ext.onResponse({
|
ext.onResponse({
|
||||||
device_lists: {
|
device_lists: {
|
||||||
changed: ["@alice:localhost"],
|
changed: ["@alice:localhost"],
|
||||||
@@ -672,7 +672,7 @@ describe("SlidingSyncSdk", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("can update OTK counts and unused fallback keys", () => {
|
it("can update OTK counts and unused fallback keys", () => {
|
||||||
syncCryptoCallback!.processKeyCounts = jest.fn();
|
syncCryptoCallback!.processKeyCounts = vi.fn();
|
||||||
ext.onResponse({
|
ext.onResponse({
|
||||||
device_one_time_keys_count: {
|
device_one_time_keys_count: {
|
||||||
signed_curve25519: 42,
|
signed_curve25519: 42,
|
||||||
@@ -722,7 +722,7 @@ describe("SlidingSyncSdk", () => {
|
|||||||
});
|
});
|
||||||
globalData = client!.getAccountData(globalType)!;
|
globalData = client!.getAccountData(globalType)!;
|
||||||
expect(globalData).toBeTruthy();
|
expect(globalData).toBeTruthy();
|
||||||
expect(globalData.getContent()).toEqual(globalContent);
|
expect(globalData.getContent<IContent>()).toEqual(globalContent);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("processes rooms account data", async () => {
|
it("processes rooms account data", async () => {
|
||||||
@@ -757,7 +757,7 @@ describe("SlidingSyncSdk", () => {
|
|||||||
expect(room).toBeTruthy();
|
expect(room).toBeTruthy();
|
||||||
const event = room.getAccountData(roomType)!;
|
const event = room.getAccountData(roomType)!;
|
||||||
expect(event).toBeTruthy();
|
expect(event).toBeTruthy();
|
||||||
expect(event.getContent()).toEqual(roomContent);
|
expect(event.getContent<IContent>()).toEqual(roomContent);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("doesn't crash for unknown room account data", async () => {
|
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 () => {
|
it("can handle missing fields", async () => {
|
||||||
ext.onResponse({
|
ext.onResponse({
|
||||||
next_batch: "23456",
|
next_batch: "23456",
|
||||||
@@ -861,7 +862,7 @@ describe("SlidingSyncSdk", () => {
|
|||||||
};
|
};
|
||||||
let called = false;
|
let called = false;
|
||||||
client!.once(ClientEvent.ToDeviceEvent, (ev) => {
|
client!.once(ClientEvent.ToDeviceEvent, (ev) => {
|
||||||
expect(ev.getContent()).toEqual(toDeviceContent);
|
expect(ev.getContent<IContent>()).toEqual(toDeviceContent);
|
||||||
expect(ev.getType()).toEqual(toDeviceType);
|
expect(ev.getType()).toEqual(toDeviceType);
|
||||||
called = true;
|
called = true;
|
||||||
});
|
});
|
||||||
@@ -1095,6 +1096,7 @@ describe("SlidingSyncSdk", () => {
|
|||||||
expect(receipt?.data.thread_id).toBeFalsy();
|
expect(receipt?.data.thread_id).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @vitest/expect-expect
|
||||||
it("gracefully handles missing rooms when receiving receipts", async () => {
|
it("gracefully handles missing rooms when receiving receipts", async () => {
|
||||||
const roomId = "!room:id";
|
const roomId = "!room:id";
|
||||||
const alice = "@alice:alice";
|
const alice = "@alice:alice";
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ describe("SlidingSync", () => {
|
|||||||
await p;
|
await p;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @vitest/expect-expect
|
||||||
it("should stop the sync loop upon calling stop()", () => {
|
it("should stop the sync loop upon calling stop()", () => {
|
||||||
slidingSync.stop();
|
slidingSync.stop();
|
||||||
httpBackend!.verifyNoOutstandingExpectation();
|
httpBackend!.verifyNoOutstandingExpectation();
|
||||||
|
|||||||
+13
-4
@@ -14,13 +14,22 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
jest.mock("../src/http-api/utils", () => ({
|
import fetchMock, { manageFetchMockGlobally } from "@fetch-mock/vitest";
|
||||||
...jest.requireActual("../src/http-api/utils"),
|
|
||||||
|
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
|
// We mock timeoutSignal otherwise it causes tests to leave timers running
|
||||||
timeoutSignal: () => new AbortController().signal,
|
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) {
|
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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fetchMock from "fetch-mock-jest";
|
import fetchMock from "@fetch-mock/vitest";
|
||||||
|
|
||||||
import { type ISyncResponder } from "./SyncResponder";
|
import { type ISyncResponder } from "./SyncResponder";
|
||||||
|
|
||||||
@@ -36,65 +36,80 @@ export class AccountDataAccumulator {
|
|||||||
|
|
||||||
public constructor(private syncResponder: ISyncResponder) {}
|
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
|
* Once it is set, its data is stored (for future return by `interceptGetAccountData` etc) and the resolved promise is
|
||||||
* resolved.
|
* 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.
|
* @returns a Promise which will resolve (with the content of the account data) once it is set.
|
||||||
*/
|
*/
|
||||||
public interceptSetAccountData(
|
public interceptSetAccountData(): void {
|
||||||
accountDataType: string,
|
if (this.setInterceptRunning) return;
|
||||||
opts?: Parameters<(typeof fetchMock)["put"]>[2],
|
this.setInterceptRunning = true;
|
||||||
): 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);
|
|
||||||
|
|
||||||
// return a sync response
|
fetchMock.put(`express:/_matrix/client/v3/user/:userId/account_data/:type`, (callLog) => {
|
||||||
this.sendSyncResponseWithUpdatedAccountData();
|
const content = JSON.parse(callLog.options.body as string);
|
||||||
return {};
|
const type = callLog.url.split("/").pop();
|
||||||
},
|
// update account data for sync response
|
||||||
opts,
|
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
|
* Intercept all requests to get account data
|
||||||
*/
|
*/
|
||||||
public interceptGetAccountData(): void {
|
public interceptGetAccountData(): void {
|
||||||
fetchMock.get(
|
fetchMock.get(`express:/_matrix/client/v3/user/:userId/account_data/:type`, (callLog) => {
|
||||||
`express:/_matrix/client/v3/user/:userId/account_data/:type`,
|
const type = callLog.url.split("/").pop();
|
||||||
(url) => {
|
const existing = this.accountDataEvents.get(type!);
|
||||||
const type = url.split("/").pop();
|
if (existing) {
|
||||||
const existing = this.accountDataEvents.get(type!);
|
// return it
|
||||||
if (existing) {
|
return {
|
||||||
// return it
|
status: 200,
|
||||||
return {
|
body: existing,
|
||||||
status: 200,
|
};
|
||||||
body: existing,
|
} else {
|
||||||
};
|
// 404
|
||||||
} else {
|
return {
|
||||||
// 404
|
status: 404,
|
||||||
return {
|
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
|
||||||
status: 404,
|
};
|
||||||
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
|
}
|
||||||
};
|
});
|
||||||
}
|
|
||||||
},
|
|
||||||
{ overwriteRoutes: true },
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import debugFunc, { type Debugger } from "debug";
|
import debugFunc, { type Debugger } from "debug";
|
||||||
import fetchMock from "fetch-mock-jest";
|
import fetchMock from "@fetch-mock/vitest";
|
||||||
|
|
||||||
import type { IDeviceKeys, IOneTimeKey } from "../../src/@types/crypto";
|
import type { IDeviceKeys, IOneTimeKey } from "../../src/@types/crypto";
|
||||||
import type { CrossSigningKeys, ISignedKey, KeySignatures } from "../../src";
|
import type { CrossSigningKeys, ISignedKey, KeySignatures } from "../../src";
|
||||||
@@ -81,26 +81,27 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
|
|||||||
|
|
||||||
// set up a listener for /keys/upload.
|
// set up a listener for /keys/upload.
|
||||||
this.oneTimeKeysPromise = new Promise((resolveOneTimeKeys) => {
|
this.oneTimeKeysPromise = new Promise((resolveOneTimeKeys) => {
|
||||||
const listener = (url: string, options: RequestInit) =>
|
fetchMock.post(
|
||||||
this.onKeyUploadRequest(resolveOneTimeKeys, options);
|
new URL("/_matrix/client/v3/keys/upload", homeserverUrl).toString(),
|
||||||
|
(callLog) => this.onKeyUploadRequest(resolveOneTimeKeys, callLog.options),
|
||||||
fetchMock.post(new URL("/_matrix/client/v3/keys/upload", homeserverUrl).toString(), listener);
|
{ name: routeNamePrefix + "keys-upload" },
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
fetchMock.post(
|
fetchMock.post(
|
||||||
|
new URL("/_matrix/client/v3/keys/signatures/upload", homeserverUrl).toString(),
|
||||||
|
(callLog) => this.onSignaturesUploadRequest(callLog.options),
|
||||||
{
|
{
|
||||||
url: new URL("/_matrix/client/v3/keys/signatures/upload", homeserverUrl).toString(),
|
|
||||||
name: routeNamePrefix + "upload-sigs",
|
name: routeNamePrefix + "upload-sigs",
|
||||||
},
|
},
|
||||||
(url, options) => this.onSignaturesUploadRequest(options),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
fetchMock.post(
|
fetchMock.post(
|
||||||
|
new URL("/_matrix/client/v3/keys/device_signing/upload", homeserverUrl).toString(),
|
||||||
|
(callLog) => this.onSigningKeyUploadRequest(callLog.options),
|
||||||
{
|
{
|
||||||
url: new URL("/_matrix/client/v3/keys/device_signing/upload", homeserverUrl).toString(),
|
|
||||||
name: routeNamePrefix + "upload-cross-signing-keys",
|
name: routeNamePrefix + "upload-cross-signing-keys",
|
||||||
},
|
},
|
||||||
(url, options) => this.onSigningKeyUploadRequest(options),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fetchMock from "fetch-mock-jest";
|
import fetchMock from "@fetch-mock/vitest";
|
||||||
|
|
||||||
import { MapWithDefault } from "../../src/utils";
|
import { MapWithDefault } from "../../src/utils";
|
||||||
import { type IDownloadKeyResult, type SigningKeys } from "../../src";
|
import { type IDownloadKeyResult, type SigningKeys } from "../../src";
|
||||||
@@ -42,8 +42,9 @@ export class E2EKeyResponder {
|
|||||||
*/
|
*/
|
||||||
public constructor(homeserverUrl: string) {
|
public constructor(homeserverUrl: string) {
|
||||||
// set up a listener for /keys/query.
|
// 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(), (callLog) =>
|
||||||
fetchMock.post(new URL("/_matrix/client/v3/keys/query", homeserverUrl).toString(), listener);
|
this.onKeyQueryRequest(callLog.options),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onKeyQueryRequest(options: RequestInit) {
|
private onKeyQueryRequest(options: RequestInit) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fetchMock from "fetch-mock-jest";
|
import fetchMock from "@fetch-mock/vitest";
|
||||||
|
|
||||||
import { MapWithDefault } from "../../src/utils";
|
import { MapWithDefault } from "../../src/utils";
|
||||||
import { type E2EKeyReceiver } from "./E2EKeyReceiver";
|
import { type E2EKeyReceiver } from "./E2EKeyReceiver";
|
||||||
@@ -36,8 +36,9 @@ export class E2EOTKClaimResponder {
|
|||||||
* @param homeserverUrl - the Homeserver Url of the client under test.
|
* @param homeserverUrl - the Homeserver Url of the client under test.
|
||||||
*/
|
*/
|
||||||
public constructor(homeserverUrl: string) {
|
public constructor(homeserverUrl: string) {
|
||||||
const listener = (url: string, options: RequestInit) => this.onKeyClaimRequest(options);
|
fetchMock.post(new URL("/_matrix/client/v3/keys/claim", homeserverUrl).toString(), (callLog) =>
|
||||||
fetchMock.post(new URL("/_matrix/client/v3/keys/claim", homeserverUrl).toString(), listener);
|
this.onKeyClaimRequest(callLog.options),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onKeyClaimRequest(options: RequestInit) {
|
private onKeyClaimRequest(options: RequestInit) {
|
||||||
|
|||||||
@@ -16,9 +16,8 @@ limitations under the License.
|
|||||||
|
|
||||||
import debugFunc from "debug";
|
import debugFunc from "debug";
|
||||||
import { type Debugger } from "debug";
|
import { type Debugger } from "debug";
|
||||||
import fetchMock from "fetch-mock-jest";
|
import fetchMock from "@fetch-mock/vitest";
|
||||||
|
import { type RouteResponse } from "fetch-mock";
|
||||||
import type FetchMock from "fetch-mock";
|
|
||||||
|
|
||||||
/** Interface implemented by classes that intercept `/sync` requests from test clients
|
/** Interface implemented by classes that intercept `/sync` requests from test clients
|
||||||
*
|
*
|
||||||
@@ -76,12 +75,12 @@ export class SyncResponder implements ISyncResponder {
|
|||||||
*/
|
*/
|
||||||
public constructor(homeserverUrl: string) {
|
public constructor(homeserverUrl: string) {
|
||||||
this.debug = debugFunc(`sync-responder:[${homeserverUrl}]`);
|
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(),
|
this.onSyncRequest(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async onSyncRequest(): Promise<FetchMock.MockResponse> {
|
private async onSyncRequest(): Promise<RouteResponse> {
|
||||||
switch (this.state) {
|
switch (this.state) {
|
||||||
case SyncResponderState.IDLE: {
|
case SyncResponderState.IDLE: {
|
||||||
this.debug("Got /sync request: waiting for response to be ready");
|
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.
|
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 { type ClientEventHandlerMap, type EmittedEvents, type MatrixClient } from "../../src/client";
|
||||||
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
|
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
|
||||||
import { User } from "../../src/models/user";
|
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
|
* Mock client with real event emitter
|
||||||
* useful for testing code that listens
|
* useful for testing code that listens
|
||||||
@@ -34,19 +40,19 @@ export class MockClientWithEventEmitter extends TypedEventEmitter<EmittedEvents,
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* - make a mock client
|
* - 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
|
* - spy on MatrixClientPeg.get to return the mock
|
||||||
* eg
|
* eg
|
||||||
* ```
|
* ```
|
||||||
* const mockClient = getMockClientWithEventEmitter({
|
* const mockClient = getMockClientWithEventEmitter({
|
||||||
getUserId: jest.fn().mockReturnValue(aliceId),
|
getUserId: vi.fn().mockReturnValue(aliceId),
|
||||||
});
|
});
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export const getMockClientWithEventEmitter = (
|
export const getMockClientWithEventEmitter = (
|
||||||
mockProperties: Partial<Record<MethodLikeKeys<MatrixClient>, unknown>>,
|
mockProperties: Partial<Record<MethodLikeKeys<MatrixClient>, unknown>>,
|
||||||
): MockedObject<MatrixClient> => {
|
): 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;
|
return mock;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,14 +65,14 @@ export const getMockClientWithEventEmitter = (
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export const mockClientMethodsUser = (userId = "@alice:domain") => ({
|
export const mockClientMethodsUser = (userId = "@alice:domain") => ({
|
||||||
getUserId: jest.fn().mockReturnValue(userId),
|
getUserId: vi.fn().mockReturnValue(userId),
|
||||||
getSafeUserId: jest.fn().mockReturnValue(userId),
|
getSafeUserId: vi.fn().mockReturnValue(userId),
|
||||||
getUser: jest.fn().mockReturnValue(new User(userId)),
|
getUser: vi.fn().mockReturnValue(new User(userId)),
|
||||||
isGuest: jest.fn().mockReturnValue(false),
|
isGuest: vi.fn().mockReturnValue(false),
|
||||||
mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
|
mxcUrlToHttp: vi.fn().mockReturnValue("mock-mxcUrlToHttp"),
|
||||||
credentials: { userId },
|
credentials: { userId },
|
||||||
getThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
|
getThreePids: vi.fn().mockResolvedValue({ threepids: [] }),
|
||||||
getAccessToken: jest.fn(),
|
getAccessToken: vi.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,16 +84,16 @@ export const mockClientMethodsUser = (userId = "@alice:domain") => ({
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export const mockClientMethodsEvents = () => ({
|
export const mockClientMethodsEvents = () => ({
|
||||||
decryptEventIfNeeded: jest.fn(),
|
decryptEventIfNeeded: vi.fn(),
|
||||||
getPushActionsForEvent: jest.fn(),
|
getPushActionsForEvent: vi.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns basic mocked client methods related to server support
|
* Returns basic mocked client methods related to server support
|
||||||
*/
|
*/
|
||||||
export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({
|
export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({
|
||||||
getIdentityServerUrl: jest.fn(),
|
getIdentityServerUrl: vi.fn(),
|
||||||
getHomeserverUrl: jest.fn(),
|
getHomeserverUrl: vi.fn(),
|
||||||
getCachedCapabilities: jest.fn().mockReturnValue({}),
|
getCachedCapabilities: vi.fn().mockReturnValue({}),
|
||||||
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
|
doesServerSupportUnstableFeature: vi.fn().mockResolvedValue(false),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,15 +14,17 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { type MockInstance } from "vitest";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter emitter.emit mock calls to find relevant events
|
* Filter emitter.emit mock calls to find relevant events
|
||||||
* eg:
|
* eg:
|
||||||
* ```
|
* ```
|
||||||
* const emitSpy = jest.spyOn(state, 'emit');
|
* const emitSpy = vi.spyOn(state, 'emit');
|
||||||
* << actions >>
|
* << actions >>
|
||||||
* const beaconLivenessEmits = emitCallsByEventType(BeaconEvent.New, emitSpy);
|
* const beaconLivenessEmits = emitCallsByEventType(BeaconEvent.New, emitSpy);
|
||||||
* expect(beaconLivenessEmits.length).toBe(1);
|
* 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);
|
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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Jest now uses @sinonjs/fake-timers which exposes tickAsync() and a number of
|
// Vitest lacks tickAsync() and a number of other async methods which break the event loop,
|
||||||
// other async methods which break the event loop, letting scheduled promise
|
// letting scheduled promise callbacks run. So we have to do it manually
|
||||||
// callbacks run. Unfortunately, Jest doesn't expose these, so we have to do
|
// (this is what sinon does under the hood). We do both in a loop until the thing we expect happens:
|
||||||
// it manually (this is what sinon does under the hood). We do both in a loop
|
// hopefully this is the least flakey way and avoids assuming anything about the app's behaviour.
|
||||||
// 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;
|
const realSetTimeout = setTimeout;
|
||||||
export function flushPromises() {
|
export function flushPromises() {
|
||||||
return new Promise((r) => {
|
return new Promise((r) => {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fetchMock from "fetch-mock-jest";
|
import fetchMock from "@fetch-mock/vitest";
|
||||||
|
|
||||||
import { type KeyBackupInfo } from "../../src/crypto-api";
|
import { type KeyBackupInfo } from "../../src/crypto-api";
|
||||||
|
|
||||||
@@ -25,20 +25,15 @@ import { type KeyBackupInfo } from "../../src/crypto-api";
|
|||||||
* @param userId - the local user's ID. Defaults to `@alice:localhost`.
|
* @param userId - the local user's ID. Defaults to `@alice:localhost`.
|
||||||
*/
|
*/
|
||||||
export function mockInitialApiRequests(homeserverUrl: string, userId: string = "@alice:localhost") {
|
export function mockInitialApiRequests(homeserverUrl: string, userId: string = "@alice:localhost") {
|
||||||
fetchMock.getOnce(
|
fetchMock.getOnce(new URL("/_matrix/client/versions", homeserverUrl).toString(), { versions: ["v1.1"] });
|
||||||
new URL("/_matrix/client/versions", homeserverUrl).toString(),
|
fetchMock.getOnce(new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(), {});
|
||||||
{ versions: ["v1.1"] },
|
|
||||||
{ overwriteRoutes: true },
|
|
||||||
);
|
|
||||||
fetchMock.getOnce(
|
|
||||||
new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(),
|
|
||||||
{},
|
|
||||||
{ overwriteRoutes: true },
|
|
||||||
);
|
|
||||||
fetchMock.postOnce(
|
fetchMock.postOnce(
|
||||||
new URL(`/_matrix/client/v3/user/${encodeURIComponent(userId)}/filter`, homeserverUrl).toString(),
|
new URL(`/_matrix/client/v3/user/${encodeURIComponent(userId)}/filter`, homeserverUrl).toString(),
|
||||||
{ filter_id: "fid" },
|
{ filter_id: "fid" },
|
||||||
{ overwriteRoutes: true },
|
);
|
||||||
|
fetchMock.getOnce(
|
||||||
|
new URL(`/_matrix/client/v3/user/${encodeURIComponent(userId)}/filter/fid`, homeserverUrl).toString(),
|
||||||
|
{ filter_id: "fid" },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,24 +60,30 @@ export function mockSetupCrossSigningRequests(): void {
|
|||||||
* @param backupVersion - The backup version that will be returned by `POST room_keys/version`.
|
* @param backupVersion - The backup version that will be returned by `POST room_keys/version`.
|
||||||
*/
|
*/
|
||||||
export function mockSetupMegolmBackupRequests(backupVersion: string): void {
|
export function mockSetupMegolmBackupRequests(backupVersion: string): void {
|
||||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
fetchMock.get(
|
||||||
status: 404,
|
"path:/_matrix/client/v3/room_keys/version",
|
||||||
body: {
|
{
|
||||||
errcode: "M_NOT_FOUND",
|
status: 404,
|
||||||
error: "No current backup version",
|
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) => {
|
fetchMock.post(
|
||||||
const backupData: KeyBackupInfo = JSON.parse((request.body as string) ?? "{}");
|
"path:/_matrix/client/v3/room_keys/version",
|
||||||
backupData.version = backupVersion;
|
(callLog) => {
|
||||||
backupData.count = 0;
|
const backupData: KeyBackupInfo = JSON.parse((callLog.options.body as string) ?? "{}");
|
||||||
backupData.etag = "zer";
|
backupData.version = backupVersion;
|
||||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData, {
|
backupData.count = 0;
|
||||||
overwriteRoutes: true,
|
backupData.etag = "zer";
|
||||||
});
|
fetchMock.modifyRoute("room-keys-version", { response: backupData });
|
||||||
return {
|
return {
|
||||||
version: backupVersion,
|
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`
|
* Do not edit by hand! This file is generated by `./generate-test-data.py`
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {{ IDeviceKeys, IMegolmSessionData }} from "../../../src/@types/crypto";
|
import type {{ IDeviceKeys, IMegolmSessionData }} from "../../../src/@types/crypto";
|
||||||
import {{ IDownloadKeyResult, IEvent }} from "../../../src";
|
import type {{ IDownloadKeyResult, IEvent }} from "../../../src";
|
||||||
import {{ KeyBackupSession, KeyBackupInfo }} from "../../../src/crypto-api/keybackup";
|
import type {{ KeyBackupSession, KeyBackupInfo, KeyBackupRoomSessions }} from "../../../src/crypto-api/keybackup";
|
||||||
|
|
||||||
/* eslint-disable comma-dangle */
|
/* 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` */
|
/** Signed OTKs, returned by `POST /keys/claim` */
|
||||||
export const {prefix}ONE_TIME_KEYS = { json.dumps(otks, indent=4) };
|
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 */
|
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
|
||||||
export const {prefix}MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = {
|
export const {prefix}MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = {
|
||||||
json.dumps(set_of_exported_room_keys, indent=4)
|
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 */
|
/** The encrypted CLEAR_EVENT by MEGOLM_SESSION_DATA */
|
||||||
export const {prefix}ENCRYPTED_EVENT: Partial<IEvent> = {json.dumps(encrypted_event, indent=4)};
|
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")
|
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]:
|
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
|
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)
|
Returns the exported key, the matching privat edKey (needed to encrypt)
|
||||||
"""
|
"""
|
||||||
index = 0
|
index = 0
|
||||||
@@ -409,11 +417,12 @@ def build_exported_megolm_key(device_curve_key: x25519.X25519PrivateKey) -> tupl
|
|||||||
"session_id": encode_base64(
|
"session_id": encode_base64(
|
||||||
private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
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": {
|
"sender_claimed_keys": {
|
||||||
"ed25519": encode_base64(ed25519.Ed25519PrivateKey.from_private_bytes(randbytes(32)).public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)),
|
"ed25519": encode_base64(ed25519.Ed25519PrivateKey.from_private_bytes(randbytes(32)).public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)),
|
||||||
},
|
},
|
||||||
"forwarding_curve25519_key_chain": [],
|
"forwarding_curve25519_key_chain": [],
|
||||||
|
"m.shared_history": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
return megolm_export, private_key
|
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",
|
"room_id": "!room:id",
|
||||||
"sender_key": previous["sender_key"],
|
"sender_key": previous["sender_key"],
|
||||||
"session_id": previous["session_id"],
|
"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"],
|
"sender_claimed_keys": previous["sender_claimed_keys"],
|
||||||
"forwarding_curve25519_key_chain": [],
|
"forwarding_curve25519_key_chain": [],
|
||||||
}
|
}
|
||||||
@@ -609,7 +618,7 @@ def generate_encrypted_event_content(exported_key: dict, ed_key: ed25519.Ed25519
|
|||||||
|
|
||||||
message += signature
|
message += signature
|
||||||
|
|
||||||
cipher_text = encode_base64(message)
|
cipher_text = encode_base64(bytes(message))
|
||||||
|
|
||||||
encrypted_payload = {
|
encrypted_payload = {
|
||||||
"algorithm" : "m.megolm.v1.aes-sha2",
|
"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')
|
export_bytes += parity_byte.to_bytes(1, 'big')
|
||||||
|
|
||||||
# The byte string is encoded using base58
|
# 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)]
|
split = [recovery_key[i:i + 4] for i in range(0, len(recovery_key), 4)]
|
||||||
return ' '.join(split)
|
return ' '.join(split)
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
* Do not edit by hand! This file is generated by `./generate-test-data.py`
|
* 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 { IDeviceKeys, IMegolmSessionData } from "../../../src/@types/crypto";
|
||||||
import { type IDownloadKeyResult, type IEvent } from "../../../src";
|
import type { IDownloadKeyResult, IEvent } from "../../../src";
|
||||||
import { type KeyBackupSession, type KeyBackupInfo } from "../../../src/crypto-api/keybackup";
|
import type { KeyBackupSession, KeyBackupInfo, KeyBackupRoomSessions } from "../../../src/crypto-api/keybackup";
|
||||||
|
|
||||||
/* eslint-disable comma-dangle */
|
/* 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 */
|
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
|
||||||
export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||||
{
|
{
|
||||||
@@ -149,7 +129,8 @@ export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
|||||||
"sender_claimed_keys": {
|
"sender_claimed_keys": {
|
||||||
"ed25519": "QdgHgdpDgihgovpPzUiThXur1fbErTFh7paFvNKSgN0"
|
"ed25519": "QdgHgdpDgihgovpPzUiThXur1fbErTFh7paFvNKSgN0"
|
||||||
},
|
},
|
||||||
"forwarding_curve25519_key_chain": []
|
"forwarding_curve25519_key_chain": [],
|
||||||
|
"org.matrix.msc3061.shared_history": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"algorithm": "m.megolm.v1.aes-sha2",
|
"algorithm": "m.megolm.v1.aes-sha2",
|
||||||
@@ -160,7 +141,8 @@ export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
|||||||
"sender_claimed_keys": {
|
"sender_claimed_keys": {
|
||||||
"ed25519": "IrkbT6H+0urDf6wKDSyVC1fh1t84Vz6T62snni86Cog"
|
"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": {
|
"sender_claimed_keys": {
|
||||||
"ed25519": "Bhbpt6hqMZlSH4sJV7xiEEEiPVeTWz4Vkujl1EMdIPI"
|
"ed25519": "Bhbpt6hqMZlSH4sJV7xiEEEiPVeTWz4Vkujl1EMdIPI"
|
||||||
},
|
},
|
||||||
"forwarding_curve25519_key_chain": []
|
"forwarding_curve25519_key_chain": [],
|
||||||
|
"org.matrix.msc3061.shared_history": true
|
||||||
};
|
};
|
||||||
|
|
||||||
/** A ratcheted version of MEGOLM_SESSION_DATA */
|
/** A ratcheted version of MEGOLM_SESSION_DATA */
|
||||||
@@ -196,7 +179,7 @@ export const CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {
|
|||||||
"forwarded_count": 0,
|
"forwarded_count": 0,
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"session_data": {
|
"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",
|
"ephemeral": "q+P1WdRtEiPIEtNuuGrRcueZxUbLnSKdsuTAkxewXgU",
|
||||||
"mac": "OibmACbORhI"
|
"mac": "OibmACbORhI"
|
||||||
}
|
}
|
||||||
@@ -229,6 +212,37 @@ export const ENCRYPTED_EVENT: Partial<IEvent> = {
|
|||||||
"origin_server_ts": 1507753886000
|
"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
|
// Bob data
|
||||||
|
|
||||||
export const BOB_TEST_USER_ID = "@bob:xyz";
|
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 */
|
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
|
||||||
export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||||
{
|
{
|
||||||
@@ -369,7 +363,8 @@ export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
|||||||
"sender_claimed_keys": {
|
"sender_claimed_keys": {
|
||||||
"ed25519": "F4P7f1Z0RjbiZMgHk1xBCG3KC4/Ng9PmxLJ4hQ13sHA"
|
"ed25519": "F4P7f1Z0RjbiZMgHk1xBCG3KC4/Ng9PmxLJ4hQ13sHA"
|
||||||
},
|
},
|
||||||
"forwarding_curve25519_key_chain": []
|
"forwarding_curve25519_key_chain": [],
|
||||||
|
"org.matrix.msc3061.shared_history": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"algorithm": "m.megolm.v1.aes-sha2",
|
"algorithm": "m.megolm.v1.aes-sha2",
|
||||||
@@ -380,7 +375,8 @@ export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
|||||||
"sender_claimed_keys": {
|
"sender_claimed_keys": {
|
||||||
"ed25519": "OsZMdC1gQ5nPr+L9tuT6xXsaFJkVPkgxP2FexHF1/QM"
|
"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": {
|
"sender_claimed_keys": {
|
||||||
"ed25519": "zBdpQwWYyz1MkZuEUhXqcdMfUNN/B9psLFDDDTJOg64"
|
"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 */
|
/** A ratcheted version of BOB_MEGOLM_SESSION_DATA */
|
||||||
@@ -416,7 +413,7 @@ export const BOB_CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {
|
|||||||
"forwarded_count": 0,
|
"forwarded_count": 0,
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"session_data": {
|
"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",
|
"ephemeral": "oO0VX84OUIzm2i/12zAhTWOZT5IFRH5mXaKZ8fXkCgU",
|
||||||
"mac": "lEfHlqfJQwU"
|
"mac": "lEfHlqfJQwU"
|
||||||
}
|
}
|
||||||
@@ -449,6 +446,34 @@ export const BOB_ENCRYPTED_EVENT: Partial<IEvent> = {
|
|||||||
"origin_server_ts": 1507753886000
|
"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 */
|
/** 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> = {
|
export const BOB_ALT_SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
|
||||||
"master_keys": {
|
"master_keys": {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
ClientEvent,
|
ClientEvent,
|
||||||
EventType,
|
EventType,
|
||||||
|
HistoryVisibility,
|
||||||
type IJoinedRoom,
|
type IJoinedRoom,
|
||||||
type IPusher,
|
type IPusher,
|
||||||
type ISyncResponse,
|
type ISyncResponse,
|
||||||
@@ -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
|
* Return a sync response which contains a single room (by default `TEST_ROOM_ID`), with the members given
|
||||||
* @param roomMembers
|
* and history visibility set to `shared`.
|
||||||
* @param roomId
|
|
||||||
*
|
*
|
||||||
* @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 = {
|
const roomResponse: IJoinedRoom = {
|
||||||
summary: {
|
summary: {
|
||||||
"m.heroes": [],
|
"m.heroes": [],
|
||||||
@@ -77,7 +87,16 @@ export function getSyncResponse(roomMembers: string[], roomId = TEST_ROOM_ID): I
|
|||||||
type: "m.room.encryption",
|
type: "m.room.encryption",
|
||||||
state_key: "",
|
state_key: "",
|
||||||
content: {
|
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
|
// eslint-disable-line guard-for-in
|
||||||
try {
|
try {
|
||||||
if (constr.prototype[key] instanceof Function) {
|
if (constr.prototype[key] instanceof Function) {
|
||||||
result[key] = jest.fn();
|
result[key] = vi.fn();
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Direct access to some non-function fields of DOM prototypes may
|
// Direct access to some non-function fields of DOM prototypes may
|
||||||
@@ -587,7 +606,7 @@ export async function advanceTimersUntil<T>(promise: Promise<T>): Promise<T> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
while (!resolved) {
|
while (!resolved) {
|
||||||
await jest.advanceTimersByTimeAsync(1);
|
await vi.advanceTimersByTimeAsync(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await promise;
|
return await promise;
|
||||||
@@ -636,7 +655,7 @@ export function waitFor<T>(
|
|||||||
checkCallback();
|
checkCallback();
|
||||||
|
|
||||||
while (!finished) {
|
while (!finished) {
|
||||||
jest.advanceTimersByTime(interval);
|
vi.advanceTimersByTime(interval);
|
||||||
|
|
||||||
// Could have timed-out
|
// Could have timed-out
|
||||||
if (finished) break;
|
if (finished) break;
|
||||||
|
|||||||
+51
-92
@@ -17,21 +17,17 @@ limitations under the License.
|
|||||||
import {
|
import {
|
||||||
type ClientEvent,
|
type ClientEvent,
|
||||||
type ClientEventHandlerMap,
|
type ClientEventHandlerMap,
|
||||||
type EmptyObject,
|
|
||||||
EventType,
|
EventType,
|
||||||
type GroupCall,
|
type GroupCall,
|
||||||
GroupCallIntent,
|
GroupCallIntent,
|
||||||
GroupCallType,
|
GroupCallType,
|
||||||
type IContent,
|
type IContent,
|
||||||
type ISendEventResponse,
|
|
||||||
type MatrixClient,
|
type MatrixClient,
|
||||||
type MatrixEvent,
|
type MatrixEvent,
|
||||||
type Room,
|
|
||||||
RoomMember,
|
RoomMember,
|
||||||
type RoomState,
|
type RoomState,
|
||||||
RoomStateEvent,
|
RoomStateEvent,
|
||||||
type RoomStateEventHandlerMap,
|
type RoomStateEventHandlerMap,
|
||||||
type SendToDeviceContentMap,
|
|
||||||
} from "../../src";
|
} from "../../src";
|
||||||
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
|
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
|
||||||
import { ReEmitter } from "../../src/ReEmitter";
|
import { ReEmitter } from "../../src/ReEmitter";
|
||||||
@@ -269,19 +265,20 @@ export class MockRTCRtpTransceiver {
|
|||||||
this.peerConn.needsNegotiation = true;
|
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(
|
constructor(
|
||||||
public readonly id: string,
|
public readonly id: string,
|
||||||
public readonly kind: "audio" | "video",
|
public readonly kind: "audio" | "video",
|
||||||
public enabled = true,
|
public enabled = true,
|
||||||
) {}
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
public stop = jest.fn<void, []>();
|
public stop = vi.fn<() => void>();
|
||||||
|
|
||||||
public listeners: [string, (...args: any[]) => any][] = [];
|
|
||||||
public isStopped = false;
|
public isStopped = false;
|
||||||
public settings?: MediaTrackSettings;
|
public settings?: MediaTrackSettings;
|
||||||
|
|
||||||
@@ -289,45 +286,21 @@ export class MockMediaStreamTrack {
|
|||||||
return this.settings!;
|
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 {
|
public typed(): MediaStreamTrack {
|
||||||
return this as unknown as MediaStreamTrack;
|
return this as unknown as MediaStreamTrack;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
|
export class MockMediaStream extends EventTarget {
|
||||||
// implementation
|
|
||||||
export class MockMediaStream {
|
|
||||||
constructor(
|
constructor(
|
||||||
public id: string,
|
public id: string,
|
||||||
private tracks: MockMediaStreamTrack[] = [],
|
private tracks: MockMediaStreamTrack[] = [],
|
||||||
) {}
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
public listeners: [string, (...args: any[]) => any][] = [];
|
|
||||||
public isStopped = false;
|
public isStopped = false;
|
||||||
|
|
||||||
public dispatchEvent(eventType: string) {
|
|
||||||
this.listeners.forEach(([t, c]) => {
|
|
||||||
if (t !== eventType) return;
|
|
||||||
c();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
public getTracks() {
|
public getTracks() {
|
||||||
return this.tracks;
|
return this.tracks;
|
||||||
}
|
}
|
||||||
@@ -337,17 +310,9 @@ export class MockMediaStream {
|
|||||||
public getVideoTracks() {
|
public getVideoTracks() {
|
||||||
return this.tracks.filter((track) => track.kind === "video");
|
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) {
|
public addTrack(track: MockMediaStreamTrack) {
|
||||||
this.tracks.push(track);
|
this.tracks.push(track);
|
||||||
this.dispatchEvent("addtrack");
|
this.dispatchEvent(new Event("addtrack"));
|
||||||
}
|
}
|
||||||
public removeTrack(track: MockMediaStreamTrack) {
|
public removeTrack(track: MockMediaStreamTrack) {
|
||||||
this.tracks.splice(this.tracks.indexOf(track), 1);
|
this.tracks.splice(this.tracks.indexOf(track), 1);
|
||||||
@@ -391,7 +356,7 @@ export class MockMediaHandler {
|
|||||||
public stopUserMediaStream(stream: MockMediaStream) {
|
public stopUserMediaStream(stream: MockMediaStream) {
|
||||||
stream.isStopped = true;
|
stream.isStopped = true;
|
||||||
}
|
}
|
||||||
public getScreensharingStream = jest.fn((opts?: IScreensharingOpts) => {
|
public getScreensharingStream = vi.fn((opts?: IScreensharingOpts) => {
|
||||||
const tracks = [new MockMediaStreamTrack("screenshare_video_track", "video")];
|
const tracks = [new MockMediaStreamTrack("screenshare_video_track", "video")];
|
||||||
if (opts?.audio) tracks.push(new MockMediaStreamTrack("screenshare_audio_track", "audio"));
|
if (opts?.audio) tracks.push(new MockMediaStreamTrack("screenshare_audio_track", "audio"));
|
||||||
|
|
||||||
@@ -416,19 +381,19 @@ export class MockMediaHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class MockMediaDevices {
|
export class MockMediaDevices {
|
||||||
public enumerateDevices = jest
|
public enumerateDevices = vi
|
||||||
.fn<Promise<MediaDeviceInfo[]>, []>()
|
.fn<MediaDevices["enumerateDevices"]>()
|
||||||
.mockResolvedValue([
|
.mockResolvedValue([
|
||||||
new MockMediaDeviceInfo("audioinput").typed(),
|
new MockMediaDeviceInfo("audioinput").typed(),
|
||||||
new MockMediaDeviceInfo("videoinput").typed(),
|
new MockMediaDeviceInfo("videoinput").typed(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
public getUserMedia = jest
|
public getUserMedia = vi
|
||||||
.fn<Promise<MediaStream>, [MediaStreamConstraints]>()
|
.fn<MediaDevices["getUserMedia"]>()
|
||||||
.mockReturnValue(Promise.resolve(new MockMediaStream("local_stream").typed()));
|
.mockReturnValue(Promise.resolve(new MockMediaStream("local_stream").typed()));
|
||||||
|
|
||||||
public getDisplayMedia = jest
|
public getDisplayMedia = vi
|
||||||
.fn<Promise<MediaStream>, [MediaStreamConstraints]>()
|
.fn<MediaDevices["getDisplayMedia"]>()
|
||||||
.mockReturnValue(Promise.resolve(new MockMediaStream("local_display_stream").typed()));
|
.mockReturnValue(Promise.resolve(new MockMediaStream("local_display_stream").typed()));
|
||||||
|
|
||||||
public typed(): MediaDevices {
|
public typed(): MediaDevices {
|
||||||
@@ -462,14 +427,8 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
|
|||||||
calls: new Map<string, MatrixCall>(),
|
calls: new Map<string, MatrixCall>(),
|
||||||
};
|
};
|
||||||
|
|
||||||
public sendStateEvent = jest.fn<
|
public sendStateEvent = vi.fn<MatrixClient["sendStateEvent"]>();
|
||||||
Promise<ISendEventResponse>,
|
public sendToDevice = vi.fn<MatrixClient["sendToDevice"]>();
|
||||||
[roomId: string, eventType: EventType, content: any, statekey: string]
|
|
||||||
>();
|
|
||||||
public sendToDevice = jest.fn<
|
|
||||||
Promise<EmptyObject>,
|
|
||||||
[eventType: string, contentMap: SendToDeviceContentMap, txnId?: string]
|
|
||||||
>();
|
|
||||||
|
|
||||||
public isInitialSyncComplete(): boolean {
|
public isInitialSyncComplete(): boolean {
|
||||||
return false;
|
return false;
|
||||||
@@ -499,11 +458,11 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
|
|||||||
public getUseE2eForGroupCall = () => false;
|
public getUseE2eForGroupCall = () => false;
|
||||||
public checkTurnServers = () => null;
|
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 getRooms = vi.fn<MatrixClient["getRooms"]>().mockReturnValue([]);
|
||||||
public getRoom = jest.fn();
|
public getRoom = vi.fn();
|
||||||
public getFoci = jest.fn();
|
public getFoci = vi.fn();
|
||||||
|
|
||||||
public supportsThreads(): boolean {
|
public supportsThreads(): boolean {
|
||||||
return true;
|
return true;
|
||||||
@@ -534,20 +493,20 @@ export class MockMatrixCall extends TypedEventEmitter<CallEvent, CallEventHandle
|
|||||||
public opponentMember = { userId: this.opponentUserId };
|
public opponentMember = { userId: this.opponentUserId };
|
||||||
public callId = "1";
|
public callId = "1";
|
||||||
public localUsermediaFeed = {
|
public localUsermediaFeed = {
|
||||||
setAudioVideoMuted: jest.fn<void, [boolean, boolean]>(),
|
setAudioVideoMuted: vi.fn<CallFeed["setAudioVideoMuted"]>(),
|
||||||
isAudioMuted: jest.fn().mockReturnValue(false),
|
isAudioMuted: vi.fn().mockReturnValue(false),
|
||||||
isVideoMuted: jest.fn().mockReturnValue(false),
|
isVideoMuted: vi.fn().mockReturnValue(false),
|
||||||
stream: new MockMediaStream("stream"),
|
stream: new MockMediaStream("stream"),
|
||||||
} as unknown as CallFeed;
|
} as unknown as CallFeed;
|
||||||
public remoteUsermediaFeed?: CallFeed;
|
public remoteUsermediaFeed?: CallFeed;
|
||||||
public remoteScreensharingFeed?: CallFeed;
|
public remoteScreensharingFeed?: CallFeed;
|
||||||
|
|
||||||
public reject = jest.fn<void, []>();
|
public reject = vi.fn<() => void>();
|
||||||
public answerWithCallFeeds = jest.fn<void, [CallFeed[]]>();
|
public answerWithCallFeeds = vi.fn<MatrixCall["answerWithCallFeeds"]>();
|
||||||
public hangup = jest.fn<void, []>();
|
public hangup = vi.fn<() => void>();
|
||||||
public initStats = jest.fn<void, []>();
|
public initStats = vi.fn<() => void>();
|
||||||
|
|
||||||
public sendMetadataUpdate = jest.fn<void, []>();
|
public sendMetadataUpdate = vi.fn<() => void>();
|
||||||
|
|
||||||
public getOpponentMember(): Partial<RoomMember> {
|
public getOpponentMember(): Partial<RoomMember> {
|
||||||
return this.opponentMember;
|
return this.opponentMember;
|
||||||
@@ -586,11 +545,11 @@ export class MockCallFeed {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function installWebRTCMocks() {
|
export function installWebRTCMocks() {
|
||||||
globalThis.navigator = {
|
vi.stubGlobal("navigator", {
|
||||||
mediaDevices: new MockMediaDevices().typed(),
|
mediaDevices: new MockMediaDevices().typed(),
|
||||||
} as unknown as Navigator;
|
});
|
||||||
|
|
||||||
globalThis.window = {
|
vi.stubGlobal("window", {
|
||||||
// @ts-ignore Mock
|
// @ts-ignore Mock
|
||||||
RTCPeerConnection: MockRTCPeerConnection,
|
RTCPeerConnection: MockRTCPeerConnection,
|
||||||
// @ts-ignore Mock
|
// @ts-ignore Mock
|
||||||
@@ -598,16 +557,16 @@ export function installWebRTCMocks() {
|
|||||||
// @ts-ignore Mock
|
// @ts-ignore Mock
|
||||||
RTCIceCandidate: {},
|
RTCIceCandidate: {},
|
||||||
getUserMedia: () => new MockMediaStream("local_stream"),
|
getUserMedia: () => new MockMediaStream("local_stream"),
|
||||||
};
|
});
|
||||||
// @ts-ignore Mock
|
|
||||||
globalThis.document = {};
|
vi.stubGlobal("document", {});
|
||||||
|
|
||||||
// @ts-ignore Mock
|
// @ts-ignore Mock
|
||||||
globalThis.AudioContext = MockAudioContext;
|
globalThis.AudioContext = MockAudioContext;
|
||||||
|
|
||||||
// @ts-ignore Mock
|
// @ts-ignore Mock
|
||||||
globalThis.RTCRtpReceiver = {
|
globalThis.RTCRtpReceiver = {
|
||||||
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
|
getCapabilities: vi.fn().mockReturnValue({
|
||||||
codecs: [],
|
codecs: [],
|
||||||
headerExtensions: [],
|
headerExtensions: [],
|
||||||
}),
|
}),
|
||||||
@@ -615,7 +574,7 @@ export function installWebRTCMocks() {
|
|||||||
|
|
||||||
// @ts-ignore Mock
|
// @ts-ignore Mock
|
||||||
globalThis.RTCRtpSender = {
|
globalThis.RTCRtpSender = {
|
||||||
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
|
getCapabilities: vi.fn().mockReturnValue({
|
||||||
codecs: [],
|
codecs: [],
|
||||||
headerExtensions: [],
|
headerExtensions: [],
|
||||||
}),
|
}),
|
||||||
@@ -632,22 +591,22 @@ export function makeMockGroupCallStateEvent(
|
|||||||
redacted?: boolean,
|
redacted?: boolean,
|
||||||
): MatrixEvent {
|
): MatrixEvent {
|
||||||
return {
|
return {
|
||||||
getType: jest.fn().mockReturnValue(EventType.GroupCallPrefix),
|
getType: vi.fn().mockReturnValue(EventType.GroupCallPrefix),
|
||||||
getRoomId: jest.fn().mockReturnValue(roomId),
|
getRoomId: vi.fn().mockReturnValue(roomId),
|
||||||
getTs: jest.fn().mockReturnValue(0),
|
getTs: vi.fn().mockReturnValue(0),
|
||||||
getContent: jest.fn().mockReturnValue(content),
|
getContent: vi.fn().mockReturnValue(content),
|
||||||
getStateKey: jest.fn().mockReturnValue(groupCallId),
|
getStateKey: vi.fn().mockReturnValue(groupCallId),
|
||||||
isRedacted: jest.fn().mockReturnValue(redacted ?? false),
|
isRedacted: vi.fn().mockReturnValue(redacted ?? false),
|
||||||
} as unknown as MatrixEvent;
|
} as unknown as MatrixEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeMockGroupCallMemberStateEvent(roomId: string, groupCallId: string): MatrixEvent {
|
export function makeMockGroupCallMemberStateEvent(roomId: string, groupCallId: string): MatrixEvent {
|
||||||
return {
|
return {
|
||||||
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
getType: vi.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
||||||
getRoomId: jest.fn().mockReturnValue(roomId),
|
getRoomId: vi.fn().mockReturnValue(roomId),
|
||||||
getTs: jest.fn().mockReturnValue(0),
|
getTs: vi.fn().mockReturnValue(0),
|
||||||
getContent: jest.fn().mockReturnValue({}),
|
getContent: vi.fn().mockReturnValue({}),
|
||||||
getStateKey: jest.fn().mockReturnValue(groupCallId),
|
getStateKey: vi.fn().mockReturnValue(groupCallId),
|
||||||
} as unknown as MatrixEvent;
|
} as unknown as MatrixEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ describe("NamespacedValue", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should have a falsey unstable if needed", () => {
|
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.name).toBe(ns.stable);
|
||||||
expect(ns.altName).toBeFalsy();
|
expect(ns.altName).toBeFalsy();
|
||||||
expect(ns.names).toEqual([ns.stable]);
|
expect(ns.names).toEqual([ns.stable]);
|
||||||
@@ -61,7 +61,7 @@ describe("UnstableValue", () => {
|
|||||||
it("should return unstable if there is no stable", () => {
|
it("should return unstable if there is no stable", () => {
|
||||||
const ns = new UnstableValue(null!, "unstable");
|
const ns = new UnstableValue(null!, "unstable");
|
||||||
expect(ns.name).toBe(ns.unstable);
|
expect(ns.name).toBe(ns.unstable);
|
||||||
expect(ns.altName).toBeFalsy();
|
expect(<any>ns.altName).toBeFalsy();
|
||||||
expect(ns.names).toEqual([ns.unstable]);
|
expect(ns.names).toEqual([ns.unstable]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ describe("ReEmitter", function () {
|
|||||||
const src = new EventSource();
|
const src = new EventSource();
|
||||||
const tgt = new EventTarget();
|
const tgt = new EventTarget();
|
||||||
|
|
||||||
const handler = jest.fn();
|
const handler = vi.fn();
|
||||||
tgt.on(EVENTNAME, handler);
|
tgt.on(EVENTNAME, handler);
|
||||||
|
|
||||||
const reEmitter = new ReEmitter(tgt);
|
const reEmitter = new ReEmitter(tgt);
|
||||||
@@ -61,7 +61,7 @@ describe("ReEmitter", function () {
|
|||||||
// without the workaround in ReEmitter, this would throw
|
// without the workaround in ReEmitter, this would throw
|
||||||
src.doAnError();
|
src.doAnError();
|
||||||
|
|
||||||
const handler = jest.fn();
|
const handler = vi.fn();
|
||||||
tgt.on("error", handler);
|
tgt.on("error", handler);
|
||||||
|
|
||||||
src.doAnError();
|
src.doAnError();
|
||||||
|
|||||||
@@ -35,16 +35,16 @@ describe("onResumedSync", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
store = new StubStore();
|
store = new StubStore();
|
||||||
store.getOldestToDeviceBatch = jest.fn().mockImplementation(() => {
|
store.getOldestToDeviceBatch = vi.fn().mockImplementation(() => {
|
||||||
return batch;
|
return batch;
|
||||||
});
|
});
|
||||||
store.removeToDeviceBatch = jest.fn().mockImplementation(() => {
|
store.removeToDeviceBatch = vi.fn().mockImplementation(() => {
|
||||||
batch = null;
|
batch = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
mockClient = getMockClientWithEventEmitter({});
|
mockClient = getMockClientWithEventEmitter({});
|
||||||
mockClient.store = store;
|
mockClient.store = store;
|
||||||
mockClient.sendToDevice = jest.fn().mockImplementation(async () => {
|
mockClient.sendToDevice = vi.fn().mockImplementation(async () => {
|
||||||
if (shouldFailSendToDevice) {
|
if (shouldFailSendToDevice) {
|
||||||
await Promise.reject(new ConnectionError("")).finally(() => {
|
await Promise.reject(new ConnectionError("")).finally(() => {
|
||||||
setTimeout(onSendToDeviceFailure, 0);
|
setTimeout(onSendToDeviceFailure, 0);
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ describe("AutoDiscovery", function () {
|
|||||||
AutoDiscovery.setFetchFn(realAutoDiscoveryFetch);
|
AutoDiscovery.setFetchFn(realAutoDiscoveryFetch);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @vitest/expect-expect
|
||||||
it("should throw an error when no domain is specified", function () {
|
it("should throw an error when no domain is specified", function () {
|
||||||
getHttpBackend();
|
getHttpBackend();
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
@@ -190,7 +191,7 @@ describe("AutoDiscovery", function () {
|
|||||||
};
|
};
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
httpBackend.flushAllExpected(),
|
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()", () => {
|
describe("makeBeaconInfoContent()", () => {
|
||||||
const mockDateNow = 123456789;
|
const mockDateNow = 123456789;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(globalThis.Date, "now").mockReturnValue(mockDateNow);
|
vi.spyOn(globalThis.Date, "now").mockReturnValue(mockDateNow);
|
||||||
});
|
});
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
jest.spyOn(globalThis.Date, "now").mockRestore();
|
vi.spyOn(globalThis.Date, "now").mockRestore();
|
||||||
});
|
});
|
||||||
it("create fully defined event content", () => {
|
it("create fully defined event content", () => {
|
||||||
expect(makeBeaconInfoContent(1234, true, "nice beacon_info", LocationAssetType.Pin)).toEqual({
|
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", () => {
|
it("creates fully defined event content without html", () => {
|
||||||
expect(makeTopicContent("pizza")).toEqual({
|
expect(makeTopicContent("pizza")).toEqual({
|
||||||
topic: "pizza",
|
topic: "pizza",
|
||||||
[M_TOPIC.name]: [
|
[M_TOPIC.name]: {
|
||||||
{
|
"m.text": [
|
||||||
body: "pizza",
|
{
|
||||||
mimetype: "text/plain",
|
body: "pizza",
|
||||||
},
|
mimetype: "text/plain",
|
||||||
],
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates fully defined event content with html", () => {
|
it("creates fully defined event content with html", () => {
|
||||||
expect(makeTopicContent("pizza", "<b>pizza</b>")).toEqual({
|
expect(makeTopicContent("pizza", "<b>pizza</b>")).toEqual({
|
||||||
topic: "pizza",
|
topic: "pizza",
|
||||||
[M_TOPIC.name]: [
|
[M_TOPIC.name]: {
|
||||||
{
|
"m.text": [
|
||||||
body: "<b>pizza</b>",
|
{
|
||||||
mimetype: "text/html",
|
body: "<b>pizza</b>",
|
||||||
},
|
mimetype: "text/html",
|
||||||
{
|
},
|
||||||
body: "pizza",
|
{
|
||||||
mimetype: "text/plain",
|
body: "pizza",
|
||||||
},
|
mimetype: "text/plain",
|
||||||
],
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates an empty event when the topic is falsey", () => {
|
it("creates an empty event when the topic is falsey", () => {
|
||||||
expect(makeTopicContent(undefined)).toEqual({
|
expect(makeTopicContent(undefined)).toEqual({
|
||||||
topic: undefined,
|
topic: undefined,
|
||||||
[M_TOPIC.name]: [],
|
[M_TOPIC.name]: { "m.text": [] },
|
||||||
});
|
});
|
||||||
expect(makeTopicContent(null)).toEqual({
|
expect(makeTopicContent(null)).toEqual({
|
||||||
topic: null,
|
topic: null,
|
||||||
[M_TOPIC.name]: [],
|
[M_TOPIC.name]: { "m.text": [] },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -225,11 +229,13 @@ describe("Topic content helpers", () => {
|
|||||||
expect(
|
expect(
|
||||||
parseTopicContent({
|
parseTopicContent({
|
||||||
topic: "pizza",
|
topic: "pizza",
|
||||||
[M_TOPIC.name]: [
|
[M_TOPIC.name]: {
|
||||||
{
|
"m.text": [
|
||||||
body: "pizza",
|
{
|
||||||
},
|
body: "pizza",
|
||||||
],
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
text: "pizza",
|
text: "pizza",
|
||||||
@@ -240,12 +246,14 @@ describe("Topic content helpers", () => {
|
|||||||
expect(
|
expect(
|
||||||
parseTopicContent({
|
parseTopicContent({
|
||||||
topic: "pizza",
|
topic: "pizza",
|
||||||
[M_TOPIC.name]: [
|
[M_TOPIC.name]: {
|
||||||
{
|
"m.text": [
|
||||||
body: "pizza",
|
{
|
||||||
mimetype: "text/plain",
|
body: "pizza",
|
||||||
},
|
mimetype: "text/plain",
|
||||||
],
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
text: "pizza",
|
text: "pizza",
|
||||||
@@ -256,12 +264,14 @@ describe("Topic content helpers", () => {
|
|||||||
expect(
|
expect(
|
||||||
parseTopicContent({
|
parseTopicContent({
|
||||||
topic: "pizza",
|
topic: "pizza",
|
||||||
[M_TOPIC.name]: [
|
[M_TOPIC.name]: {
|
||||||
{
|
"m.text": [
|
||||||
body: "<b>pizza</b>",
|
{
|
||||||
mimetype: "text/html",
|
body: "<b>pizza</b>",
|
||||||
},
|
mimetype: "text/html",
|
||||||
],
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
text: "pizza",
|
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(
|
expect(
|
||||||
parseTopicContent({
|
parseTopicContent({
|
||||||
"topic": "pizza",
|
"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({
|
).toEqual({
|
||||||
text: "pizza",
|
text: "pizza",
|
||||||
|
|||||||
@@ -14,8 +14,11 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @vitest-environment happy-dom
|
||||||
|
*/
|
||||||
|
|
||||||
import "fake-indexeddb/auto";
|
import "fake-indexeddb/auto";
|
||||||
import "jest-localstorage-mock";
|
|
||||||
import { IndexedDBCryptoStore, LocalStorageCryptoStore, MemoryCryptoStore } from "../../../../src";
|
import { IndexedDBCryptoStore, LocalStorageCryptoStore, MemoryCryptoStore } from "../../../../src";
|
||||||
import { type CryptoStore, MigrationState, SESSION_BATCH_SIZE } from "../../../../src/crypto/store/base";
|
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 () => {
|
it("throws if webcrypto is not available", async () => {
|
||||||
const oldCrypto = globalThis.crypto;
|
vi.stubGlobal("crypto", {});
|
||||||
try {
|
await expect(sha256("test")).rejects.toThrow();
|
||||||
globalThis.crypto = {} as any;
|
|
||||||
await expect(sha256("test")).rejects.toThrow();
|
|
||||||
} finally {
|
|
||||||
globalThis.crypto = oldCrypto;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+268
-51
@@ -1,7 +1,3 @@
|
|||||||
/**
|
|
||||||
* @jest-environment jsdom
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
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
|
// project, which doesn't know about our TypeEventEmitter implementation at all
|
||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import { type MockedObject } from "jest-mock";
|
import { type MockedObject } from "vitest";
|
||||||
import {
|
import {
|
||||||
type WidgetApi,
|
type WidgetApi,
|
||||||
WidgetApiToWidgetAction,
|
WidgetApiToWidgetAction,
|
||||||
@@ -36,7 +32,14 @@ import {
|
|||||||
type IRoomEvent,
|
type IRoomEvent,
|
||||||
} from "matrix-widget-api";
|
} 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 { MatrixClient, ClientEvent, type ITurnServer as IClientTurnServer } from "../../src/client";
|
||||||
import { SyncState } from "../../src/sync";
|
import { SyncState } from "../../src/sync";
|
||||||
import { type ICapabilities, type RoomWidgetClient } from "../../src/embedded";
|
import { type ICapabilities, type RoomWidgetClient } from "../../src/embedded";
|
||||||
@@ -46,6 +49,7 @@ import { sleep } from "../../src/utils";
|
|||||||
import { SlidingSync } from "../../src/sliding-sync";
|
import { SlidingSync } from "../../src/sliding-sync";
|
||||||
import { logger } from "../../src/logger";
|
import { logger } from "../../src/logger";
|
||||||
import { flushPromises } from "../test-utils/flushPromises";
|
import { flushPromises } from "../test-utils/flushPromises";
|
||||||
|
import { RoomStickyEventsEvent, type RoomStickyEventsMap } from "../../src/models/room-sticky-events";
|
||||||
|
|
||||||
const testOIDCToken = {
|
const testOIDCToken = {
|
||||||
access_token: "12345678",
|
access_token: "12345678",
|
||||||
@@ -53,27 +57,28 @@ const testOIDCToken = {
|
|||||||
matrix_server_name: "homeserver.oabc",
|
matrix_server_name: "homeserver.oabc",
|
||||||
token_type: "Bearer",
|
token_type: "Bearer",
|
||||||
};
|
};
|
||||||
|
|
||||||
class MockWidgetApi extends EventEmitter {
|
class MockWidgetApi extends EventEmitter {
|
||||||
public start = jest.fn().mockResolvedValue(undefined);
|
public start = vi.fn().mockResolvedValue(undefined);
|
||||||
public getClientVersions = jest.fn();
|
public getClientVersions = vi.fn();
|
||||||
public requestCapability = jest.fn().mockResolvedValue(undefined);
|
public requestCapability = vi.fn().mockResolvedValue(undefined);
|
||||||
public requestCapabilities = jest.fn().mockResolvedValue(undefined);
|
public requestCapabilities = vi.fn().mockResolvedValue(undefined);
|
||||||
public requestCapabilityForRoomTimeline = jest.fn().mockResolvedValue(undefined);
|
public requestCapabilityForRoomTimeline = vi.fn().mockResolvedValue(undefined);
|
||||||
public requestCapabilityToSendEvent = jest.fn().mockResolvedValue(undefined);
|
public requestCapabilityToSendEvent = vi.fn().mockResolvedValue(undefined);
|
||||||
public requestCapabilityToReceiveEvent = jest.fn().mockResolvedValue(undefined);
|
public requestCapabilityToReceiveEvent = vi.fn().mockResolvedValue(undefined);
|
||||||
public requestCapabilityToSendMessage = jest.fn().mockResolvedValue(undefined);
|
public requestCapabilityToSendMessage = vi.fn().mockResolvedValue(undefined);
|
||||||
public requestCapabilityToReceiveMessage = jest.fn().mockResolvedValue(undefined);
|
public requestCapabilityToReceiveMessage = vi.fn().mockResolvedValue(undefined);
|
||||||
public requestCapabilityToSendState = jest.fn().mockResolvedValue(undefined);
|
public requestCapabilityToSendState = vi.fn().mockResolvedValue(undefined);
|
||||||
public requestCapabilityToReceiveState = jest.fn().mockResolvedValue(undefined);
|
public requestCapabilityToReceiveState = vi.fn().mockResolvedValue(undefined);
|
||||||
public requestCapabilityToSendToDevice = jest.fn().mockResolvedValue(undefined);
|
public requestCapabilityToSendToDevice = vi.fn().mockResolvedValue(undefined);
|
||||||
public requestCapabilityToReceiveToDevice = jest.fn().mockResolvedValue(undefined);
|
public requestCapabilityToReceiveToDevice = vi.fn().mockResolvedValue(undefined);
|
||||||
public sendRoomEvent = jest.fn(
|
public sendRoomEvent = vi.fn(
|
||||||
async (eventType: string, content: unknown, roomId?: string, delay?: number, parentDelayId?: string) =>
|
async (eventType: string, content: unknown, roomId?: string, delay?: number, parentDelayId?: string) =>
|
||||||
delay === undefined && parentDelayId === undefined
|
delay === undefined && parentDelayId === undefined
|
||||||
? { event_id: `$${Math.random()}` }
|
? { event_id: `$${Math.random()}` }
|
||||||
: { delay_id: `id-${Math.random()}` },
|
: { delay_id: `id-${Math.random()}` },
|
||||||
);
|
);
|
||||||
public sendStateEvent = jest.fn(
|
public sendStateEvent = vi.fn(
|
||||||
async (
|
async (
|
||||||
eventType: string,
|
eventType: string,
|
||||||
stateKey: string,
|
stateKey: string,
|
||||||
@@ -86,22 +91,24 @@ class MockWidgetApi extends EventEmitter {
|
|||||||
? { event_id: `$${Math.random()}` }
|
? { event_id: `$${Math.random()}` }
|
||||||
: { delay_id: `id-${Math.random()}` },
|
: { delay_id: `id-${Math.random()}` },
|
||||||
);
|
);
|
||||||
public updateDelayedEvent = jest.fn().mockResolvedValue(undefined);
|
public cancelScheduledDelayedEvent = vi.fn().mockResolvedValue(undefined);
|
||||||
public sendToDevice = jest.fn().mockResolvedValue(undefined);
|
public restartScheduledDelayedEvent = vi.fn().mockResolvedValue(undefined);
|
||||||
public requestOpenIDConnectToken = jest.fn(async () => {
|
public sendScheduledDelayedEvent = vi.fn().mockResolvedValue(undefined);
|
||||||
|
public sendToDevice = vi.fn().mockResolvedValue(undefined);
|
||||||
|
public requestOpenIDConnectToken = vi.fn(async () => {
|
||||||
return testOIDCToken;
|
return testOIDCToken;
|
||||||
return new Promise<IOpenIDCredentials>(() => {
|
return new Promise<IOpenIDCredentials>(() => {
|
||||||
return testOIDCToken;
|
return testOIDCToken;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
public readStateEvents = jest.fn(async () => []);
|
public readStateEvents = vi.fn(async () => []);
|
||||||
public getTurnServers = jest.fn(async () => []);
|
public getTurnServers = vi.fn(async () => []);
|
||||||
public sendContentLoaded = jest.fn().mockResolvedValue(undefined);
|
public sendContentLoaded = vi.fn().mockResolvedValue(undefined);
|
||||||
|
|
||||||
public transport = {
|
public transport = {
|
||||||
reply: jest.fn(),
|
reply: vi.fn(),
|
||||||
send: jest.fn(),
|
send: vi.fn(),
|
||||||
sendComplete: jest.fn(),
|
sendComplete: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -169,6 +176,9 @@ describe("RoomWidgetClient", () => {
|
|||||||
"org.matrix.rageshake_request",
|
"org.matrix.rageshake_request",
|
||||||
{ request_id: 123 },
|
{ request_id: 123 },
|
||||||
"!1:example.org",
|
"!1:example.org",
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -228,7 +238,7 @@ describe("RoomWidgetClient", () => {
|
|||||||
);
|
);
|
||||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||||
expect(widgetApi.requestCapabilityToReceiveEvent).toHaveBeenCalledWith("org.matrix.rageshake_request");
|
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 widgetSendEmitter = new EventEmitter();
|
||||||
const widgetSendPromise = new Promise<void>((resolve) =>
|
const widgetSendPromise = new Promise<void>((resolve) =>
|
||||||
widgetSendEmitter.once("send", () => resolve()),
|
widgetSendEmitter.once("send", () => resolve()),
|
||||||
@@ -355,10 +365,10 @@ describe("RoomWidgetClient", () => {
|
|||||||
|
|
||||||
it("handles widget errors with generic error data", async () => {
|
it("handles widget errors with generic error data", async () => {
|
||||||
const error = new Error("failed to send");
|
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"] });
|
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
|
||||||
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send);
|
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send as any);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
|
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
|
||||||
@@ -381,22 +391,22 @@ describe("RoomWidgetClient", () => {
|
|||||||
response: errorData,
|
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"] });
|
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
|
||||||
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send);
|
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send as any);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
|
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
|
||||||
).rejects.toThrow(matrixError);
|
).rejects.toStrictEqual(matrixError);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("delayed events", () => {
|
describe("delayed events", () => {
|
||||||
describe("when supported", () => {
|
describe("when supported", () => {
|
||||||
const doesServerSupportUnstableFeatureMock = jest.fn((feature) =>
|
const doesServerSupportUnstableFeatureMock = vi.fn((feature) =>
|
||||||
Promise.resolve(feature === "org.matrix.msc4140"),
|
Promise.resolve(feature === "org.matrix.msc4140"),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -424,6 +434,7 @@ describe("RoomWidgetClient", () => {
|
|||||||
"!1:example.org",
|
"!1:example.org",
|
||||||
2000,
|
2000,
|
||||||
undefined,
|
undefined,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -444,6 +455,7 @@ describe("RoomWidgetClient", () => {
|
|||||||
"!1:example.org",
|
"!1:example.org",
|
||||||
undefined,
|
undefined,
|
||||||
parentDelayId,
|
parentDelayId,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -531,21 +543,63 @@ describe("RoomWidgetClient", () => {
|
|||||||
).rejects.toThrow();
|
).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"] });
|
await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
|
||||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157UpdateDelayedEvent);
|
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157UpdateDelayedEvent);
|
||||||
for (const action of [
|
await client._unstable_cancelScheduledDelayedEvent("id");
|
||||||
UpdateDelayedEventAction.Cancel,
|
expect(widgetApi.cancelScheduledDelayedEvent).toHaveBeenCalledWith("id");
|
||||||
UpdateDelayedEventAction.Restart,
|
});
|
||||||
UpdateDelayedEventAction.Send,
|
|
||||||
]) {
|
it("can restart scheduled delayed events (action in method)", async () => {
|
||||||
await client._unstable_updateDelayedEvent("id", action);
|
await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
|
||||||
expect(widgetApi.updateDelayedEvent).toHaveBeenCalledWith("id", action);
|
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", () => {
|
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 () => {
|
it("fails to send delayed message events", async () => {
|
||||||
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
|
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
|
||||||
await expect(
|
await expect(
|
||||||
@@ -583,6 +637,13 @@ describe("RoomWidgetClient", () => {
|
|||||||
"Server does not support",
|
"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 emittedEvent = new Promise<MatrixEvent>((resolve) => client.once(ClientEvent.Event, resolve));
|
||||||
const emittedSync = new Promise<SyncState>((resolve) => client.once(ClientEvent.Sync, 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(
|
widgetApi.emit(
|
||||||
`action:${WidgetApiToWidgetAction.SendEvent}`,
|
`action:${WidgetApiToWidgetAction.SendEvent}`,
|
||||||
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
|
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", () => {
|
describe("to-device messages", () => {
|
||||||
const unencryptedContentMap = new Map([
|
const unencryptedContentMap = new Map([
|
||||||
["@alice:example.org", new Map([["*", { hello: "alice!" }]])],
|
["@alice:example.org", new Map([["*", { hello: "alice!" }]])],
|
||||||
@@ -929,7 +1146,7 @@ describe("RoomWidgetClient", () => {
|
|||||||
|
|
||||||
it("handles widget errors with generic error data", async () => {
|
it("handles widget errors with generic error data", async () => {
|
||||||
const error = new Error("failed to get token");
|
const error = new Error("failed to get token");
|
||||||
widgetApi.transport.sendComplete.mockRejectedValue(error);
|
vi.mocked(widgetApi.transport.sendComplete).mockRejectedValue(error);
|
||||||
|
|
||||||
await makeClient({});
|
await makeClient({});
|
||||||
widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any);
|
widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any);
|
||||||
@@ -953,9 +1170,9 @@ describe("RoomWidgetClient", () => {
|
|||||||
response: errorData,
|
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({});
|
await makeClient({});
|
||||||
widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any);
|
widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any);
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ describe("eventMapperFor", function () {
|
|||||||
setUserCreator(_) {},
|
setUserCreator(_) {},
|
||||||
} as IStore,
|
} as IStore,
|
||||||
scheduler: {
|
scheduler: {
|
||||||
setProcessFunction: jest.fn(),
|
setProcessFunction: vi.fn(),
|
||||||
} as unknown as MatrixScheduler,
|
} as unknown as MatrixScheduler,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
});
|
});
|
||||||
@@ -133,7 +133,7 @@ describe("eventMapperFor", function () {
|
|||||||
event_id: eventId,
|
event_id: eventId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const decryptEventIfNeededSpy = jest.spyOn(client, "decryptEventIfNeeded");
|
const decryptEventIfNeededSpy = vi.spyOn(client, "decryptEventIfNeeded");
|
||||||
decryptEventIfNeededSpy.mockResolvedValue(); // stub it out
|
decryptEventIfNeededSpy.mockResolvedValue(); // stub it out
|
||||||
|
|
||||||
const mapper = eventMapperFor(client, {
|
const mapper = eventMapperFor(client, {
|
||||||
@@ -161,7 +161,7 @@ describe("eventMapperFor", function () {
|
|||||||
event_id: eventId,
|
event_id: eventId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const evListener = jest.fn();
|
const evListener = vi.fn();
|
||||||
client.on(MatrixEventEvent.Replaced, evListener);
|
client.on(MatrixEventEvent.Replaced, evListener);
|
||||||
|
|
||||||
const noReEmitMapper = eventMapperFor(client, {
|
const noReEmitMapper = eventMapperFor(client, {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { mocked } from "jest-mock";
|
import { type MockInstance } from "vitest";
|
||||||
|
|
||||||
import * as utils from "../test-utils/test-utils";
|
import * as utils from "../test-utils/test-utils";
|
||||||
import {
|
import {
|
||||||
@@ -196,7 +196,7 @@ describe("EventTimelineSet", () => {
|
|||||||
|
|
||||||
it("should aggregate relations which belong to unknown timeline without adding them to any timeline", () => {
|
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
|
// 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 reactionEvent = utils.mkReaction(messageEvent, client, client.getSafeUserId(), roomId);
|
||||||
|
|
||||||
const liveTimeline = eventTimelineSet.getLiveTimeline();
|
const liveTimeline = eventTimelineSet.getLiveTimeline();
|
||||||
@@ -228,7 +228,7 @@ describe("EventTimelineSet", () => {
|
|||||||
let thread: Thread;
|
let thread: Thread;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
(client.supportsThreads as jest.Mock).mockReturnValue(true);
|
vi.mocked(client.supportsThreads).mockReturnValue(true);
|
||||||
thread = new Thread("!thread_id:server", messageEvent, { room, client });
|
thread = new Thread("!thread_id:server", messageEvent, { room, client });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -306,20 +306,20 @@ describe("EventTimelineSet", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("with events to be decrypted", () => {
|
describe("with events to be decrypted", () => {
|
||||||
let messageEventShouldAttemptDecryptionSpy: jest.SpyInstance;
|
let messageEventShouldAttemptDecryptionSpy: MockInstance;
|
||||||
let messageEventIsDecryptionFailureSpy: jest.SpyInstance;
|
let messageEventIsDecryptionFailureSpy: MockInstance;
|
||||||
|
|
||||||
let replyEventShouldAttemptDecryptionSpy: jest.SpyInstance;
|
let replyEventShouldAttemptDecryptionSpy: MockInstance;
|
||||||
let replyEventIsDecryptionFailureSpy: jest.SpyInstance;
|
let replyEventIsDecryptionFailureSpy: MockInstance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
messageEventShouldAttemptDecryptionSpy = jest.spyOn(messageEvent, "shouldAttemptDecryption");
|
messageEventShouldAttemptDecryptionSpy = vi.spyOn(messageEvent, "shouldAttemptDecryption");
|
||||||
messageEventShouldAttemptDecryptionSpy.mockReturnValue(true);
|
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);
|
replyEventShouldAttemptDecryptionSpy.mockReturnValue(true);
|
||||||
replyEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, "isDecryptionFailure");
|
replyEventIsDecryptionFailureSpy = vi.spyOn(messageEvent, "isDecryptionFailure");
|
||||||
|
|
||||||
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, false, eventTimeline, "foo");
|
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, false, eventTimeline, "foo");
|
||||||
});
|
});
|
||||||
@@ -384,7 +384,7 @@ describe("EventTimelineSet", () => {
|
|||||||
let thread: Thread;
|
let thread: Thread;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
(client.supportsThreads as jest.Mock).mockReturnValue(true);
|
vi.mocked(client.supportsThreads).mockReturnValue(true);
|
||||||
thread = new Thread("!thread_id:server", messageEvent, { room, client });
|
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 * as utils from "../test-utils/test-utils";
|
||||||
import { Direction, EventTimeline } from "../../src/models/event-timeline";
|
import { Direction, EventTimeline } from "../../src/models/event-timeline";
|
||||||
import { RoomState } from "../../src/models/room-state";
|
import { RoomState } from "../../src/models/room-state";
|
||||||
@@ -20,21 +18,21 @@ describe("EventTimeline", function () {
|
|||||||
const getTimeline = (): EventTimeline => {
|
const getTimeline = (): EventTimeline => {
|
||||||
const room = new Room(roomId, mockClient, userA);
|
const room = new Room(roomId, mockClient, userA);
|
||||||
const timelineSet = new EventTimelineSet(room);
|
const timelineSet = new EventTimelineSet(room);
|
||||||
jest.spyOn(room, "getUnfilteredTimelineSet").mockReturnValue(timelineSet);
|
vi.spyOn(room, "getUnfilteredTimelineSet").mockReturnValue(timelineSet);
|
||||||
|
|
||||||
const timeline = new EventTimeline(timelineSet);
|
const timeline = new EventTimeline(timelineSet);
|
||||||
// We manually stub the methods we'll be mocking out later instead of mocking the whole module
|
// 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
|
// otherwise the default member property values (e.g. paginationToken) will be incorrect
|
||||||
timeline.getState(Direction.Backward)!.setStateEvents = jest.fn();
|
timeline.getState(Direction.Backward)!.setStateEvents = vi.fn();
|
||||||
timeline.getState(Direction.Backward)!.getSentinelMember = jest.fn();
|
timeline.getState(Direction.Backward)!.getSentinelMember = vi.fn();
|
||||||
timeline.getState(Direction.Forward)!.setStateEvents = jest.fn();
|
timeline.getState(Direction.Forward)!.setStateEvents = vi.fn();
|
||||||
timeline.getState(Direction.Forward)!.getSentinelMember = jest.fn();
|
timeline.getState(Direction.Forward)!.getSentinelMember = vi.fn();
|
||||||
return timeline;
|
return timeline;
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
// reset any RoomState mocks
|
// reset any RoomState mocks
|
||||||
jest.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
|
|
||||||
timeline = getTimeline();
|
timeline = getTimeline();
|
||||||
});
|
});
|
||||||
@@ -67,12 +65,12 @@ describe("EventTimeline", function () {
|
|||||||
timeline.initialiseState(events);
|
timeline.initialiseState(events);
|
||||||
// @ts-ignore private prop
|
// @ts-ignore private prop
|
||||||
const timelineStartState = timeline.startState!;
|
const timelineStartState = timeline.startState!;
|
||||||
expect(mocked(timelineStartState).setStateEvents).toHaveBeenCalledWith(events, {
|
expect(vi.mocked(timelineStartState).setStateEvents).toHaveBeenCalledWith(events, {
|
||||||
timelineWasEmpty: undefined,
|
timelineWasEmpty: undefined,
|
||||||
});
|
});
|
||||||
// @ts-ignore private prop
|
// @ts-ignore private prop
|
||||||
const timelineEndState = timeline.endState!;
|
const timelineEndState = timeline.endState!;
|
||||||
expect(mocked(timelineEndState).setStateEvents).toHaveBeenCalledWith(events, {
|
expect(vi.mocked(timelineEndState).setStateEvents).toHaveBeenCalledWith(events, {
|
||||||
timelineWasEmpty: undefined,
|
timelineWasEmpty: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -210,13 +208,13 @@ describe("EventTimeline", function () {
|
|||||||
sentinel.name = "Old Alice";
|
sentinel.name = "Old Alice";
|
||||||
sentinel.membership = KnownMembership.Join;
|
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) {
|
if (uid === userA) {
|
||||||
return sentinel;
|
return sentinel;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
vi.mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||||
if (uid === userA) {
|
if (uid === userA) {
|
||||||
return oldSentinel;
|
return oldSentinel;
|
||||||
}
|
}
|
||||||
@@ -253,13 +251,13 @@ describe("EventTimeline", function () {
|
|||||||
sentinel.name = "Old Alice";
|
sentinel.name = "Old Alice";
|
||||||
sentinel.membership = KnownMembership.Join;
|
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) {
|
if (uid === userA) {
|
||||||
return sentinel;
|
return sentinel;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
vi.mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||||
if (uid === userA) {
|
if (uid === userA) {
|
||||||
return oldSentinel;
|
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",
|
"base": "http://baseUrl",
|
||||||
"params": {
|
"params": {
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ describe("MatrixError", () => {
|
|||||||
|
|
||||||
it("should retrieve Date Retry-After header from rate-limit error", () => {
|
it("should retrieve Date Retry-After header from rate-limit error", () => {
|
||||||
headers.set("Retry-After", `${new Date(160000).toUTCString()}`);
|
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 });
|
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED", retry_after_ms: 150000 });
|
||||||
expect(err.isRateLimitError()).toBe(true);
|
expect(err.isRateLimitError()).toBe(true);
|
||||||
// prefer Retry-After header over retry_after_ms
|
// prefer Retry-After header over retry_after_ms
|
||||||
|
|||||||
+165
-105
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
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 { FetchHttpApi } from "../../../src/http-api/fetch";
|
||||||
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
|
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
|
||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
type IHttpOpts,
|
type IHttpOpts,
|
||||||
MatrixError,
|
MatrixError,
|
||||||
Method,
|
Method,
|
||||||
|
TokenRefreshError,
|
||||||
} from "../../../src";
|
} from "../../../src";
|
||||||
import { emitPromise } from "../../test-utils/test-utils";
|
import { emitPromise } from "../../test-utils/test-utils";
|
||||||
import { type QueryDict, sleep } from "../../../src/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);
|
const tokenInactiveError = new MatrixError({ errcode: "M_UNKNOWN_TOKEN", error: "Token is not active" }, 401);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should support aborting multiple times", () => {
|
it("should support aborting multiple times", () => {
|
||||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
const fetchFn = makeMockFetchFn();
|
||||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
|
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
|
||||||
|
|
||||||
api.request(Method.Get, "/foo");
|
api.request(Method.Get, "/foo");
|
||||||
api.request(Method.Get, "/baz");
|
api.request(Method.Get, "/baz");
|
||||||
expect(fetchFn.mock.calls[0][0].href.endsWith("/foo")).toBeTruthy();
|
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[0][1]?.signal?.aborted).toBeFalsy();
|
||||||
expect(fetchFn.mock.calls[1][0].href.endsWith("/baz")).toBeTruthy();
|
expect((fetchFn.mock.calls[1][0] as URL).href.endsWith("/baz")).toBeTruthy();
|
||||||
expect(fetchFn.mock.calls[1][1].signal.aborted).toBeFalsy();
|
expect(fetchFn.mock.calls[1][1]?.signal?.aborted).toBeFalsy();
|
||||||
|
|
||||||
api.abort();
|
api.abort();
|
||||||
expect(fetchFn.mock.calls[0][1].signal.aborted).toBeTruthy();
|
expect(fetchFn.mock.calls[0][1]?.signal?.aborted).toBeTruthy();
|
||||||
expect(fetchFn.mock.calls[1][1].signal.aborted).toBeTruthy();
|
expect(fetchFn.mock.calls[1][1]?.signal?.aborted).toBeTruthy();
|
||||||
|
|
||||||
api.request(Method.Get, "/bar");
|
api.request(Method.Get, "/bar");
|
||||||
expect(fetchFn.mock.calls[2][0].href.endsWith("/bar")).toBeTruthy();
|
expect((fetchFn.mock.calls[2][0] as URL).href.endsWith("/bar")).toBeTruthy();
|
||||||
expect(fetchFn.mock.calls[2][1].signal.aborted).toBeFalsy();
|
expect(fetchFn.mock.calls[2][1]?.signal?.aborted).toBeFalsy();
|
||||||
|
|
||||||
api.abort();
|
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", () => {
|
it("should fall back to global fetch if fetchFn not provided", () => {
|
||||||
globalThis.fetch = jest.fn();
|
const spy = (globalThis.fetch = vi.fn());
|
||||||
expect(globalThis.fetch).not.toHaveBeenCalled();
|
expect(spy).not.toHaveBeenCalled();
|
||||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||||
api.fetch("test");
|
api.fetch("test");
|
||||||
expect(globalThis.fetch).toHaveBeenCalled();
|
expect(spy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should update identity server base url", () => {
|
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();
|
expect(api.opts.idBaseUrl).toBeUndefined();
|
||||||
api.setIdBaseUrl("https://id.foo.bar");
|
api.setIdBaseUrl("https://id.foo.bar");
|
||||||
expect(api.opts.idBaseUrl).toBe("https://id.foo.bar");
|
expect(api.opts.idBaseUrl).toBe("https://id.foo.bar");
|
||||||
@@ -82,47 +83,68 @@ describe("FetchHttpApi", () => {
|
|||||||
|
|
||||||
describe("idServerRequest", () => {
|
describe("idServerRequest", () => {
|
||||||
it("should throw if no idBaseUrl", () => {
|
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(
|
expect(() => api.idServerRequest(Method.Get, "/test", {}, IdentityPrefix.V2)).toThrow(
|
||||||
"No identity server base URL set",
|
"No identity server base URL set",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should send params as query string for GET requests", () => {
|
it("should send params as query string for GET requests", () => {
|
||||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
const fetchFn = makeMockFetchFn();
|
||||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
|
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);
|
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] as URL).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.getAll("via")).toEqual(["a", "b"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should send params as body for non-GET requests", () => {
|
it("should send params as body for non-GET requests", () => {
|
||||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
const fetchFn = makeMockFetchFn();
|
||||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
|
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||||
|
baseUrl,
|
||||||
|
idBaseUrl,
|
||||||
|
prefix,
|
||||||
|
fetchFn,
|
||||||
|
onlyData: true,
|
||||||
|
});
|
||||||
const params = { foo: "bar", via: ["a", "b"] };
|
const params = { foo: "bar", via: ["a", "b"] };
|
||||||
api.idServerRequest(Method.Post, "/test", params, IdentityPrefix.V2);
|
api.idServerRequest(Method.Post, "/test", params, IdentityPrefix.V2);
|
||||||
expect(fetchFn.mock.calls[0][0].searchParams.get("foo")).not.toBe("bar");
|
expect((fetchFn.mock.calls[0][0] as URL).searchParams.get("foo")).not.toBe("bar");
|
||||||
expect(JSON.parse(fetchFn.mock.calls[0][1].body)).toStrictEqual(params);
|
expect(JSON.parse(fetchFn.mock.calls[0][1]!.body as string)).toStrictEqual(params);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should add Authorization header if token provided", () => {
|
it("should add Authorization header if token provided", () => {
|
||||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
const fetchFn = makeMockFetchFn();
|
||||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
|
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||||
|
baseUrl,
|
||||||
|
idBaseUrl,
|
||||||
|
prefix,
|
||||||
|
fetchFn,
|
||||||
|
onlyData: true,
|
||||||
|
});
|
||||||
api.idServerRequest(Method.Post, "/test", {}, IdentityPrefix.V2, "token");
|
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 () => {
|
it("should complain if constructed without `onlyData: true`", async () => {
|
||||||
const res = { ok: true };
|
expect(
|
||||||
const fetchFn = jest.fn().mockResolvedValue(res);
|
() =>
|
||||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: false });
|
new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||||
await expect(api.requestOtherUrl(Method.Get, "http://url")).resolves.toBe(res);
|
baseUrl,
|
||||||
|
prefix,
|
||||||
|
}),
|
||||||
|
).toThrow("Constructing FetchHttpApi without `onlyData=true` is no longer supported.");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should set an Accept header, and parse the response as JSON, by default", async () => {
|
it("should set an Accept header, and parse the response as JSON, by default", async () => {
|
||||||
const result = { a: 1 };
|
const result = { a: 1 };
|
||||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue(result) });
|
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 });
|
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
|
||||||
await expect(api.requestOtherUrl(Method.Get, "http://url")).resolves.toBe(result);
|
await expect(api.requestOtherUrl(Method.Get, "http://url")).resolves.toBe(result);
|
||||||
expect(fetchFn.mock.calls[0][1].headers.Accept).toBe("application/json");
|
expect(fetchFn.mock.calls[0][1].headers.Accept).toBe("application/json");
|
||||||
@@ -130,7 +152,7 @@ describe("FetchHttpApi", () => {
|
|||||||
|
|
||||||
it("should not set an Accept header, and should return text if json=false", async () => {
|
it("should not set an Accept header, and should return text if json=false", async () => {
|
||||||
const text = "418 I'm a teapot";
|
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 });
|
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
|
||||||
await expect(
|
await expect(
|
||||||
api.requestOtherUrl(Method.Get, "http://url", undefined, {
|
api.requestOtherUrl(Method.Get, "http://url", undefined, {
|
||||||
@@ -142,7 +164,7 @@ describe("FetchHttpApi", () => {
|
|||||||
|
|
||||||
it("should not set an Accept header, and should return a blob, if rawResponseBody is true", async () => {
|
it("should not set an Accept header, and should return a blob, if rawResponseBody is true", async () => {
|
||||||
const blob = new Blob(["blobby"]);
|
const blob = new Blob(["blobby"]);
|
||||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true, blob: jest.fn().mockResolvedValue(blob) });
|
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 });
|
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
|
||||||
await expect(
|
await expect(
|
||||||
api.requestOtherUrl(Method.Get, "http://url", undefined, {
|
api.requestOtherUrl(Method.Get, "http://url", undefined, {
|
||||||
@@ -156,7 +178,7 @@ describe("FetchHttpApi", () => {
|
|||||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
prefix,
|
prefix,
|
||||||
fetchFn: jest.fn(),
|
fetchFn: vi.fn(),
|
||||||
onlyData: true,
|
onlyData: true,
|
||||||
});
|
});
|
||||||
await expect(
|
await expect(
|
||||||
@@ -165,103 +187,109 @@ describe("FetchHttpApi", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should send token via query params if useAuthorizationHeader=false", async () => {
|
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>(), {
|
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
prefix,
|
prefix,
|
||||||
fetchFn,
|
fetchFn,
|
||||||
accessToken: "token",
|
accessToken: "token",
|
||||||
useAuthorizationHeader: false,
|
useAuthorizationHeader: false,
|
||||||
|
onlyData: true,
|
||||||
});
|
});
|
||||||
await api.authedRequest(Method.Get, "/path");
|
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 () => {
|
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>(), {
|
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
prefix,
|
prefix,
|
||||||
fetchFn,
|
fetchFn,
|
||||||
accessToken: "token",
|
accessToken: "token",
|
||||||
|
onlyData: true,
|
||||||
});
|
});
|
||||||
await api.authedRequest(Method.Get, "/path");
|
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`", () => {
|
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>(), {
|
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
prefix,
|
prefix,
|
||||||
fetchFn,
|
fetchFn,
|
||||||
accessToken: "token",
|
accessToken: "token",
|
||||||
|
onlyData: true,
|
||||||
});
|
});
|
||||||
api.request(Method.Get, "/path");
|
api.request(Method.Get, "/path");
|
||||||
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBeFalsy();
|
expect((fetchFn.mock.calls[0][0] as URL).searchParams.get("access_token")).toBeFalsy();
|
||||||
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).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 () => {
|
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>(), {
|
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
prefix,
|
prefix,
|
||||||
fetchFn,
|
fetchFn,
|
||||||
accessToken: "token",
|
accessToken: "token",
|
||||||
useAuthorizationHeader: true,
|
useAuthorizationHeader: true,
|
||||||
|
onlyData: true,
|
||||||
});
|
});
|
||||||
await api.authedRequest(Method.Get, "/path", { access_token: "123" });
|
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][0] as URL).searchParams.get("access_token")).toBeFalsy();
|
||||||
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 override manually specified access token via query params", async () => {
|
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>(), {
|
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
prefix,
|
prefix,
|
||||||
fetchFn,
|
fetchFn,
|
||||||
accessToken: "token",
|
accessToken: "token",
|
||||||
useAuthorizationHeader: false,
|
useAuthorizationHeader: false,
|
||||||
|
onlyData: true,
|
||||||
});
|
});
|
||||||
await api.authedRequest(Method.Get, "/path", { access_token: "RealToken" });
|
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 () => {
|
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>(), {
|
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
prefix,
|
prefix,
|
||||||
fetchFn,
|
fetchFn,
|
||||||
accessToken: "token",
|
accessToken: "token",
|
||||||
useAuthorizationHeader: true,
|
useAuthorizationHeader: true,
|
||||||
|
onlyData: true,
|
||||||
});
|
});
|
||||||
await api.authedRequest(Method.Get, "/path", undefined, undefined, {
|
await api.authedRequest(Method.Get, "/path", undefined, undefined, {
|
||||||
headers: { Authorization: "Bearer RealToken" },
|
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 () => {
|
it("should not override Accept header", async () => {
|
||||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
const fetchFn = makeMockFetchFn();
|
||||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
|
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
|
||||||
await api.authedRequest(Method.Get, "/path", undefined, undefined, {
|
await api.authedRequest(Method.Get, "/path", undefined, undefined, {
|
||||||
headers: { Accept: "text/html" },
|
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 () => {
|
it("should emit NoConsent when given errcode=M_CONTENT_NOT_GIVEN", async () => {
|
||||||
const fetchFn = jest.fn().mockResolvedValue({
|
const fetchFn = vi.fn().mockResolvedValue({
|
||||||
ok: false,
|
ok: false,
|
||||||
headers: {
|
headers: {
|
||||||
get(name: string): string | null {
|
get(name: string): string | null {
|
||||||
return name === "Content-Type" ? "application/json" : null;
|
return name === "Content-Type" ? "application/json" : null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
text: jest.fn().mockResolvedValue(
|
text: vi.fn().mockResolvedValue(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
errcode: "M_CONSENT_NOT_GIVEN",
|
errcode: "M_CONSENT_NOT_GIVEN",
|
||||||
error: "Ye shall ask for consent",
|
error: "Ye shall ask for consent",
|
||||||
@@ -269,7 +297,7 @@ describe("FetchHttpApi", () => {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
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([
|
await Promise.all([
|
||||||
emitPromise(emitter, HttpApiEvent.NoConsent),
|
emitPromise(emitter, HttpApiEvent.NoConsent),
|
||||||
@@ -279,11 +307,11 @@ describe("FetchHttpApi", () => {
|
|||||||
|
|
||||||
describe("authedRequest", () => {
|
describe("authedRequest", () => {
|
||||||
it("should not include token if unset", async () => {
|
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 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");
|
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", () => {
|
describe("with refresh token", () => {
|
||||||
@@ -296,7 +324,13 @@ describe("FetchHttpApi", () => {
|
|||||||
error: "Token is not active",
|
error: "Token is not active",
|
||||||
soft_logout: false,
|
soft_logout: false,
|
||||||
};
|
};
|
||||||
const unknownTokenErr = new MatrixError(unknownTokenErrBody, 401);
|
const unknownTokenErr = new MatrixError(
|
||||||
|
unknownTokenErrBody,
|
||||||
|
401,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
const unknownTokenResponse = {
|
const unknownTokenResponse = {
|
||||||
ok: false,
|
ok: false,
|
||||||
status: 401,
|
status: 401,
|
||||||
@@ -305,19 +339,27 @@ describe("FetchHttpApi", () => {
|
|||||||
return name === "Content-Type" ? "application/json" : null;
|
return name === "Content-Type" ? "application/json" : null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
text: jest.fn().mockResolvedValue(JSON.stringify(unknownTokenErrBody)),
|
text: vi.fn().mockResolvedValue(JSON.stringify(unknownTokenErrBody)),
|
||||||
};
|
};
|
||||||
const okayResponse = {
|
const okayResponse = {
|
||||||
ok: true,
|
ok: true,
|
||||||
status: 200,
|
status: 200,
|
||||||
|
json: vi.fn().mockResolvedValue({ x: 1 }),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("without a tokenRefreshFunction", () => {
|
describe("without a tokenRefreshFunction", () => {
|
||||||
it("should emit logout and throw", async () => {
|
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>();
|
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||||
jest.spyOn(emitter, "emit");
|
vi.spyOn(emitter, "emit");
|
||||||
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn, accessToken, refreshToken });
|
const api = new FetchHttpApi(emitter, {
|
||||||
|
baseUrl,
|
||||||
|
prefix,
|
||||||
|
fetchFn,
|
||||||
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
|
onlyData: true,
|
||||||
|
});
|
||||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
|
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
|
||||||
unknownTokenErr,
|
unknownTokenErr,
|
||||||
);
|
);
|
||||||
@@ -328,10 +370,10 @@ describe("FetchHttpApi", () => {
|
|||||||
describe("with a tokenRefreshFunction", () => {
|
describe("with a tokenRefreshFunction", () => {
|
||||||
it("should emit logout and throw when token refresh fails", async () => {
|
it("should emit logout and throw when token refresh fails", async () => {
|
||||||
const error = new MatrixError();
|
const error = new MatrixError();
|
||||||
const tokenRefreshFunction = jest.fn().mockRejectedValue(error);
|
const tokenRefreshFunction = vi.fn().mockRejectedValue(error);
|
||||||
const fetchFn = jest.fn().mockResolvedValue(unknownTokenResponse);
|
const fetchFn = vi.fn().mockResolvedValue(unknownTokenResponse);
|
||||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||||
jest.spyOn(emitter, "emit");
|
vi.spyOn(emitter, "emit");
|
||||||
const api = new FetchHttpApi(emitter, {
|
const api = new FetchHttpApi(emitter, {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
prefix,
|
prefix,
|
||||||
@@ -339,6 +381,7 @@ describe("FetchHttpApi", () => {
|
|||||||
tokenRefreshFunction,
|
tokenRefreshFunction,
|
||||||
accessToken,
|
accessToken,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
|
onlyData: true,
|
||||||
});
|
});
|
||||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
|
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
|
||||||
unknownTokenErr,
|
unknownTokenErr,
|
||||||
@@ -349,10 +392,10 @@ describe("FetchHttpApi", () => {
|
|||||||
|
|
||||||
it("should not emit logout but still throw when token refresh fails due to transitive fault", async () => {
|
it("should not emit logout but still throw when token refresh fails due to transitive fault", async () => {
|
||||||
const error = new ConnectionError("transitive fault");
|
const error = new ConnectionError("transitive fault");
|
||||||
const tokenRefreshFunction = jest.fn().mockRejectedValue(error);
|
const tokenRefreshFunction = vi.fn().mockRejectedValue(error);
|
||||||
const fetchFn = jest.fn().mockResolvedValue(unknownTokenResponse);
|
const fetchFn = vi.fn().mockResolvedValue(unknownTokenResponse);
|
||||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||||
jest.spyOn(emitter, "emit");
|
vi.spyOn(emitter, "emit");
|
||||||
const api = new FetchHttpApi(emitter, {
|
const api = new FetchHttpApi(emitter, {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
prefix,
|
prefix,
|
||||||
@@ -360,9 +403,10 @@ describe("FetchHttpApi", () => {
|
|||||||
tokenRefreshFunction,
|
tokenRefreshFunction,
|
||||||
accessToken,
|
accessToken,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
|
onlyData: true,
|
||||||
});
|
});
|
||||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
|
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
|
||||||
unknownTokenErr,
|
new TokenRefreshError(unknownTokenErr),
|
||||||
);
|
);
|
||||||
expect(tokenRefreshFunction).toHaveBeenCalledWith(refreshToken);
|
expect(tokenRefreshFunction).toHaveBeenCalledWith(refreshToken);
|
||||||
expect(emitter.emit).not.toHaveBeenCalledWith(HttpApiEvent.SessionLoggedOut, unknownTokenErr);
|
expect(emitter.emit).not.toHaveBeenCalledWith(HttpApiEvent.SessionLoggedOut, unknownTokenErr);
|
||||||
@@ -371,16 +415,16 @@ describe("FetchHttpApi", () => {
|
|||||||
it("should refresh token and retry request", async () => {
|
it("should refresh token and retry request", async () => {
|
||||||
const newAccessToken = "new-access-token";
|
const newAccessToken = "new-access-token";
|
||||||
const newRefreshToken = "new-refresh-token";
|
const newRefreshToken = "new-refresh-token";
|
||||||
const tokenRefreshFunction = jest.fn().mockResolvedValue({
|
const tokenRefreshFunction = vi.fn().mockResolvedValue({
|
||||||
accessToken: newAccessToken,
|
accessToken: newAccessToken,
|
||||||
refreshToken: newRefreshToken,
|
refreshToken: newRefreshToken,
|
||||||
});
|
});
|
||||||
const fetchFn = jest
|
const fetchFn = vi
|
||||||
.fn()
|
.fn()
|
||||||
.mockResolvedValueOnce(unknownTokenResponse)
|
.mockResolvedValueOnce(unknownTokenResponse)
|
||||||
.mockResolvedValueOnce(okayResponse);
|
.mockResolvedValueOnce(okayResponse);
|
||||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||||
jest.spyOn(emitter, "emit");
|
vi.spyOn(emitter, "emit");
|
||||||
const api = new FetchHttpApi(emitter, {
|
const api = new FetchHttpApi(emitter, {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
prefix,
|
prefix,
|
||||||
@@ -388,11 +432,12 @@ describe("FetchHttpApi", () => {
|
|||||||
tokenRefreshFunction,
|
tokenRefreshFunction,
|
||||||
accessToken,
|
accessToken,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
|
onlyData: true,
|
||||||
});
|
});
|
||||||
const result = await api.authedRequest(Method.Post, "/account/password", undefined, undefined, {
|
const result = await api.authedRequest(Method.Post, "/account/password", undefined, undefined, {
|
||||||
headers: {},
|
headers: {},
|
||||||
});
|
});
|
||||||
expect(result).toEqual(okayResponse);
|
expect(result).toEqual({ x: 1 });
|
||||||
expect(tokenRefreshFunction).toHaveBeenCalledWith(refreshToken);
|
expect(tokenRefreshFunction).toHaveBeenCalledWith(refreshToken);
|
||||||
|
|
||||||
expect(fetchFn).toHaveBeenCalledTimes(2);
|
expect(fetchFn).toHaveBeenCalledTimes(2);
|
||||||
@@ -411,7 +456,7 @@ describe("FetchHttpApi", () => {
|
|||||||
// count because it's only to get a token with an expiry)
|
// count because it's only to get a token with an expiry)
|
||||||
const newAccessToken = "new-access-token";
|
const newAccessToken = "new-access-token";
|
||||||
const newRefreshToken = "new-refresh-token";
|
const newRefreshToken = "new-refresh-token";
|
||||||
const tokenRefreshFunction = jest.fn().mockReturnValue({
|
const tokenRefreshFunction = vi.fn().mockReturnValue({
|
||||||
accessToken: newAccessToken,
|
accessToken: newAccessToken,
|
||||||
refreshToken: newRefreshToken,
|
refreshToken: newRefreshToken,
|
||||||
// This needs to be sufficiently high that it's over the threshold for
|
// This needs to be sufficiently high that it's over the threshold for
|
||||||
@@ -420,10 +465,10 @@ describe("FetchHttpApi", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// fetch doesn't like our new or old tokens
|
// 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>();
|
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||||
jest.spyOn(emitter, "emit");
|
vi.spyOn(emitter, "emit");
|
||||||
const api = new FetchHttpApi(emitter, {
|
const api = new FetchHttpApi(emitter, {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
prefix,
|
prefix,
|
||||||
@@ -431,8 +476,9 @@ describe("FetchHttpApi", () => {
|
|||||||
tokenRefreshFunction,
|
tokenRefreshFunction,
|
||||||
accessToken,
|
accessToken,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
|
onlyData: true,
|
||||||
});
|
});
|
||||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
|
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrowError(
|
||||||
unknownTokenErr,
|
unknownTokenErr,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -454,7 +500,7 @@ describe("FetchHttpApi", () => {
|
|||||||
|
|
||||||
// first refresh is to get a token with an expiry at all, because we
|
// first refresh is to get a token with an expiry at all, because we
|
||||||
// can't specify an expiry on the token we inject
|
// can't specify an expiry on the token we inject
|
||||||
const tokenRefreshFunction = jest.fn().mockResolvedValueOnce({
|
const tokenRefreshFunction = vi.fn().mockResolvedValueOnce({
|
||||||
accessToken: newAccessToken,
|
accessToken: newAccessToken,
|
||||||
refreshToken: newRefreshToken,
|
refreshToken: newRefreshToken,
|
||||||
expiry: new Date(Date.now() + 1000),
|
expiry: new Date(Date.now() + 1000),
|
||||||
@@ -475,10 +521,10 @@ describe("FetchHttpApi", () => {
|
|||||||
expiry: new Date(Date.now() + 5 * 60 * 1000),
|
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>();
|
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||||
jest.spyOn(emitter, "emit");
|
vi.spyOn(emitter, "emit");
|
||||||
const api = new FetchHttpApi(emitter, {
|
const api = new FetchHttpApi(emitter, {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
prefix,
|
prefix,
|
||||||
@@ -486,8 +532,9 @@ describe("FetchHttpApi", () => {
|
|||||||
tokenRefreshFunction,
|
tokenRefreshFunction,
|
||||||
accessToken,
|
accessToken,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
|
onlyData: true,
|
||||||
});
|
});
|
||||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
|
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrowError(
|
||||||
unknownTokenErr,
|
unknownTokenErr,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -504,9 +551,9 @@ describe("FetchHttpApi", () => {
|
|||||||
const localBaseUrl = "http://baseurl";
|
const localBaseUrl = "http://baseurl";
|
||||||
const baseUrlWithTrailingSlash = "http://baseurl/";
|
const baseUrlWithTrailingSlash = "http://baseurl/";
|
||||||
const makeApi = (thisBaseUrl = baseUrl): FetchHttpApi<any> => {
|
const makeApi = (thisBaseUrl = baseUrl): FetchHttpApi<any> => {
|
||||||
const fetchFn = jest.fn();
|
const fetchFn = vi.fn();
|
||||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
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 = {
|
type TestParams = {
|
||||||
@@ -557,9 +604,15 @@ describe("FetchHttpApi", () => {
|
|||||||
|
|
||||||
describe("extraParams handling", () => {
|
describe("extraParams handling", () => {
|
||||||
const makeApiWithExtraParams = (extraParams: QueryDict): FetchHttpApi<any> => {
|
const makeApiWithExtraParams = (extraParams: QueryDict): FetchHttpApi<any> => {
|
||||||
const fetchFn = jest.fn();
|
const fetchFn = vi.fn();
|
||||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
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";
|
const userId = "@rsb-tbg:localhost";
|
||||||
@@ -610,9 +663,9 @@ describe("FetchHttpApi", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should work when extraParams is undefined", () => {
|
it("should work when extraParams is undefined", () => {
|
||||||
const fetchFn = jest.fn();
|
const fetchFn = vi.fn();
|
||||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
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 queryParams = { userId: "123" };
|
||||||
const result = api.getUrl("/test", queryParams);
|
const result = api.getUrl("/test", queryParams);
|
||||||
@@ -634,21 +687,22 @@ describe("FetchHttpApi", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should not log query parameters", async () => {
|
it("should not log query parameters", async () => {
|
||||||
jest.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
const responseResolvers = Promise.withResolvers<Response>();
|
const responseResolvers = Promise.withResolvers<Response>();
|
||||||
const fetchFn = jest.fn().mockReturnValue(responseResolvers.promise);
|
const fetchFn = vi.fn().mockReturnValue(responseResolvers.promise);
|
||||||
const mockLogger = {
|
const mockLogger = {
|
||||||
debug: jest.fn(),
|
debug: vi.fn(),
|
||||||
} as unknown as Mocked<Logger>;
|
} as unknown as Mocked<Logger>;
|
||||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
prefix,
|
prefix,
|
||||||
fetchFn,
|
fetchFn,
|
||||||
logger: mockLogger,
|
logger: mockLogger,
|
||||||
|
onlyData: true,
|
||||||
});
|
});
|
||||||
const prom = api.requestOtherUrl(Method.Get, "https://server:8448/some/path?query=param#fragment");
|
const prom = api.requestOtherUrl(Method.Get, "https://server:8448/some/path?query=param#fragment");
|
||||||
jest.advanceTimersByTime(1234);
|
vi.advanceTimersByTime(1234);
|
||||||
responseResolvers.resolve({ ok: true, status: 200, text: () => Promise.resolve("RESPONSE") } as Response);
|
responseResolvers.resolve({ ok: true, status: 200, json: () => Promise.resolve("RESPONSE") } as Response);
|
||||||
await prom;
|
await prom;
|
||||||
expect(mockLogger.debug).not.toHaveBeenCalledWith("fragment");
|
expect(mockLogger.debug).not.toHaveBeenCalledWith("fragment");
|
||||||
expect(mockLogger.debug).not.toHaveBeenCalledWith("query");
|
expect(mockLogger.debug).not.toHaveBeenCalledWith("query");
|
||||||
@@ -668,7 +722,7 @@ describe("FetchHttpApi", () => {
|
|||||||
|
|
||||||
it("should not make multiple concurrent refresh token requests", async () => {
|
it("should not make multiple concurrent refresh token requests", async () => {
|
||||||
const deferredTokenRefresh = Promise.withResolvers<{ accessToken: string; refreshToken: string }>();
|
const deferredTokenRefresh = Promise.withResolvers<{ accessToken: string; refreshToken: string }>();
|
||||||
const fetchFn = jest.fn().mockResolvedValue({
|
const fetchFn = vi.fn().mockResolvedValue({
|
||||||
ok: false,
|
ok: false,
|
||||||
status: tokenInactiveError.httpStatus,
|
status: tokenInactiveError.httpStatus,
|
||||||
async text() {
|
async text() {
|
||||||
@@ -678,10 +732,10 @@ describe("FetchHttpApi", () => {
|
|||||||
return tokenInactiveError.data;
|
return tokenInactiveError.data;
|
||||||
},
|
},
|
||||||
headers: {
|
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>(), {
|
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
@@ -691,6 +745,7 @@ describe("FetchHttpApi", () => {
|
|||||||
tokenRefreshFunction,
|
tokenRefreshFunction,
|
||||||
accessToken: "ACCESS_TOKEN",
|
accessToken: "ACCESS_TOKEN",
|
||||||
refreshToken: "REFRESH_TOKEN",
|
refreshToken: "REFRESH_TOKEN",
|
||||||
|
onlyData: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const prom1 = api.authedRequest(Method.Get, "/path1");
|
const prom1 = api.authedRequest(Method.Get, "/path1");
|
||||||
@@ -708,7 +763,7 @@ describe("FetchHttpApi", () => {
|
|||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
headers: {
|
headers: {
|
||||||
get: jest.fn().mockReturnValue("application/json"),
|
get: vi.fn().mockReturnValue("application/json"),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
deferredTokenRefresh.resolve({ accessToken: "NEW_ACCESS_TOKEN", refreshToken: "NEW_REFRESH_TOKEN" });
|
deferredTokenRefresh.resolve({ accessToken: "NEW_ACCESS_TOKEN", refreshToken: "NEW_REFRESH_TOKEN" });
|
||||||
@@ -723,7 +778,7 @@ describe("FetchHttpApi", () => {
|
|||||||
|
|
||||||
it("should use newly refreshed token if request starts mid-refresh", async () => {
|
it("should use newly refreshed token if request starts mid-refresh", async () => {
|
||||||
const deferredTokenRefresh = Promise.withResolvers<{ accessToken: string; refreshToken: string }>();
|
const deferredTokenRefresh = Promise.withResolvers<{ accessToken: string; refreshToken: string }>();
|
||||||
const fetchFn = jest.fn().mockResolvedValue({
|
const fetchFn = vi.fn().mockResolvedValue({
|
||||||
ok: false,
|
ok: false,
|
||||||
status: tokenInactiveError.httpStatus,
|
status: tokenInactiveError.httpStatus,
|
||||||
async text() {
|
async text() {
|
||||||
@@ -733,10 +788,10 @@ describe("FetchHttpApi", () => {
|
|||||||
return tokenInactiveError.data;
|
return tokenInactiveError.data;
|
||||||
},
|
},
|
||||||
headers: {
|
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>(), {
|
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
@@ -746,6 +801,7 @@ describe("FetchHttpApi", () => {
|
|||||||
tokenRefreshFunction,
|
tokenRefreshFunction,
|
||||||
accessToken: "ACCESS_TOKEN",
|
accessToken: "ACCESS_TOKEN",
|
||||||
refreshToken: "REFRESH_TOKEN",
|
refreshToken: "REFRESH_TOKEN",
|
||||||
|
onlyData: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const prom1 = api.authedRequest(Method.Get, "/path1");
|
const prom1 = api.authedRequest(Method.Get, "/path1");
|
||||||
@@ -765,7 +821,7 @@ describe("FetchHttpApi", () => {
|
|||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
headers: {
|
headers: {
|
||||||
get: jest.fn().mockReturnValue("application/json"),
|
get: vi.fn().mockReturnValue("application/json"),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -783,3 +839,7 @@ describe("FetchHttpApi", () => {
|
|||||||
expect(api.opts.refreshToken).toBe("NEW_REFRESH_TOKEN");
|
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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { mocked } from "jest-mock";
|
|
||||||
|
|
||||||
import { ClientPrefix, MatrixHttpApi, Method, type UploadResponse } from "../../../src";
|
import { ClientPrefix, MatrixHttpApi, Method, type UploadResponse } from "../../../src";
|
||||||
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
|
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
|
||||||
|
|
||||||
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
|
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
|
||||||
|
|
||||||
jest.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
|
|
||||||
describe("MatrixHttpApi", () => {
|
describe("MatrixHttpApi", () => {
|
||||||
const baseUrl = "http://baseUrl";
|
const baseUrl = "http://baseUrl";
|
||||||
const prefix = ClientPrefix.V3;
|
const prefix = ClientPrefix.V3;
|
||||||
|
|
||||||
let xhr: Writeable<XMLHttpRequest>;
|
|
||||||
let upload: Promise<UploadResponse>;
|
let upload: Promise<UploadResponse>;
|
||||||
|
|
||||||
const DONE = 0;
|
const DONE = 0;
|
||||||
|
|
||||||
|
function getRequest(): Writeable<XMLHttpRequest> | undefined {
|
||||||
|
return vi.mocked(globalThis.XMLHttpRequest)?.mock.instances.at(-1);
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
xhr = {
|
// We stub out XHR here as it is not available in the test environment
|
||||||
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
|
|
||||||
// @ts-ignore
|
// @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
|
// @ts-ignore
|
||||||
globalThis.XMLHttpRequest.DONE = DONE;
|
globalThis.XMLHttpRequest.DONE = DONE;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
upload?.catch(() => {});
|
upload?.catch(() => {});
|
||||||
// Abort any remaining requests
|
const xhr = getRequest();
|
||||||
xhr.readyState = DONE;
|
if (xhr) {
|
||||||
xhr.status = 0;
|
// Abort any remaining requests
|
||||||
// @ts-ignore
|
xhr.readyState = DONE;
|
||||||
xhr.onreadystatechange?.(new Event("test"));
|
xhr.status = 0;
|
||||||
|
// @ts-ignore
|
||||||
|
xhr.onreadystatechange?.(new Event("test"));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should fall back to `fetch` where xhr is unavailable", async () => {
|
it("should fall back to `fetch` where xhr is unavailable", async () => {
|
||||||
globalThis.XMLHttpRequest = undefined!;
|
globalThis.XMLHttpRequest = undefined!;
|
||||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({}) });
|
const fetchFn = vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue({}) });
|
||||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
|
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
|
||||||
upload = api.uploadContent({} as File);
|
upload = api.uploadContent({} as File);
|
||||||
await upload;
|
await upload;
|
||||||
expect(fetchFn).toHaveBeenCalled();
|
expect(fetchFn).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should prefer xhr where available", () => {
|
it("should prefer xhr where available", () => {
|
||||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
const fetchFn = vi.fn().mockResolvedValue({ ok: true });
|
||||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
|
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
|
||||||
upload = api.uploadContent({} as File);
|
upload = api.uploadContent({} as File);
|
||||||
expect(fetchFn).not.toHaveBeenCalled();
|
expect(fetchFn).not.toHaveBeenCalled();
|
||||||
expect(xhr.open).toHaveBeenCalled();
|
expect(getRequest()!.open).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should send access token in query params if header disabled", () => {
|
it("should send access token in query params if header disabled", () => {
|
||||||
@@ -82,13 +87,14 @@ describe("MatrixHttpApi", () => {
|
|||||||
prefix,
|
prefix,
|
||||||
accessToken: "token",
|
accessToken: "token",
|
||||||
useAuthorizationHeader: false,
|
useAuthorizationHeader: false,
|
||||||
|
onlyData: true,
|
||||||
});
|
});
|
||||||
upload = api.uploadContent({} as File);
|
upload = api.uploadContent({} as File);
|
||||||
expect(xhr.open).toHaveBeenCalledWith(
|
expect(getRequest()!.open).toHaveBeenCalledWith(
|
||||||
Method.Post,
|
Method.Post,
|
||||||
baseUrl.toLowerCase() + "/_matrix/media/v3/upload?access_token=token",
|
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", () => {
|
it("should send access token in header by default", () => {
|
||||||
@@ -96,131 +102,138 @@ describe("MatrixHttpApi", () => {
|
|||||||
baseUrl,
|
baseUrl,
|
||||||
prefix,
|
prefix,
|
||||||
accessToken: "token",
|
accessToken: "token",
|
||||||
|
onlyData: true,
|
||||||
});
|
});
|
||||||
upload = api.uploadContent({} as File);
|
upload = api.uploadContent({} as File);
|
||||||
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/v3/upload");
|
expect(getRequest()!.open).toHaveBeenCalledWith(
|
||||||
expect(xhr.setRequestHeader).toHaveBeenCalledWith("Authorization", "Bearer token");
|
Method.Post,
|
||||||
|
baseUrl.toLowerCase() + "/_matrix/media/v3/upload",
|
||||||
|
);
|
||||||
|
expect(getRequest()!.setRequestHeader).toHaveBeenCalledWith("Authorization", "Bearer token");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should include filename by default", () => {
|
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" });
|
upload = api.uploadContent({} as File, { name: "name" });
|
||||||
expect(xhr.open).toHaveBeenCalledWith(
|
expect(getRequest()!.open).toHaveBeenCalledWith(
|
||||||
Method.Post,
|
Method.Post,
|
||||||
baseUrl.toLowerCase() + "/_matrix/media/v3/upload?filename=name",
|
baseUrl.toLowerCase() + "/_matrix/media/v3/upload?filename=name",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should allow not sending the filename", () => {
|
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 });
|
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", () => {
|
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);
|
upload = api.uploadContent({} as File);
|
||||||
api.cancelUpload(upload);
|
api.cancelUpload(upload);
|
||||||
expect(xhr.abort).toHaveBeenCalled();
|
expect(getRequest()!.abort).toHaveBeenCalled();
|
||||||
return expect(upload).rejects.toThrow("Aborted");
|
return expect(upload).rejects.toThrow("Aborted");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should timeout if no progress in 30s", () => {
|
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);
|
upload = api.uploadContent({} as File);
|
||||||
jest.advanceTimersByTime(25000);
|
vi.advanceTimersByTime(25000);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
xhr.upload.onprogress(new Event("progress", { loaded: 1, total: 100 }));
|
getRequest()!.upload.onprogress(new Event("progress", { loaded: 1, total: 100 }));
|
||||||
jest.advanceTimersByTime(25000);
|
vi.advanceTimersByTime(25000);
|
||||||
expect(xhr.abort).not.toHaveBeenCalled();
|
expect(getRequest()!.abort).not.toHaveBeenCalled();
|
||||||
jest.advanceTimersByTime(5000);
|
vi.advanceTimersByTime(5000);
|
||||||
expect(xhr.abort).toHaveBeenCalled();
|
expect(getRequest()!.abort).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call progressHandler", () => {
|
it("should call progressHandler", () => {
|
||||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||||
const progressHandler = jest.fn();
|
const progressHandler = vi.fn();
|
||||||
upload = api.uploadContent({} as File, { progressHandler });
|
upload = api.uploadContent({} as File, { progressHandler });
|
||||||
const progressEvent = new Event("progress") as ProgressEvent;
|
const progressEvent = new Event("progress") as ProgressEvent;
|
||||||
Object.assign(progressEvent, { loaded: 1, total: 100 });
|
Object.assign(progressEvent, { loaded: 1, total: 100 });
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
xhr.upload.onprogress(progressEvent);
|
getRequest()!.upload.onprogress(progressEvent);
|
||||||
expect(progressHandler).toHaveBeenCalledWith({ loaded: 1, total: 100 });
|
expect(progressHandler).toHaveBeenCalledWith({ loaded: 1, total: 100 });
|
||||||
|
|
||||||
Object.assign(progressEvent, { loaded: 95, total: 100 });
|
Object.assign(progressEvent, { loaded: 95, total: 100 });
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
xhr.upload.onprogress(progressEvent);
|
getRequest()!.upload.onprogress(progressEvent);
|
||||||
expect(progressHandler).toHaveBeenCalledWith({ loaded: 95, total: 100 });
|
expect(progressHandler).toHaveBeenCalledWith({ loaded: 95, total: 100 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should error when no response body", () => {
|
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);
|
upload = api.uploadContent({} as File);
|
||||||
|
|
||||||
xhr.readyState = DONE;
|
getRequest()!.readyState = DONE;
|
||||||
xhr.responseText = "";
|
getRequest()!.responseText = "";
|
||||||
xhr.status = 200;
|
getRequest()!.status = 200;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
xhr.onreadystatechange?.(new Event("test"));
|
getRequest()!.onreadystatechange?.(new Event("test"));
|
||||||
|
|
||||||
return expect(upload).rejects.toThrow("No response body.");
|
return expect(upload).rejects.toThrow("No response body.");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should error on a 400-code", () => {
|
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);
|
upload = api.uploadContent({} as File);
|
||||||
|
|
||||||
xhr.readyState = DONE;
|
getRequest()!.readyState = DONE;
|
||||||
xhr.responseText = '{"errcode": "M_NOT_FOUND", "error": "Not found"}';
|
getRequest()!.responseText = '{"errcode": "M_NOT_FOUND", "error": "Not found"}';
|
||||||
xhr.status = 404;
|
getRequest()!.status = 404;
|
||||||
mocked(xhr.getResponseHeader).mockImplementation((name) =>
|
vi.mocked(getRequest()!.getResponseHeader).mockImplementation((name) =>
|
||||||
name.toLowerCase() === "content-type" ? "application/json" : null,
|
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
|
// @ts-ignore
|
||||||
xhr.onreadystatechange?.(new Event("test"));
|
getRequest()!.onreadystatechange?.(new Event("test"));
|
||||||
|
|
||||||
return expect(upload).rejects.toThrow("Not found");
|
return expect(upload).rejects.toThrow("Not found");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return response on successful upload", () => {
|
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);
|
upload = api.uploadContent({} as File);
|
||||||
|
|
||||||
xhr.readyState = DONE;
|
getRequest()!.readyState = DONE;
|
||||||
xhr.responseText = '{"content_uri": "mxc://server/foobar"}';
|
getRequest()!.responseText = '{"content_uri": "mxc://server/foobar"}';
|
||||||
xhr.status = 200;
|
getRequest()!.status = 200;
|
||||||
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
|
vi.mocked(getRequest()!.getResponseHeader).mockReturnValue("application/json");
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
xhr.onreadystatechange?.(new Event("test"));
|
getRequest()!.onreadystatechange?.(new Event("test"));
|
||||||
|
|
||||||
return expect(upload).resolves.toStrictEqual({ content_uri: "mxc://server/foobar" });
|
return expect(upload).resolves.toStrictEqual({ content_uri: "mxc://server/foobar" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should abort xhr when calling `cancelUpload`", () => {
|
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);
|
upload = api.uploadContent({} as File);
|
||||||
expect(api.cancelUpload(upload)).toBeTruthy();
|
expect(api.cancelUpload(upload)).toBeTruthy();
|
||||||
expect(xhr.abort).toHaveBeenCalled();
|
expect(getRequest()!.abort).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return false when `cancelUpload` is called but unsuccessful", async () => {
|
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);
|
upload = api.uploadContent({} as File);
|
||||||
|
|
||||||
xhr.readyState = DONE;
|
getRequest()!.readyState = DONE;
|
||||||
xhr.status = 500;
|
getRequest()!.status = 500;
|
||||||
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
|
vi.mocked(getRequest()!.getResponseHeader).mockReturnValue("application/json");
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
xhr.onreadystatechange?.(new Event("test"));
|
getRequest()!.onreadystatechange?.(new Event("test"));
|
||||||
await upload.catch(() => {});
|
await upload.catch(() => {});
|
||||||
|
|
||||||
expect(api.cancelUpload(upload)).toBeFalsy();
|
expect(api.cancelUpload(upload)).toBeFalsy();
|
||||||
expect(xhr.abort).not.toHaveBeenCalled();
|
expect(getRequest()!.abort).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return active uploads in `getCurrentUploads`", () => {
|
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);
|
upload = api.uploadContent({} as File);
|
||||||
expect(api.getCurrentUploads().find((u) => u.promise === upload)).toBeTruthy();
|
expect(api.getCurrentUploads().find((u) => u.promise === upload)).toBeTruthy();
|
||||||
api.cancelUpload(upload);
|
api.cancelUpload(upload);
|
||||||
@@ -228,7 +241,12 @@ describe("MatrixHttpApi", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return expected object from `getContentUri`", () => {
|
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();
|
expect(api.getContentUri()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,51 +14,51 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { mocked } from "jest-mock";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
anySignal,
|
anySignal,
|
||||||
ConnectionError,
|
ConnectionError,
|
||||||
HTTPError,
|
HTTPError,
|
||||||
MatrixError,
|
MatrixError,
|
||||||
|
MatrixSafetyError,
|
||||||
|
MatrixSafetyErrorCode,
|
||||||
parseErrorResponse,
|
parseErrorResponse,
|
||||||
retryNetworkOperation,
|
retryNetworkOperation,
|
||||||
timeoutSignal,
|
timeoutSignal,
|
||||||
} from "../../../src";
|
} from "../../../src";
|
||||||
import { sleep } from "../../../src/utils";
|
import { sleep } from "../../../src/utils";
|
||||||
|
|
||||||
jest.mock("../../../src/utils");
|
vi.mock("../../../src/utils");
|
||||||
// setupTests mocks `timeoutSignal` due to hanging timers
|
// setupTests mocks `timeoutSignal` due to hanging timers
|
||||||
jest.unmock("../../../src/http-api/utils");
|
vi.unmock("../../../src/http-api/utils");
|
||||||
|
|
||||||
describe("timeoutSignal", () => {
|
describe("timeoutSignal", () => {
|
||||||
jest.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
|
|
||||||
it("should fire abort signal after specified timeout", () => {
|
it("should fire abort signal after specified timeout", () => {
|
||||||
const signal = timeoutSignal(3000);
|
const signal = timeoutSignal(3000);
|
||||||
const onabort = jest.fn();
|
const onabort = vi.fn();
|
||||||
signal.onabort = onabort;
|
signal.onabort = onabort;
|
||||||
expect(signal.aborted).toBeFalsy();
|
expect(signal.aborted).toBeFalsy();
|
||||||
expect(onabort).not.toHaveBeenCalled();
|
expect(onabort).not.toHaveBeenCalled();
|
||||||
|
|
||||||
jest.advanceTimersByTime(3000);
|
vi.advanceTimersByTime(3000);
|
||||||
expect(signal.aborted).toBeTruthy();
|
expect(signal.aborted).toBeTruthy();
|
||||||
expect(onabort).toHaveBeenCalled();
|
expect(onabort).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("anySignal", () => {
|
describe("anySignal", () => {
|
||||||
jest.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
|
|
||||||
it("should fire when any signal fires", () => {
|
it("should fire when any signal fires", () => {
|
||||||
const { signal } = anySignal([timeoutSignal(3000), timeoutSignal(2000)]);
|
const { signal } = anySignal([timeoutSignal(3000), timeoutSignal(2000)]);
|
||||||
|
|
||||||
const onabort = jest.fn();
|
const onabort = vi.fn();
|
||||||
signal.onabort = onabort;
|
signal.onabort = onabort;
|
||||||
expect(signal.aborted).toBeFalsy();
|
expect(signal.aborted).toBeFalsy();
|
||||||
expect(onabort).not.toHaveBeenCalled();
|
expect(onabort).not.toHaveBeenCalled();
|
||||||
|
|
||||||
jest.advanceTimersByTime(2000);
|
vi.advanceTimersByTime(2000);
|
||||||
expect(signal.aborted).toBeTruthy();
|
expect(signal.aborted).toBeTruthy();
|
||||||
expect(onabort).toHaveBeenCalled();
|
expect(onabort).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -66,13 +66,13 @@ describe("anySignal", () => {
|
|||||||
it("should cleanup when instructed", () => {
|
it("should cleanup when instructed", () => {
|
||||||
const { signal, cleanup } = anySignal([timeoutSignal(3000), timeoutSignal(2000)]);
|
const { signal, cleanup } = anySignal([timeoutSignal(3000), timeoutSignal(2000)]);
|
||||||
|
|
||||||
const onabort = jest.fn();
|
const onabort = vi.fn();
|
||||||
signal.onabort = onabort;
|
signal.onabort = onabort;
|
||||||
expect(signal.aborted).toBeFalsy();
|
expect(signal.aborted).toBeFalsy();
|
||||||
expect(onabort).not.toHaveBeenCalled();
|
expect(onabort).not.toHaveBeenCalled();
|
||||||
|
|
||||||
cleanup();
|
cleanup();
|
||||||
jest.advanceTimersByTime(2000);
|
vi.advanceTimersByTime(2000);
|
||||||
expect(signal.aborted).toBeFalsy();
|
expect(signal.aborted).toBeFalsy();
|
||||||
expect(onabort).not.toHaveBeenCalled();
|
expect(onabort).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -86,9 +86,14 @@ describe("anySignal", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("parseErrorResponse", () => {
|
describe("parseErrorResponse", () => {
|
||||||
|
const url = "https://example.org";
|
||||||
|
|
||||||
let headers: Headers;
|
let headers: Headers;
|
||||||
const xhrHeaderMethods = {
|
const xhrHeaderMethods = {
|
||||||
getResponseHeader: (name: string) => headers.get(name),
|
responseURL: url,
|
||||||
|
getResponseHeader: (name: string) => {
|
||||||
|
headers.get(name);
|
||||||
|
},
|
||||||
getAllResponseHeaders: () => {
|
getAllResponseHeaders: () => {
|
||||||
let allHeaders = "";
|
let allHeaders = "";
|
||||||
headers.forEach((value, key) => {
|
headers.forEach((value, key) => {
|
||||||
@@ -118,6 +123,9 @@ describe("parseErrorResponse", () => {
|
|||||||
errcode: "TEST",
|
errcode: "TEST",
|
||||||
},
|
},
|
||||||
500,
|
500,
|
||||||
|
url,
|
||||||
|
undefined,
|
||||||
|
expect.any(Headers),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -127,6 +135,7 @@ describe("parseErrorResponse", () => {
|
|||||||
expect(
|
expect(
|
||||||
parseErrorResponse(
|
parseErrorResponse(
|
||||||
{
|
{
|
||||||
|
url,
|
||||||
headers,
|
headers,
|
||||||
status: 500,
|
status: 500,
|
||||||
} as Response,
|
} as Response,
|
||||||
@@ -138,6 +147,9 @@ describe("parseErrorResponse", () => {
|
|||||||
errcode: "TEST",
|
errcode: "TEST",
|
||||||
},
|
},
|
||||||
500,
|
500,
|
||||||
|
url,
|
||||||
|
undefined,
|
||||||
|
expect.any(Headers),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -147,8 +159,8 @@ describe("parseErrorResponse", () => {
|
|||||||
expect(
|
expect(
|
||||||
parseErrorResponse(
|
parseErrorResponse(
|
||||||
{
|
{
|
||||||
responseURL: "https://example.com",
|
|
||||||
...xhrHeaderMethods,
|
...xhrHeaderMethods,
|
||||||
|
responseURL: "https://example.com",
|
||||||
status: 500,
|
status: 500,
|
||||||
} as XMLHttpRequest,
|
} as XMLHttpRequest,
|
||||||
'{"errcode": "TEST"}',
|
'{"errcode": "TEST"}',
|
||||||
@@ -160,6 +172,8 @@ describe("parseErrorResponse", () => {
|
|||||||
},
|
},
|
||||||
500,
|
500,
|
||||||
"https://example.com",
|
"https://example.com",
|
||||||
|
undefined,
|
||||||
|
expect.any(Headers),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -182,9 +196,40 @@ describe("parseErrorResponse", () => {
|
|||||||
},
|
},
|
||||||
500,
|
500,
|
||||||
"https://example.com",
|
"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", () => {
|
describe("with HTTP headers", () => {
|
||||||
function addHeaders(headers: Headers) {
|
function addHeaders(headers: Headers) {
|
||||||
@@ -196,7 +241,7 @@ describe("parseErrorResponse", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function compareHeaders(expectedHeaders: Headers, otherHeaders: Headers | undefined) {
|
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", () => {
|
it("should resolve HTTP Errors from XHR with headers", () => {
|
||||||
@@ -265,7 +310,7 @@ describe("parseErrorResponse", () => {
|
|||||||
} as Response,
|
} as Response,
|
||||||
'{"errcode": "TEST"}',
|
'{"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", () => {
|
it("should handle empty type gracefully", () => {
|
||||||
@@ -304,27 +349,27 @@ describe("parseErrorResponse", () => {
|
|||||||
} as Response,
|
} as Response,
|
||||||
"I'm a teapot",
|
"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", () => {
|
describe("retryNetworkOperation", () => {
|
||||||
it("should retry given number of times with exponential sleeps", async () => {
|
it("should retry given number of times with exponential sleeps", async () => {
|
||||||
const err = new ConnectionError("test");
|
const err = new ConnectionError("test");
|
||||||
const fn = jest.fn().mockRejectedValue(err);
|
const fn = vi.fn().mockRejectedValue(err);
|
||||||
mocked(sleep).mockResolvedValue(undefined);
|
vi.mocked(sleep).mockResolvedValue(undefined);
|
||||||
await expect(retryNetworkOperation(4, fn)).rejects.toThrow(err);
|
await expect(retryNetworkOperation(4, fn)).rejects.toThrow(err);
|
||||||
expect(fn).toHaveBeenCalledTimes(4);
|
expect(fn).toHaveBeenCalledTimes(4);
|
||||||
expect(mocked(sleep)).toHaveBeenCalledTimes(3);
|
expect(vi.mocked(sleep)).toHaveBeenCalledTimes(3);
|
||||||
expect(mocked(sleep).mock.calls[0][0]).toBe(2000);
|
expect(vi.mocked(sleep).mock.calls[0][0]).toBe(2000);
|
||||||
expect(mocked(sleep).mock.calls[1][0]).toBe(4000);
|
expect(vi.mocked(sleep).mock.calls[1][0]).toBe(4000);
|
||||||
expect(mocked(sleep).mock.calls[2][0]).toBe(8000);
|
expect(vi.mocked(sleep).mock.calls[2][0]).toBe(8000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should bail out on errors other than ConnectionError", async () => {
|
it("should bail out on errors other than ConnectionError", async () => {
|
||||||
const err = new TypeError("invalid JSON");
|
const err = new TypeError("invalid JSON");
|
||||||
const fn = jest.fn().mockRejectedValue(err);
|
const fn = vi.fn().mockRejectedValue(err);
|
||||||
mocked(sleep).mockResolvedValue(undefined);
|
vi.mocked(sleep).mockResolvedValue(undefined);
|
||||||
await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err);
|
await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err);
|
||||||
expect(fn).toHaveBeenCalledTimes(1);
|
expect(fn).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -334,10 +379,10 @@ describe("retryNetworkOperation", () => {
|
|||||||
const err2 = new ConnectionError("test2");
|
const err2 = new ConnectionError("test2");
|
||||||
const err3 = new ConnectionError("test3");
|
const err3 = new ConnectionError("test3");
|
||||||
const errors = [err1, err2, err3];
|
const errors = [err1, err2, err3];
|
||||||
const fn = jest.fn().mockImplementation(() => {
|
const fn = vi.fn().mockImplementation(() => {
|
||||||
throw errors.shift();
|
throw errors.shift();
|
||||||
});
|
});
|
||||||
mocked(sleep).mockResolvedValue(undefined);
|
vi.mocked(sleep).mockResolvedValue(undefined);
|
||||||
await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err3);
|
await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err3);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import { type MatrixClient } from "../../src/client";
|
import { type MatrixClient } from "../../src/client";
|
||||||
import { logger } from "../../src/logger";
|
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 { HTTPError, MatrixError } from "../../src/http-api";
|
||||||
import { sleep } from "../../src/utils";
|
import { sleep } from "../../src/utils";
|
||||||
import { secureRandomString } from "../../src/randomstring";
|
import { secureRandomString } from "../../src/randomstring";
|
||||||
@@ -34,14 +34,14 @@ const getFakeClient = (): MatrixClient => new FakeClient() as unknown as MatrixC
|
|||||||
|
|
||||||
describe("InteractiveAuth", () => {
|
describe("InteractiveAuth", () => {
|
||||||
it("should start an auth stage and complete it", async () => {
|
it("should start an auth stage and complete it", async () => {
|
||||||
const doRequest = jest.fn();
|
const doRequest = vi.fn();
|
||||||
const stateUpdated = jest.fn();
|
const stateUpdated = vi.fn();
|
||||||
|
|
||||||
const ia = new InteractiveAuth({
|
const ia = new InteractiveAuth({
|
||||||
matrixClient: getFakeClient(),
|
matrixClient: getFakeClient(),
|
||||||
doRequest: doRequest,
|
doRequest: doRequest,
|
||||||
stateUpdated: stateUpdated,
|
stateUpdated: stateUpdated,
|
||||||
requestEmailToken: jest.fn(),
|
requestEmailToken: vi.fn(),
|
||||||
authData: {
|
authData: {
|
||||||
session: "sessionId",
|
session: "sessionId",
|
||||||
flows: [{ stages: [AuthType.Password] }],
|
flows: [{ stages: [AuthType.Password] }],
|
||||||
@@ -83,14 +83,14 @@ describe("InteractiveAuth", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should handle auth errcode presence", async () => {
|
it("should handle auth errcode presence", async () => {
|
||||||
const doRequest = jest.fn();
|
const doRequest = vi.fn();
|
||||||
const stateUpdated = jest.fn();
|
const stateUpdated = vi.fn();
|
||||||
|
|
||||||
const ia = new InteractiveAuth({
|
const ia = new InteractiveAuth({
|
||||||
matrixClient: getFakeClient(),
|
matrixClient: getFakeClient(),
|
||||||
doRequest: doRequest,
|
doRequest: doRequest,
|
||||||
stateUpdated: stateUpdated,
|
stateUpdated: stateUpdated,
|
||||||
requestEmailToken: jest.fn(),
|
requestEmailToken: vi.fn(),
|
||||||
authData: {
|
authData: {
|
||||||
session: "sessionId",
|
session: "sessionId",
|
||||||
flows: [{ stages: [AuthType.Password] }],
|
flows: [{ stages: [AuthType.Password] }],
|
||||||
@@ -132,9 +132,9 @@ describe("InteractiveAuth", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should handle set emailSid for email flow", async () => {
|
it("should handle set emailSid for email flow", async () => {
|
||||||
const doRequest = jest.fn();
|
const doRequest = vi.fn();
|
||||||
const stateUpdated = jest.fn();
|
const stateUpdated = vi.fn();
|
||||||
const requestEmailToken = jest.fn();
|
const requestEmailToken = vi.fn();
|
||||||
|
|
||||||
const ia = new InteractiveAuth({
|
const ia = new InteractiveAuth({
|
||||||
doRequest,
|
doRequest,
|
||||||
@@ -186,9 +186,9 @@ describe("InteractiveAuth", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should make a request if no authdata is provided", async () => {
|
it("should make a request if no authdata is provided", async () => {
|
||||||
const doRequest = jest.fn();
|
const doRequest = vi.fn();
|
||||||
const stateUpdated = jest.fn();
|
const stateUpdated = vi.fn();
|
||||||
const requestEmailToken = jest.fn();
|
const requestEmailToken = vi.fn();
|
||||||
|
|
||||||
const ia = new InteractiveAuth({
|
const ia = new InteractiveAuth({
|
||||||
matrixClient: getFakeClient(),
|
matrixClient: getFakeClient(),
|
||||||
@@ -248,9 +248,9 @@ describe("InteractiveAuth", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should make a request if authdata is null", async () => {
|
it("should make a request if authdata is null", async () => {
|
||||||
const doRequest = jest.fn();
|
const doRequest = vi.fn();
|
||||||
const stateUpdated = jest.fn();
|
const stateUpdated = vi.fn();
|
||||||
const requestEmailToken = jest.fn();
|
const requestEmailToken = vi.fn();
|
||||||
|
|
||||||
const ia = new InteractiveAuth({
|
const ia = new InteractiveAuth({
|
||||||
matrixClient: getFakeClient(),
|
matrixClient: getFakeClient(),
|
||||||
@@ -310,9 +310,9 @@ describe("InteractiveAuth", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should start an auth stage and reject if no auth flow", async () => {
|
it("should start an auth stage and reject if no auth flow", async () => {
|
||||||
const doRequest = jest.fn();
|
const doRequest = vi.fn();
|
||||||
const stateUpdated = jest.fn();
|
const stateUpdated = vi.fn();
|
||||||
const requestEmailToken = jest.fn();
|
const requestEmailToken = vi.fn();
|
||||||
|
|
||||||
const ia = new InteractiveAuth({
|
const ia = new InteractiveAuth({
|
||||||
matrixClient: getFakeClient(),
|
matrixClient: getFakeClient(),
|
||||||
@@ -337,13 +337,15 @@ describe("InteractiveAuth", () => {
|
|||||||
throw err;
|
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 () => {
|
it("should start an auth stage and reject if no auth flow but has session", async () => {
|
||||||
const doRequest = jest.fn();
|
const doRequest = vi.fn();
|
||||||
const stateUpdated = jest.fn();
|
const stateUpdated = vi.fn();
|
||||||
const requestEmailToken = jest.fn();
|
const requestEmailToken = vi.fn();
|
||||||
|
|
||||||
const ia = new InteractiveAuth({
|
const ia = new InteractiveAuth({
|
||||||
matrixClient: getFakeClient(),
|
matrixClient: getFakeClient(),
|
||||||
@@ -372,13 +374,15 @@ describe("InteractiveAuth", () => {
|
|||||||
throw err;
|
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 () => {
|
it("should handle unexpected error types without data property set", async () => {
|
||||||
const doRequest = jest.fn();
|
const doRequest = vi.fn();
|
||||||
const stateUpdated = jest.fn();
|
const stateUpdated = vi.fn();
|
||||||
const requestEmailToken = jest.fn();
|
const requestEmailToken = vi.fn();
|
||||||
|
|
||||||
const ia = new InteractiveAuth({
|
const ia = new InteractiveAuth({
|
||||||
matrixClient: getFakeClient(),
|
matrixClient: getFakeClient(),
|
||||||
@@ -397,13 +401,13 @@ describe("InteractiveAuth", () => {
|
|||||||
throw err;
|
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 () => {
|
it("should allow dummy auth", async () => {
|
||||||
const doRequest = jest.fn();
|
const doRequest = vi.fn();
|
||||||
const stateUpdated = jest.fn();
|
const stateUpdated = vi.fn();
|
||||||
const requestEmailToken = jest.fn();
|
const requestEmailToken = vi.fn();
|
||||||
|
|
||||||
const ia = new InteractiveAuth({
|
const ia = new InteractiveAuth({
|
||||||
matrixClient: getFakeClient(),
|
matrixClient: getFakeClient(),
|
||||||
@@ -435,9 +439,9 @@ describe("InteractiveAuth", () => {
|
|||||||
|
|
||||||
describe("requestEmailToken", () => {
|
describe("requestEmailToken", () => {
|
||||||
it("increases auth attempts", async () => {
|
it("increases auth attempts", async () => {
|
||||||
const doRequest = jest.fn();
|
const doRequest = vi.fn();
|
||||||
const stateUpdated = jest.fn();
|
const stateUpdated = vi.fn();
|
||||||
const requestEmailToken = jest.fn();
|
const requestEmailToken = vi.fn();
|
||||||
requestEmailToken.mockImplementation(async () => ({ sid: "" }));
|
requestEmailToken.mockImplementation(async () => ({ sid: "" }));
|
||||||
|
|
||||||
const ia = new InteractiveAuth({
|
const ia = new InteractiveAuth({
|
||||||
@@ -464,9 +468,9 @@ describe("InteractiveAuth", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("passes errors through", async () => {
|
it("passes errors through", async () => {
|
||||||
const doRequest = jest.fn();
|
const doRequest = vi.fn();
|
||||||
const stateUpdated = jest.fn();
|
const stateUpdated = vi.fn();
|
||||||
const requestEmailToken = jest.fn();
|
const requestEmailToken = vi.fn();
|
||||||
requestEmailToken.mockImplementation(async () => {
|
requestEmailToken.mockImplementation(async () => {
|
||||||
throw new Error("unspecific network error");
|
throw new Error("unspecific network error");
|
||||||
});
|
});
|
||||||
@@ -482,9 +486,9 @@ describe("InteractiveAuth", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("only starts one request at a time", async () => {
|
it("only starts one request at a time", async () => {
|
||||||
const doRequest = jest.fn();
|
const doRequest = vi.fn();
|
||||||
const stateUpdated = jest.fn();
|
const stateUpdated = vi.fn();
|
||||||
const requestEmailToken = jest.fn();
|
const requestEmailToken = vi.fn();
|
||||||
requestEmailToken.mockImplementation(() => sleep(500, { sid: "" }));
|
requestEmailToken.mockImplementation(() => sleep(500, { sid: "" }));
|
||||||
|
|
||||||
const ia = new InteractiveAuth({
|
const ia = new InteractiveAuth({
|
||||||
@@ -499,9 +503,9 @@ describe("InteractiveAuth", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("stores result in email sid", async () => {
|
it("stores result in email sid", async () => {
|
||||||
const doRequest = jest.fn();
|
const doRequest = vi.fn();
|
||||||
const stateUpdated = jest.fn();
|
const stateUpdated = vi.fn();
|
||||||
const requestEmailToken = jest.fn();
|
const requestEmailToken = vi.fn();
|
||||||
const sid = secureRandomString(24);
|
const sid = secureRandomString(24);
|
||||||
requestEmailToken.mockImplementation(() => sleep(500, { sid }));
|
requestEmailToken.mockImplementation(() => sleep(500, { sid }));
|
||||||
|
|
||||||
@@ -518,14 +522,14 @@ describe("InteractiveAuth", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should prioritise shorter flows", async () => {
|
it("should prioritise shorter flows", async () => {
|
||||||
const doRequest = jest.fn();
|
const doRequest = vi.fn();
|
||||||
const stateUpdated = jest.fn();
|
const stateUpdated = vi.fn();
|
||||||
|
|
||||||
const ia = new InteractiveAuth({
|
const ia = new InteractiveAuth({
|
||||||
matrixClient: getFakeClient(),
|
matrixClient: getFakeClient(),
|
||||||
doRequest: doRequest,
|
doRequest: doRequest,
|
||||||
stateUpdated: stateUpdated,
|
stateUpdated: stateUpdated,
|
||||||
requestEmailToken: jest.fn(),
|
requestEmailToken: vi.fn(),
|
||||||
authData: {
|
authData: {
|
||||||
session: "sessionId",
|
session: "sessionId",
|
||||||
flows: [{ stages: [AuthType.Recaptcha, AuthType.Password] }, { stages: [AuthType.Password] }],
|
flows: [{ stages: [AuthType.Recaptcha, AuthType.Password] }, { stages: [AuthType.Password] }],
|
||||||
@@ -539,14 +543,14 @@ describe("InteractiveAuth", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should prioritise flows with entirely supported stages", async () => {
|
it("should prioritise flows with entirely supported stages", async () => {
|
||||||
const doRequest = jest.fn();
|
const doRequest = vi.fn();
|
||||||
const stateUpdated = jest.fn();
|
const stateUpdated = vi.fn();
|
||||||
|
|
||||||
const ia = new InteractiveAuth({
|
const ia = new InteractiveAuth({
|
||||||
matrixClient: getFakeClient(),
|
matrixClient: getFakeClient(),
|
||||||
doRequest: doRequest,
|
doRequest: doRequest,
|
||||||
stateUpdated: stateUpdated,
|
stateUpdated: stateUpdated,
|
||||||
requestEmailToken: jest.fn(),
|
requestEmailToken: vi.fn(),
|
||||||
authData: {
|
authData: {
|
||||||
session: "sessionId",
|
session: "sessionId",
|
||||||
flows: [{ stages: ["com.devture.shared_secret_auth"] }, { stages: [AuthType.Password] }],
|
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 () => {
|
it("should fire stateUpdated callback with error when a request fails", async () => {
|
||||||
const doRequest = jest.fn();
|
const doRequest = vi.fn();
|
||||||
const stateUpdated = jest.fn();
|
const stateUpdated = vi.fn();
|
||||||
|
|
||||||
const ia = new InteractiveAuth({
|
const ia = new InteractiveAuth({
|
||||||
matrixClient: getFakeClient(),
|
matrixClient: getFakeClient(),
|
||||||
doRequest: doRequest,
|
doRequest: doRequest,
|
||||||
stateUpdated: stateUpdated,
|
stateUpdated: stateUpdated,
|
||||||
requestEmailToken: jest.fn(),
|
requestEmailToken: vi.fn(),
|
||||||
authData: {
|
authData: {
|
||||||
session: "sessionId",
|
session: "sessionId",
|
||||||
flows: [{ stages: [AuthType.Password] }],
|
flows: [{ stages: [AuthType.Password] }],
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ let client: MatrixClient;
|
|||||||
describe("Local notification settings", () => {
|
describe("Local notification settings", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
client = new TestClient("@alice:matrix.org", "123", undefined, undefined, undefined).client;
|
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", () => {
|
describe("Lets you set local notification settings", () => {
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ import loglevel from "loglevel";
|
|||||||
import { DebugLogger, logger } from "../../src/logger.ts";
|
import { DebugLogger, logger } from "../../src/logger.ts";
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("logger", () => {
|
describe("logger", () => {
|
||||||
it("should log to console by default", () => {
|
it("should log to console by default", () => {
|
||||||
jest.spyOn(console, "debug").mockReturnValue(undefined);
|
vi.spyOn(console, "debug").mockReturnValue(undefined);
|
||||||
logger.debug("test1");
|
logger.debug("test1");
|
||||||
logger.log("test2");
|
logger.log("test2");
|
||||||
|
|
||||||
@@ -35,8 +35,8 @@ describe("logger", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should allow creation of child loggers which add a prefix", () => {
|
it("should allow creation of child loggers which add a prefix", () => {
|
||||||
jest.spyOn(loglevel, "getLogger");
|
vi.spyOn(loglevel, "getLogger");
|
||||||
jest.spyOn(console, "debug").mockReturnValue(undefined);
|
vi.spyOn(console, "debug").mockReturnValue(undefined);
|
||||||
|
|
||||||
const childLogger = logger.getChild("[prefix1]");
|
const childLogger = logger.getChild("[prefix1]");
|
||||||
expect(loglevel.getLogger).toHaveBeenCalledWith("matrix-[prefix1]");
|
expect(loglevel.getLogger).toHaveBeenCalledWith("matrix-[prefix1]");
|
||||||
@@ -52,7 +52,7 @@ describe("logger", () => {
|
|||||||
|
|
||||||
describe("DebugLogger", () => {
|
describe("DebugLogger", () => {
|
||||||
it("should handle empty log messages", () => {
|
it("should handle empty log messages", () => {
|
||||||
const mockTarget = jest.fn();
|
const mockTarget = vi.fn();
|
||||||
const logger = new DebugLogger(mockTarget as any);
|
const logger = new DebugLogger(mockTarget as any);
|
||||||
logger.info();
|
logger.info();
|
||||||
expect(mockTarget).toHaveBeenCalledTimes(1);
|
expect(mockTarget).toHaveBeenCalledTimes(1);
|
||||||
@@ -60,7 +60,7 @@ describe("DebugLogger", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should handle logging an Error", () => {
|
it("should handle logging an Error", () => {
|
||||||
const mockTarget = jest.fn();
|
const mockTarget = vi.fn();
|
||||||
const logger = new DebugLogger(mockTarget as any);
|
const logger = new DebugLogger(mockTarget as any);
|
||||||
|
|
||||||
// If there is a stack and a message, we use the stack.
|
// If there is a stack and a message, we use the stack.
|
||||||
@@ -79,7 +79,7 @@ describe("DebugLogger", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should handle logging an object", () => {
|
it("should handle logging an object", () => {
|
||||||
const mockTarget = jest.fn();
|
const mockTarget = vi.fn();
|
||||||
const logger = new DebugLogger(mockTarget as any);
|
const logger = new DebugLogger(mockTarget as any);
|
||||||
|
|
||||||
const obj = { a: 1 };
|
const obj = { a: 1 };
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import fetchMock from "fetch-mock-jest";
|
import fetchMock from "@fetch-mock/vitest";
|
||||||
|
|
||||||
import { ClientPrefix, MatrixClient } from "../../src";
|
import { ClientPrefix, MatrixClient } from "../../src";
|
||||||
import { SSOAction } from "../../src/@types/auth";
|
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 urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, undefined);
|
||||||
const url = new URL(urlString);
|
const url = new URL(urlString);
|
||||||
expect(url.searchParams.has("org.matrix.msc3824.action")).toBe(false);
|
expect(url.searchParams.has("org.matrix.msc3824.action")).toBe(false);
|
||||||
|
expect(url.searchParams.has("action")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("register", function () {
|
it("register", function () {
|
||||||
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.REGISTER);
|
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.REGISTER);
|
||||||
const url = new URL(urlString);
|
const url = new URL(urlString);
|
||||||
expect(url.searchParams.get("org.matrix.msc3824.action")).toEqual("register");
|
expect(url.searchParams.get("org.matrix.msc3824.action")).toEqual("register");
|
||||||
|
expect(url.searchParams.get("action")).toEqual("register");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("login", function () {
|
it("login", function () {
|
||||||
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.LOGIN);
|
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.LOGIN);
|
||||||
const url = new URL(urlString);
|
const url = new URL(urlString);
|
||||||
expect(url.searchParams.get("org.matrix.msc3824.action")).toEqual("login");
|
expect(url.searchParams.get("org.matrix.msc3824.action")).toEqual("login");
|
||||||
|
expect(url.searchParams.get("action")).toEqual("login");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("refreshToken", () => {
|
describe("refreshToken", () => {
|
||||||
afterEach(() => {
|
|
||||||
fetchMock.mockReset();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("requests the correctly-prefixed /refresh endpoint when server correctly accepts /v3", async () => {
|
it("requests the correctly-prefixed /refresh endpoint when server correctly accepts /v3", async () => {
|
||||||
const client = createExampleMatrixClient();
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type MatrixEvent } from "../../../src";
|
import { type RtcMembershipData, type SessionMembershipData } from "../../../src/matrixrtc/membershipData/index.ts";
|
||||||
import {
|
import { type IContent, type MatrixEvent } from "../../../src/models/event.ts";
|
||||||
CallMembership,
|
import { EventType } from "../../../src/@types/event.ts";
|
||||||
type SessionMembershipData,
|
import { CallMembership, DEFAULT_EXPIRE_DURATION } from "../../../src/matrixrtc/CallMembership.ts";
|
||||||
DEFAULT_EXPIRE_DURATION,
|
|
||||||
} from "../../../src/matrixrtc/CallMembership";
|
|
||||||
import { membershipTemplate } from "./mocks";
|
|
||||||
|
|
||||||
function makeMockEvent(originTs = 0): MatrixEvent {
|
function createCallMembership(ev: MatrixEvent, content: IContent): CallMembership {
|
||||||
return {
|
vi.mocked(ev.getContent).mockReturnValue(content);
|
||||||
getTs: jest.fn().mockReturnValue(originTs),
|
const data = CallMembership.membershipDataFromMatrixEvent(ev);
|
||||||
getSender: jest.fn().mockReturnValue("@alice:example.org"),
|
return new CallMembership(ev, data, "xx");
|
||||||
} as unknown as MatrixEvent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("CallMembership", () => {
|
describe("CallMembership", () => {
|
||||||
describe("SessionMembershipData", () => {
|
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(() => {
|
beforeEach(() => {
|
||||||
jest.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
const membershipTemplate: SessionMembershipData = {
|
const membershipTemplate: SessionMembershipData = {
|
||||||
call_id: "",
|
"call_id": "",
|
||||||
scope: "m.room",
|
"scope": "m.room",
|
||||||
application: "m.call",
|
"application": "m.call",
|
||||||
device_id: "AAAAAAA",
|
"device_id": "AAAAAAA",
|
||||||
focus_active: { type: "livekit" },
|
"focus_active": { type: "livekit", focus_selection: "oldest_membership" },
|
||||||
foci_preferred: [{ type: "livekit" }],
|
"foci_preferred": [{ type: "livekit" }],
|
||||||
|
"m.call.intent": "voice",
|
||||||
};
|
};
|
||||||
|
|
||||||
it("rejects membership with no device_id", () => {
|
it("rejects membership with no device_id", () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { device_id: undefined }));
|
createCallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { device_id: undefined }));
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects membership with no call_id", () => {
|
it("rejects membership with no call_id", () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { call_id: undefined }));
|
createCallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { call_id: undefined }));
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allow membership with no scope", () => {
|
it("allow membership with no scope", () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined }));
|
createCallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined }));
|
||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses event timestamp if no created_ts", () => {
|
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);
|
expect(membership.createdTs()).toEqual(12345);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses created_ts if present", () => {
|
it("uses created_ts if present", () => {
|
||||||
const membership = new CallMembership(
|
const membership = createCallMembership(
|
||||||
makeMockEvent(12345),
|
makeMockEvent(12345),
|
||||||
Object.assign({}, membershipTemplate, { created_ts: 67890 }),
|
Object.assign({}, membershipTemplate, { created_ts: 67890 }),
|
||||||
);
|
);
|
||||||
@@ -81,47 +87,347 @@ describe("CallMembership", () => {
|
|||||||
|
|
||||||
it("considers memberships unexpired if local age low enough", () => {
|
it("considers memberships unexpired if local age low enough", () => {
|
||||||
const fakeEvent = makeMockEvent(1000);
|
const fakeEvent = makeMockEvent(1000);
|
||||||
fakeEvent.getTs = jest.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION - 1));
|
fakeEvent.getTs = vi.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION - 1));
|
||||||
expect(new CallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(false);
|
expect(createCallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("considers memberships expired if local age large enough", () => {
|
it("considers memberships expired if local age large enough", () => {
|
||||||
const fakeEvent = makeMockEvent(1000);
|
const fakeEvent = makeMockEvent(1000);
|
||||||
fakeEvent.getTs = jest.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION + 1));
|
fakeEvent.getTs = vi.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION + 1));
|
||||||
expect(new CallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(true);
|
expect(createCallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns preferred foci", () => {
|
it("returns preferred foci", () => {
|
||||||
const fakeEvent = makeMockEvent();
|
const fakeEvent = makeMockEvent();
|
||||||
const mockFocus = { type: "this_is_a_mock_focus" };
|
const mockFocus = { type: "this_is_a_mock_focus" };
|
||||||
const membership = new CallMembership(
|
const membership = createCallMembership(fakeEvent, { ...membershipTemplate, foci_preferred: [mockFocus] });
|
||||||
fakeEvent,
|
expect(membership.transports).toEqual([mockFocus]);
|
||||||
Object.assign({}, membershipTemplate, { foci_preferred: [mockFocus] }),
|
});
|
||||||
);
|
|
||||||
expect(membership.getPreferredFoci()).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", () => {
|
describe("RtcMembershipData", () => {
|
||||||
let fakeEvent: MatrixEvent;
|
function makeMockEvent(originTs = 0, content: IContent = {}): MatrixEvent {
|
||||||
let membership: CallMembership;
|
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(() => {
|
it("rejects membership with no slot_id", () => {
|
||||||
// server origin timestamp for this event is 1000
|
expect(() => {
|
||||||
fakeEvent = makeMockEvent(1000);
|
createCallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: undefined });
|
||||||
membership = new CallMembership(fakeEvent!, membershipTemplate);
|
}).toThrow();
|
||||||
|
});
|
||||||
jest.useFakeTimers();
|
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(() => {
|
it("rejects membership with no application", () => {
|
||||||
jest.useRealTimers();
|
expect(() => {
|
||||||
|
createCallMembership(makeMockEvent(), { ...membershipTemplate, application: undefined });
|
||||||
|
}).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calculates time until expiry", () => {
|
it("rejects membership with incorrect application", () => {
|
||||||
jest.setSystemTime(2000);
|
expect(() => {
|
||||||
// should be using absolute expiry time
|
createCallMembership(makeMockEvent(), {
|
||||||
expect(membership.getMsUntilExpiry()).toEqual(DEFAULT_EXPIRE_DURATION - 1000);
|
...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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { isLivekitFocus, isLivekitFocusActive, isLivekitFocusConfig } from "../../../src/matrixrtc/LivekitFocus";
|
import {
|
||||||
|
isLivekitTransport,
|
||||||
|
isLivekitFocusSelection,
|
||||||
|
isLivekitTransportConfig,
|
||||||
|
} from "../../../src/matrixrtc/LivekitTransport";
|
||||||
|
|
||||||
describe("LivekitFocus", () => {
|
describe("LivekitFocus", () => {
|
||||||
it("isLivekitFocus", () => {
|
it("isLivekitFocus", () => {
|
||||||
expect(
|
expect(
|
||||||
isLivekitFocus({
|
isLivekitTransport({
|
||||||
type: "livekit",
|
type: "livekit",
|
||||||
livekit_service_url: "http://test.com",
|
livekit_service_url: "http://test.com",
|
||||||
livekit_alias: "test",
|
livekit_alias: "test",
|
||||||
}),
|
}),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
expect(isLivekitFocus({ type: "livekit" })).toBeFalsy();
|
expect(isLivekitTransport({ type: "livekit" })).toBeFalsy();
|
||||||
expect(
|
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();
|
).toBeFalsy();
|
||||||
expect(
|
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();
|
).toBeFalsy();
|
||||||
expect(
|
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();
|
).toBeFalsy();
|
||||||
});
|
});
|
||||||
it("isLivekitFocusActive", () => {
|
it("isLivekitFocusActive", () => {
|
||||||
expect(
|
expect(
|
||||||
isLivekitFocusActive({
|
isLivekitFocusSelection({
|
||||||
type: "livekit",
|
type: "livekit",
|
||||||
focus_selection: "oldest_membership",
|
focus_selection: "oldest_membership",
|
||||||
}),
|
}),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
expect(isLivekitFocusActive({ type: "livekit" })).toBeFalsy();
|
expect(isLivekitFocusSelection({ type: "livekit" })).toBeFalsy();
|
||||||
expect(isLivekitFocusActive({ type: "not-livekit", focus_selection: "oldest_membership" })).toBeFalsy();
|
expect(isLivekitFocusSelection({ type: "not-livekit", focus_selection: "oldest_membership" })).toBeFalsy();
|
||||||
});
|
});
|
||||||
it("isLivekitFocusConfig", () => {
|
it("isLivekitFocusConfig", () => {
|
||||||
expect(
|
expect(
|
||||||
isLivekitFocusConfig({
|
isLivekitTransportConfig({
|
||||||
type: "livekit",
|
type: "livekit",
|
||||||
livekit_service_url: "http://test.com",
|
livekit_service_url: "http://test.com",
|
||||||
}),
|
}),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
expect(isLivekitFocusConfig({ type: "livekit" })).toBeFalsy();
|
expect(isLivekitTransportConfig({ type: "livekit" })).toBeFalsy();
|
||||||
expect(isLivekitFocusConfig({ type: "not-livekit", livekit_service_url: "http://test.com" })).toBeFalsy();
|
expect(isLivekitTransportConfig({ type: "not-livekit", livekit_service_url: "http://test.com" })).toBeFalsy();
|
||||||
expect(isLivekitFocusConfig({ type: "livekit", other_service_url: "oldest_membership" })).toBeFalsy();
|
expect(isLivekitTransportConfig({ type: "livekit", other_service_url: "oldest_membership" })).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -14,54 +14,174 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ClientEvent, EventTimeline, MatrixClient } from "../../../src";
|
import { ClientEvent, EventTimeline, MatrixClient, type Room, RoomStateEvent } from "../../../src";
|
||||||
import { RoomStateEvent } from "../../../src/models/room-state";
|
import { MatrixRTCSessionManager, MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc";
|
||||||
import { MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc/MatrixRTCSessionManager";
|
import {
|
||||||
import { makeMockRoom, membershipTemplate, mockRoomState } from "./mocks";
|
makeMockRoom,
|
||||||
|
type MembershipData,
|
||||||
|
sessionMembershipTemplate,
|
||||||
|
mockRoomState,
|
||||||
|
mockRTCEvent,
|
||||||
|
rtcMembershipTemplate,
|
||||||
|
} from "./mocks.ts";
|
||||||
|
import { logger } from "../../../src/logger";
|
||||||
|
import { flushPromises } from "../../test-utils/flushPromises";
|
||||||
|
import { type RtcMembershipData, type SessionMembershipData } from "../../../src/matrixrtc/membershipData";
|
||||||
|
|
||||||
describe("MatrixRTCSessionManager", () => {
|
describe.each([{ eventKind: "sticky" }, { eventKind: "memberState" }])(
|
||||||
let client: MatrixClient;
|
"MatrixRTCSessionManager ($eventKind)",
|
||||||
|
({ eventKind }) => {
|
||||||
|
let client: MatrixClient;
|
||||||
|
|
||||||
beforeEach(() => {
|
function generateMembership(opts: { type: string; callId?: string } = { type: "m.call" }): MembershipData {
|
||||||
client = new MatrixClient({ baseUrl: "base_url" });
|
if (eventKind === "sticky") {
|
||||||
client.matrixRTC.start();
|
return {
|
||||||
});
|
...rtcMembershipTemplate,
|
||||||
|
slot_id: opts.callId ? `${opts.type}#${opts.callId}` : rtcMembershipTemplate.slot_id,
|
||||||
|
application: {
|
||||||
|
...rtcMembershipTemplate.application,
|
||||||
|
type: opts.type,
|
||||||
|
},
|
||||||
|
} satisfies RtcMembershipData & { user_id: string };
|
||||||
|
}
|
||||||
|
|
||||||
afterEach(() => {
|
return {
|
||||||
client.stopClient();
|
...sessionMembershipTemplate,
|
||||||
client.matrixRTC.stop();
|
application: opts.type,
|
||||||
});
|
call_id: opts.callId ?? sessionMembershipTemplate.call_id, // approximate version.
|
||||||
|
} satisfies SessionMembershipData & { user_id: string };
|
||||||
|
}
|
||||||
|
|
||||||
it("Fires event when session starts", () => {
|
async function sendLeaveMembership(room: Room, membershipData: MembershipData[]): Promise<void> {
|
||||||
const onStarted = jest.fn();
|
if (eventKind === "memberState") {
|
||||||
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
mockRoomState(room, [{ user_id: sessionMembershipTemplate.user_id }]);
|
||||||
|
const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
||||||
|
const membEvent = roomState.getStateEvents("org.matrix.msc3401.call.member")[0];
|
||||||
|
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
|
||||||
|
} else {
|
||||||
|
membershipData.splice(0, 1, { user_id: sessionMembershipTemplate.user_id });
|
||||||
|
client.emit(ClientEvent.Event, mockRTCEvent(membershipData[0], room.roomId, 10000));
|
||||||
|
}
|
||||||
|
await flushPromises();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
beforeEach(() => {
|
||||||
const room1 = makeMockRoom([membershipTemplate]);
|
client = new MatrixClient({ baseUrl: "base_url" });
|
||||||
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
|
client.matrixRTC.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
client.stopClient();
|
||||||
|
client.matrixRTC.stop();
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Fires event when session starts", async () => {
|
||||||
|
const room1 = makeMockRoom([generateMembership({ type: "m.call" })], eventKind === "sticky");
|
||||||
|
vi.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||||
|
const sessionStartedPromise = new Promise((resolve) =>
|
||||||
|
client.matrixRTC.once(MatrixRTCSessionManagerEvents.SessionStarted, resolve),
|
||||||
|
);
|
||||||
|
client.emit(ClientEvent.Room, room1);
|
||||||
|
await expect(sessionStartedPromise).resolves.toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Doesn't fire event if unrelated sessions starts", () => {
|
||||||
|
const onStarted = vi.fn();
|
||||||
|
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const room1 = makeMockRoom([generateMembership({ type: "m.other" })], eventKind === "sticky");
|
||||||
|
vi.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||||
|
|
||||||
|
client.emit(ClientEvent.Room, room1);
|
||||||
|
expect(onStarted).not.toHaveBeenCalled();
|
||||||
|
} finally {
|
||||||
|
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Fires event when session ends", async () => {
|
||||||
|
const sessionStartedPromise = new Promise((resolve) =>
|
||||||
|
client.matrixRTC.once(MatrixRTCSessionManagerEvents.SessionStarted, resolve),
|
||||||
|
);
|
||||||
|
const sessionEndedPromise = new Promise((resolve) =>
|
||||||
|
client.matrixRTC.once(MatrixRTCSessionManagerEvents.SessionEnded, (...params) => resolve(params)),
|
||||||
|
);
|
||||||
|
const membershipData: MembershipData[] = [generateMembership()];
|
||||||
|
const room1 = makeMockRoom(membershipData, eventKind === "sticky");
|
||||||
|
vi.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||||
|
vi.spyOn(client, "getRoom").mockReturnValue(room1);
|
||||||
|
client.emit(ClientEvent.Room, room1);
|
||||||
|
await sessionStartedPromise;
|
||||||
|
await sendLeaveMembership(room1, membershipData);
|
||||||
|
|
||||||
|
await expect(sessionEndedPromise).resolves.toStrictEqual([
|
||||||
|
room1.roomId,
|
||||||
|
client.matrixRTC.getActiveRoomSession(room1),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Fires correctly with custom sessionDescription", async () => {
|
||||||
|
const onStarted = vi.fn();
|
||||||
|
const onEnded = vi.fn();
|
||||||
|
// create a session manager with a custom session description
|
||||||
|
const sessionManager = new MatrixRTCSessionManager(logger, client, {
|
||||||
|
id: "test",
|
||||||
|
application: "m.notCall",
|
||||||
|
});
|
||||||
|
|
||||||
|
// manually start the session manager (its not the default one started by the client)
|
||||||
|
sessionManager.start();
|
||||||
|
sessionManager.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
|
||||||
|
sessionManager.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
||||||
|
const sessionStartedPromise = new Promise((resolve) =>
|
||||||
|
sessionManager.once(MatrixRTCSessionManagerEvents.SessionStarted, resolve),
|
||||||
|
);
|
||||||
|
const sessionEndedPromise = new Promise((resolve) =>
|
||||||
|
sessionManager.once(MatrixRTCSessionManagerEvents.SessionEnded, (...params) => resolve(params)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a session for applicaation m.other, we ignore this session because it lacks a call_id
|
||||||
|
const room1MembershipData: MembershipData[] = [generateMembership({ type: "m.other" })];
|
||||||
|
const room1 = makeMockRoom(room1MembershipData, eventKind === "sticky");
|
||||||
|
vi.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||||
|
client.emit(ClientEvent.Room, room1);
|
||||||
|
await flushPromises();
|
||||||
|
expect(onStarted).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Create a session for applicaation m.notCall. We expect this call to be tracked because it has matching call_id
|
||||||
|
const room2MembershipData: MembershipData[] = [generateMembership({ type: "m.notCall", callId: "test" })];
|
||||||
|
const room2 = makeMockRoom(room2MembershipData, eventKind === "sticky");
|
||||||
|
vi.spyOn(client, "getRooms").mockReturnValue([room2]);
|
||||||
|
client.emit(ClientEvent.Room, room2);
|
||||||
|
await flushPromises();
|
||||||
|
await sessionStartedPromise;
|
||||||
|
|
||||||
|
// Stop room1's RTC session. Not tracked.
|
||||||
|
vi.spyOn(client, "getRoom").mockReturnValue(room1);
|
||||||
|
await sendLeaveMembership(room1, room1MembershipData);
|
||||||
|
expect(onEnded).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Stop room2's RTC session. Tracked.
|
||||||
|
vi.spyOn(client, "getRoom").mockReturnValue(room2);
|
||||||
|
await sendLeaveMembership(room2, room2MembershipData);
|
||||||
|
await sessionEndedPromise;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Doesn't fire event if unrelated sessions ends", async () => {
|
||||||
|
const onEnded = vi.fn();
|
||||||
|
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
|
||||||
|
const membership: MembershipData[] = [generateMembership({ type: "m.other_app" })];
|
||||||
|
const room1 = makeMockRoom(membership, eventKind === "sticky");
|
||||||
|
vi.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||||
|
vi.spyOn(client, "getRoom").mockReturnValue(room1);
|
||||||
|
|
||||||
client.emit(ClientEvent.Room, room1);
|
client.emit(ClientEvent.Room, room1);
|
||||||
expect(onStarted).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
|
|
||||||
} finally {
|
|
||||||
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Fires event when session ends", () => {
|
await sendLeaveMembership(room1, membership);
|
||||||
const onEnded = jest.fn();
|
|
||||||
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
|
|
||||||
const room1 = makeMockRoom([membershipTemplate]);
|
|
||||||
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
|
|
||||||
jest.spyOn(client, "getRoom").mockReturnValue(room1);
|
|
||||||
|
|
||||||
client.emit(ClientEvent.Room, room1);
|
expect(onEnded).not.toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
|
||||||
|
});
|
||||||
mockRoomState(room1, [{ user_id: membershipTemplate.user_id }]);
|
},
|
||||||
|
);
|
||||||
const roomState = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
|
||||||
const membEvent = roomState.getStateEvents("")[0];
|
|
||||||
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
|
|
||||||
|
|
||||||
expect(onEnded).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
Copyright 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.
|
||||||
|
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 { computeRtcIdentityRaw } from "../../../src/matrixrtc/membershipData/index.ts";
|
||||||
|
|
||||||
|
describe("computeRtcIdentityRaw", () => {
|
||||||
|
it("should compute the correct identity hash", async () => {
|
||||||
|
// Test vector taken from the spec, with the expected output updated to match the unpadded base64 encoding
|
||||||
|
// https://github.com/hughns/matrix-spec-proposals/blob/hughns/matrixrtc-livekit/proposals/4195-matrixrtc-livekit.md#appendix-hash-derivation-test-vectors
|
||||||
|
const result = await computeRtcIdentityRaw("@alice:example.com", "DEVICE123", "memberABC");
|
||||||
|
// Add assertions based on expected hash output
|
||||||
|
expect(result).toBe("J+T45tGruxc+HrUOqJJlyQSV33m728Cme4+vt8/SWrU");
|
||||||
|
});
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user