Compare commits
221 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 | |||
| 3d1bcb73c1 | |||
| a960e686b3 | |||
| 946774c3fb | |||
| 15edbc8067 | |||
| 1398ac24a2 | |||
| c76df4cd8f | |||
| a5e4dbf2d3 | |||
| 3768187395 | |||
| 08d0ce25f1 |
+28
-12
@@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: ["matrix-org", "import", "jsdoc", "n"],
|
||||
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/jest", "plugin:import/typescript"],
|
||||
plugins: ["matrix-org", "import", "jsdoc", "n", "@vitest"],
|
||||
extends: ["plugin:matrix-org/babel", "plugin:import/typescript"],
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.json"],
|
||||
},
|
||||
@@ -83,16 +83,6 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
],
|
||||
// Disabled tests are a reality for now but as soon as all of the xits are
|
||||
// eliminated, we should enforce this.
|
||||
"jest/no-disabled-tests": "off",
|
||||
// Used in some crypto tests.
|
||||
"jest/no-standalone-expect": [
|
||||
"error",
|
||||
{
|
||||
additionalTestBlockFunctions: ["beforeAll", "beforeEach"],
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
@@ -147,11 +137,37 @@ module.exports = {
|
||||
},
|
||||
{
|
||||
files: ["spec/**/*.ts"],
|
||||
extends: ["plugin:@vitest/legacy-recommended"],
|
||||
rules: {
|
||||
// We don't need super strict typing in test utilities
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-member-accessibility": "off",
|
||||
"@typescript-eslint/no-empty-object-type": "off",
|
||||
|
||||
// Disabled tests are a reality for now but as soon as all of the xits are
|
||||
// eliminated, we should enforce this.
|
||||
"@vitest/no-disabled-tests": "off",
|
||||
// Used in some crypto tests.
|
||||
"@vitest/no-standalone-expect": [
|
||||
"error",
|
||||
{
|
||||
additionalTestBlockFunctions: ["beforeAll", "beforeEach"],
|
||||
},
|
||||
],
|
||||
"@vitest/expect-expect": [
|
||||
"error",
|
||||
{
|
||||
assertFunctionNames: [
|
||||
"expect",
|
||||
"expectDevices",
|
||||
"assert.isTrue",
|
||||
"assert.isFalse",
|
||||
"passwordTest",
|
||||
"compareHeaders",
|
||||
"doTest",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
* @matrix-org/element-web-reviewers
|
||||
/.github/workflows/** @matrix-org/element-web-team
|
||||
/package.json @matrix-org/element-web-team
|
||||
/yarn.lock @matrix-org/element-web-team
|
||||
/pnpm-lock.yaml @matrix-org/element-web-team
|
||||
/scripts/** @matrix-org/element-web-team
|
||||
/src/webrtc @matrix-org/element-call-reviewers
|
||||
/src/matrixrtc @matrix-org/element-call-reviewers
|
||||
|
||||
@@ -22,7 +22,7 @@ runs:
|
||||
|
||||
- name: Upload tarball signature
|
||||
if: ${{ inputs.upload-url }}
|
||||
uses: shogo82148/actions-upload-release-asset@59cbc563d11314e48122193f8fe5cdda62ea6cf9 # v1
|
||||
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ env.VERSION }}.tar.gz.asc
|
||||
|
||||
@@ -29,13 +29,13 @@ runs:
|
||||
|
||||
- name: Upload asset signatures
|
||||
if: inputs.gpg-fingerprint
|
||||
uses: shogo82148/actions-upload-release-asset@59cbc563d11314e48122193f8fe5cdda62ea6cf9 # v1
|
||||
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ inputs.asset-path }}.asc
|
||||
|
||||
- name: Upload assets
|
||||
uses: shogo82148/actions-upload-release-asset@59cbc563d11314e48122193f8fe5cdda62ea6cf9 # v1
|
||||
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ inputs.asset-path }}
|
||||
|
||||
@@ -41,3 +41,6 @@
|
||||
- name: "Z-Flaky-Test"
|
||||
description: "A test is raising false alarms"
|
||||
color: "ededed"
|
||||
- name: "Z-Skip-Coverage"
|
||||
description: "Skip SonarQube coverage for this PR"
|
||||
color: "ededed"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
name: Backport
|
||||
on:
|
||||
pull_request_target:
|
||||
# Privilege escalation necessary to enable backporting PRs from forks
|
||||
# 🚨 We must not execute any checked out code here.
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers]
|
||||
types:
|
||||
- closed
|
||||
- labeled
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
name: Deploy documentation PR preview
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
# Privilege escalation necessary to publish to Netlify
|
||||
# 🚨 We must not execute any checked out code here.
|
||||
workflow_run: # zizmor: ignore[dangerous-triggers]
|
||||
workflows: ["Static Analysis"]
|
||||
types:
|
||||
- completed
|
||||
@@ -15,7 +17,7 @@ jobs:
|
||||
deployments: write
|
||||
steps:
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Triggers after the "Downstream artifacts" build has finished, to run the
|
||||
# matrix-react-sdk playwright tests (with access to repo secrets)
|
||||
# element-web playwright tests (with access to repo secrets)
|
||||
|
||||
name: matrix-react-sdk End to End Tests
|
||||
name: Element Web End to End Tests
|
||||
on:
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
@@ -21,11 +21,12 @@ concurrency:
|
||||
jobs:
|
||||
playwright:
|
||||
name: Playwright
|
||||
uses: element-hq/element-web/.github/workflows/end-to-end-tests.yaml@develop
|
||||
uses: element-hq/element-web/.github/workflows/build-and-test.yaml@develop # zizmor: ignore[unpinned-uses]
|
||||
permissions:
|
||||
actions: read
|
||||
issues: read
|
||||
pull-requests: read
|
||||
contents: read
|
||||
with:
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
# We only want to run the playwright tests on merge queue to prevent regressions
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
|
||||
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4
|
||||
- name: Notify element-web repo that a new SDK build is on develop so it can CI against it
|
||||
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
repository: ${{ matrix.repo }}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
name: Pull Request
|
||||
on:
|
||||
pull_request_target:
|
||||
# Privilege escalation necessary access members of the review teams
|
||||
# 🚨 We must not execute any checked out code here, and be careful around use of user-controlled inputs.
|
||||
# FIXME: only `community-prs` job needs this privilege, so it should be in its own workflow file.
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers]
|
||||
types: [opened, edited, labeled, unlabeled, synchronize]
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
@@ -15,7 +18,7 @@ jobs:
|
||||
name: Preview Changelog
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: mheap/github-action-required-labels@8afbe8ae6ab7647d0c9f0cfa7c2f939650d22509 # v5
|
||||
- uses: mheap/github-action-required-labels@0ac283b4e65c1fb28ce6079dea5546ceca98ccbe # v5
|
||||
if: github.event_name != 'merge_group'
|
||||
with:
|
||||
labels: |
|
||||
@@ -35,7 +38,7 @@ jobs:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: Add notice
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
|
||||
with:
|
||||
script: |
|
||||
@@ -60,7 +63,7 @@ jobs:
|
||||
|
||||
- name: Add label
|
||||
if: steps.teams.outputs.isTeamMember == 'false'
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.addLabels({
|
||||
@@ -73,13 +76,15 @@ jobs:
|
||||
close-if-fork-develop:
|
||||
name: Forbid develop branch fork contributions
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
pull-requests: write
|
||||
if: >
|
||||
github.event.action == 'opened' &&
|
||||
github.event.pull_request.head.ref == 'develop' &&
|
||||
github.event.pull_request.head.repo.full_name != github.repository
|
||||
steps:
|
||||
- name: Close pull request
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check for X-Release-Blocker label on any open issues or PRs
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
env:
|
||||
REPO: ${{ inputs.repository }}
|
||||
with:
|
||||
|
||||
@@ -16,18 +16,20 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: staging
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- uses: t3chguy/release-drafter@105e541c2c3d857f032bd522c0764694758fabad
|
||||
id: draft-release
|
||||
@@ -37,7 +39,7 @@ jobs:
|
||||
disable-autolabeler: true
|
||||
|
||||
- name: Get actions scripts
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
persist-credentials: false
|
||||
@@ -48,7 +50,7 @@ jobs:
|
||||
|
||||
- name: Ingest upstream changes
|
||||
if: inputs.include-changes
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
||||
|
||||
@@ -13,4 +13,4 @@ jobs:
|
||||
draft:
|
||||
permissions:
|
||||
contents: write
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-drafter-workflow.yml@develop
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-drafter-workflow.yml@develop # zizmor: ignore[unpinned-uses]
|
||||
|
||||
@@ -12,20 +12,25 @@ on:
|
||||
description: List of dependencies to reset.
|
||||
type: string
|
||||
required: false
|
||||
dir:
|
||||
description: The directory to release
|
||||
type: string
|
||||
default: "."
|
||||
concurrency: ${{ github.workflow }}
|
||||
permissions: {} # Uses ELEMENT_BOT_TOKEN
|
||||
jobs:
|
||||
merge:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
|
||||
- name: Get actions scripts
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
persist-credentials: false
|
||||
@@ -33,13 +38,14 @@ jobs:
|
||||
sparse-checkout: |
|
||||
scripts/release
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: Set up git
|
||||
run: |
|
||||
@@ -53,6 +59,7 @@ jobs:
|
||||
|
||||
- name: Reset dependencies
|
||||
if: inputs.dependencies
|
||||
working-directory: ${{ inputs.dir }}
|
||||
run: |
|
||||
while IFS= read -r PACKAGE; do
|
||||
[ -z "$PACKAGE" ] && continue
|
||||
@@ -73,7 +80,7 @@ jobs:
|
||||
fi
|
||||
|
||||
echo "Resetting $PACKAGE to develop branch..."
|
||||
yarn add "github:matrix-org/$PACKAGE#develop"
|
||||
pnpm add "github:matrix-org/$PACKAGE#develop"
|
||||
git add -u
|
||||
git commit -m "Reset $PACKAGE back to develop branch"
|
||||
done <<< "$DEPENDENCIES"
|
||||
|
||||
@@ -26,12 +26,21 @@ on:
|
||||
description: |
|
||||
The path to the asset you want to upload, if any. You can use glob patterns here.
|
||||
Will be GPG signed and an `.asc` file included in the release artifacts if `gpg-fingerprint` is set.
|
||||
Relative to `dir`.
|
||||
type: string
|
||||
required: false
|
||||
expected-asset-count:
|
||||
description: The number of expected assets, including signatures, excluding generated zip & tarball.
|
||||
type: number
|
||||
required: false
|
||||
dist-dir:
|
||||
description: The directory to release
|
||||
type: string
|
||||
default: "."
|
||||
version-dirs:
|
||||
description: Directories in which to update package.json `version` field
|
||||
type: string
|
||||
required: false
|
||||
outputs:
|
||||
npm-id:
|
||||
description: "The npm package@version string we published"
|
||||
@@ -43,7 +52,7 @@ jobs:
|
||||
permissions:
|
||||
issues: read
|
||||
pull-requests: read
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop # zizmor: ignore[unpinned-uses]
|
||||
|
||||
release:
|
||||
name: Release
|
||||
@@ -56,7 +65,7 @@ jobs:
|
||||
- name: Load GPG key
|
||||
id: gpg
|
||||
if: inputs.gpg-fingerprint
|
||||
uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6
|
||||
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
@@ -64,22 +73,23 @@ jobs:
|
||||
|
||||
- name: Get draft release
|
||||
id: draft-release
|
||||
uses: cardinalby/git-get-release-action@5172c3a026600b1d459b117738c605fabc9e4e44 # v1
|
||||
uses: cardinalby/git-get-release-action@5172c3a026600b1d459b117738c605fabc9e4e44 # 1.2.5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
draft: true
|
||||
latest: true
|
||||
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: staging
|
||||
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
|
||||
- name: Get actions scripts
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
persist-credentials: false
|
||||
@@ -90,6 +100,7 @@ jobs:
|
||||
|
||||
- name: Prepare variables
|
||||
id: prepare
|
||||
working-directory: ${{ inputs.dist-dir }}
|
||||
run: |
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
|
||||
@@ -104,7 +115,7 @@ jobs:
|
||||
run: echo "VERSION=$(echo $VERSION | cut -d- -f1)" >> $GITHUB_ENV
|
||||
|
||||
- name: Check version number not in use
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
with:
|
||||
script: |
|
||||
const { VERSION } = process.env;
|
||||
@@ -123,15 +134,17 @@ jobs:
|
||||
git config --global user.email "releases@riot.im"
|
||||
git config --global user.name "RiotRobot"
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
cache: "pnpm"
|
||||
node-version-file: ${{ inputs.dist-dir }}/package.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: "yarn install --frozen-lockfile"
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: Handle develop dependencies
|
||||
working-directory: ${{ inputs.dist-dir }}
|
||||
run: |
|
||||
ret=0
|
||||
cat package.json | jq -r '.dependencies | to_entries | .[] | "\(.key) \(.value)"' | grep '#develop$' | while read -r dep ; do
|
||||
@@ -140,15 +153,19 @@ jobs:
|
||||
VERSION=${dep[1]}
|
||||
|
||||
echo "::warning title=Develop dependency found::$DEPENDENCY will be kept at $VERSION"
|
||||
yarn upgrade "$PACKAGE@$VERSION" --exact
|
||||
pnpm add "$PACKAGE@$VERSION" --save-exact
|
||||
git add -u
|
||||
git commit -m "Keep $PACKAGE at $VERSION"
|
||||
done
|
||||
|
||||
- name: Bump package.json version
|
||||
- name: Bump package.json versions
|
||||
run: |
|
||||
yarn version --no-git-tag-version --new-version "${VERSION#v}"
|
||||
git add package.json
|
||||
for DIR in $DIRS; do
|
||||
pnpm version -C "$DIR" --no-git-tag-version "${VERSION#v}"
|
||||
git add "$DIR"/package.json
|
||||
done
|
||||
env:
|
||||
DIRS: ${{ inputs.version-dirs || inputs.dist-dir }}
|
||||
|
||||
- name: Add to CHANGELOG.md
|
||||
if: inputs.final
|
||||
@@ -175,7 +192,8 @@ jobs:
|
||||
|
||||
- name: Build assets
|
||||
if: steps.prepare.outputs.has-dist-script == '1'
|
||||
run: DIST_VERSION="$VERSION" yarn dist
|
||||
working-directory: ${{ inputs.dist-dir }}
|
||||
run: DIST_VERSION="$VERSION" pnpm dist
|
||||
|
||||
- name: Upload release assets & signatures
|
||||
if: inputs.asset-path
|
||||
@@ -183,7 +201,7 @@ jobs:
|
||||
with:
|
||||
gpg-fingerprint: ${{ inputs.gpg-fingerprint }}
|
||||
upload-url: ${{ steps.draft-release.outputs.upload_url }}
|
||||
asset-path: ${{ inputs.asset-path }}
|
||||
asset-path: ${{ inputs.dist-dir }}/${{ inputs.asset-path }}
|
||||
|
||||
- name: Create signed tag
|
||||
if: inputs.gpg-fingerprint
|
||||
@@ -216,7 +234,7 @@ jobs:
|
||||
|
||||
- name: Validate release has expected assets
|
||||
if: inputs.expected-asset-count
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
env:
|
||||
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
||||
EXPECTED_ASSET_COUNT: ${{ inputs.expected-asset-count }}
|
||||
@@ -244,7 +262,7 @@ jobs:
|
||||
git push origin master
|
||||
|
||||
- name: Publish release
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
env:
|
||||
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
||||
FINAL: ${{ inputs.final }}
|
||||
@@ -276,7 +294,9 @@ jobs:
|
||||
name: Publish to npm
|
||||
needs: release
|
||||
if: inputs.npm
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop # zizmor: ignore[unpinned-uses]
|
||||
with:
|
||||
dir: ${{ inputs.dist-dir }}
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
name: Publish to npm
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
dir:
|
||||
description: The directory to release
|
||||
type: string
|
||||
default: "."
|
||||
outputs:
|
||||
id:
|
||||
description: "The npm package@version string we published"
|
||||
@@ -17,26 +22,29 @@ jobs:
|
||||
id: ${{ steps.npm-publish.outputs.id }}
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: staging
|
||||
persist-credentials: false
|
||||
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- name: 🔧 pnpm cache
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
node-version-file: package.json
|
||||
node-version-file: ${{ inputs.dir }}/package.json
|
||||
|
||||
# Ensure npm 11.5.1 or later is installed
|
||||
- name: Update npm
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --frozen-lockfile"
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: 🚀 Publish to npm
|
||||
id: npm-publish
|
||||
working-directory: ${{ inputs.dir }}
|
||||
run: |
|
||||
npm publish --provenance --access public --tag "$TAG"
|
||||
release=$(jq -r '"\(.name)@\(.version)"' package.json)
|
||||
|
||||
@@ -24,7 +24,7 @@ concurrency: ${{ github.workflow }}
|
||||
permissions: {} # No permissions required
|
||||
jobs:
|
||||
release:
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-make.yml@develop
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-make.yml@develop # zizmor: ignore[unpinned-uses,secrets-inherit]
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
@@ -41,28 +41,32 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
repo:
|
||||
- element-hq/element-web
|
||||
include:
|
||||
- repo: element-hq/element-web
|
||||
path: apps/web
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: ${{ matrix.repo }}
|
||||
ref: staging
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
persist-credentials: true
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Bump dependency
|
||||
env:
|
||||
DEPENDENCY: ${{ needs.release.outputs.npm-id }}
|
||||
DIR: ${{ matrix.path }}
|
||||
run: |
|
||||
git config --global user.email "releases@riot.im"
|
||||
git config --global user.name "RiotRobot"
|
||||
yarn upgrade "$DEPENDENCY" --exact
|
||||
git add package.json yarn.lock
|
||||
pnpm add -C "$DIR" "$DEPENDENCY" --save-exact
|
||||
git add "$DIR"/package.json pnpm-lock.yaml
|
||||
git commit -am"Upgrade dependency to $DEPENDENCY"
|
||||
git push origin staging
|
||||
|
||||
@@ -73,22 +77,25 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- name: 🔧 pnpm cache
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --frozen-lockfile"
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: 📖 Generate docs
|
||||
run: yarn gendoc
|
||||
run: pnpm gendoc
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
|
||||
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
|
||||
with:
|
||||
path: _docs
|
||||
|
||||
@@ -106,4 +113,4 @@ jobs:
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
|
||||
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5
|
||||
|
||||
@@ -12,7 +12,11 @@ on:
|
||||
sharded:
|
||||
type: boolean
|
||||
required: false
|
||||
description: "Whether to combine multiple LCOV and jest-sonar-report files in coverage artifact"
|
||||
description: "Whether to combine multiple LCOV and sonar-report files in coverage artifact"
|
||||
version-pkg-json-dir:
|
||||
type: string
|
||||
default: "."
|
||||
description: "Relative path of the directory containing package.json with the `version` to use."
|
||||
permissions: {}
|
||||
jobs:
|
||||
sonarqube:
|
||||
@@ -27,7 +31,7 @@ jobs:
|
||||
steps:
|
||||
# We create the status here and then update it to success/failure in the `report` stage
|
||||
# This provides an easy link to this workflow_run from the PR before Sonarcloud is done.
|
||||
- uses: guibranco/github-status-action-v2@5530c593759f489bba08272e96986ffc571c1ea1
|
||||
- uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: pending
|
||||
@@ -36,14 +40,15 @@ jobs:
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- name: "🧮 Checkout code"
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
persist-credentials: false
|
||||
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
if: ${{ !inputs.sharded }}
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -51,14 +56,13 @@ jobs:
|
||||
name: coverage
|
||||
path: coverage
|
||||
- name: 📥 Download sharded artifacts
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
if: inputs.sharded
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
pattern: coverage-*
|
||||
path: coverage
|
||||
merge-multiple: true
|
||||
- name: Check coverage artifact
|
||||
run: |
|
||||
if [ ! -d coverage ]; then
|
||||
@@ -75,19 +79,20 @@ jobs:
|
||||
|
||||
- name: "🩻 SonarCloud Scan"
|
||||
id: sonarcloud
|
||||
uses: matrix-org/sonarcloud-workflow-action@820f7c2e9e94ba9e35add0f739691e5c7e23fa25 # v4.0
|
||||
uses: matrix-org/sonarcloud-workflow-action@13968a27c924fa19b1dacbce6ca3ff217daa775b
|
||||
# workflow_run fails report against the develop commit always, we don't want that for PRs
|
||||
continue-on-error: ${{ github.event.workflow_run.head_branch != 'develop' }}
|
||||
with:
|
||||
skip_checkout: true
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
version_cmd: "cat package.json | jq -r .version"
|
||||
skip_coverage_label: Z-Skip-Coverage
|
||||
version_cmd: "cat ${{ inputs.version-pkg-json-dir }}/package.json | jq -r .version"
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
revision: ${{ github.event.workflow_run.head_sha }}
|
||||
token: ${{ secrets.SONAR_TOKEN }}
|
||||
|
||||
- uses: guibranco/github-status-action-v2@5530c593759f489bba08272e96986ffc571c1ea1
|
||||
- uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
|
||||
if: always()
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
name: SonarQube
|
||||
on:
|
||||
workflow_run:
|
||||
# Privilege escalation necessary to call upon SonarCloud
|
||||
# 🚨 We must not execute any checked out code here.
|
||||
workflow_run: # zizmor: ignore[dangerous-triggers]
|
||||
workflows: ["Tests"]
|
||||
types:
|
||||
- completed
|
||||
@@ -16,7 +18,7 @@ jobs:
|
||||
actions: read
|
||||
statuses: write
|
||||
id-token: write # sonar
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop # zizmor: ignore[unpinned-uses]
|
||||
secrets:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
@@ -14,58 +14,61 @@ jobs:
|
||||
name: "Typescript Syntax Check"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
run: "pnpm install"
|
||||
|
||||
- name: Typecheck
|
||||
run: "yarn run lint:types"
|
||||
run: "pnpm run lint:types"
|
||||
|
||||
js_lint:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
run: "pnpm install"
|
||||
|
||||
- name: Run Linter
|
||||
run: "yarn run lint:js"
|
||||
run: "pnpm run lint:js"
|
||||
|
||||
node_example_lint:
|
||||
name: "Node.js example"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
run: "pnpm install"
|
||||
|
||||
- name: Build Types
|
||||
run: "yarn build:types"
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
with:
|
||||
cache: "npm"
|
||||
node-version-file: "examples/node/package.json"
|
||||
# cache-dependency-path: '**/package-lock.json'
|
||||
run: "pnpm build:types"
|
||||
|
||||
- name: Install Example Deps
|
||||
run: "npm install"
|
||||
@@ -82,39 +85,50 @@ jobs:
|
||||
workflow_lint:
|
||||
name: "Workflow Lint"
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
security-events: write
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: Run Linter
|
||||
run: "yarn lint:workflows"
|
||||
run: "pnpm lint:workflows"
|
||||
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
|
||||
|
||||
docs:
|
||||
name: "JSDoc Checker"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
run: "pnpm install"
|
||||
|
||||
- name: Generate Docs
|
||||
run: "yarn run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
|
||||
run: "pnpm run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: docs
|
||||
path: _docs
|
||||
@@ -125,31 +139,36 @@ jobs:
|
||||
name: "Analyse Dead Code"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: Run linter
|
||||
run: "yarn run lint:knip"
|
||||
run: "pnpm run lint:knip"
|
||||
|
||||
element-web:
|
||||
name: Downstream tsc element-web
|
||||
if: github.event_name == 'merge_group'
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: element-hq/element-web
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Install Dependencies
|
||||
@@ -159,15 +178,22 @@ jobs:
|
||||
JS_SDK_GITHUB_BASE_REF: ${{ github.sha }}
|
||||
|
||||
- name: Typecheck
|
||||
run: "yarn run lint:types"
|
||||
working-directory: apps/web
|
||||
run: "pnpm run lint:types"
|
||||
|
||||
# Hook for branch protection to skip downstream typechecking outside of merge queues
|
||||
downstream:
|
||||
name: Downstream Typescript Syntax Check
|
||||
# Workflow consolidation job
|
||||
done:
|
||||
needs:
|
||||
- ts_lint
|
||||
- js_lint
|
||||
- node_example_lint
|
||||
- workflow_lint
|
||||
- docs
|
||||
- analyse_dead_code
|
||||
- element-web
|
||||
name: Static Analysis
|
||||
runs-on: ubuntu-24.04
|
||||
if: always()
|
||||
needs:
|
||||
- element-web
|
||||
steps:
|
||||
- if: needs.element-web.result != 'skipped' && needs.element-web.result != 'success'
|
||||
- if: contains(needs.*.result , 'failure') || contains(needs.*.result, 'cancelled')
|
||||
run: exit 1
|
||||
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||
jobs:
|
||||
sync-labels:
|
||||
uses: element-hq/element-meta/.github/workflows/sync-labels.yml@develop
|
||||
uses: element-hq/element-meta/.github/workflows/sync-labels.yml@7f2f93fb9b52ece7a0998f60e64862aa203c1746
|
||||
with:
|
||||
LABELS: |
|
||||
element-hq/element-meta
|
||||
|
||||
+26
-24
@@ -12,8 +12,8 @@ env:
|
||||
ENABLE_COVERAGE: ${{ github.event_name != 'merge_group' }}
|
||||
permissions: {} # No permissions required
|
||||
jobs:
|
||||
jest:
|
||||
name: "Jest [${{ matrix.specs }}] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"
|
||||
test:
|
||||
name: "Vitest [${{ matrix.specs }}] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
@@ -22,17 +22,20 @@ jobs:
|
||||
node: ["lts/*", 22]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- name: Setup Node
|
||||
id: setupNode
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: "yarn install"
|
||||
run: "pnpm install"
|
||||
|
||||
- name: Get number of CPU cores
|
||||
id: cpu-cores
|
||||
@@ -40,24 +43,23 @@ jobs:
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
yarn test \
|
||||
--coverage=${{ env.ENABLE_COVERAGE }} \
|
||||
--ci \
|
||||
--max-workers ${{ steps.cpu-cores.outputs.count }} \
|
||||
pnpm test \
|
||||
--coverage=${ENABLE_COVERAGE} \
|
||||
--maxWorkers ${NUM_WORKERS} \
|
||||
./spec/${{ matrix.specs }}
|
||||
env:
|
||||
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
|
||||
|
||||
# tell jest to use coloured output
|
||||
FORCE_COLOR: true
|
||||
SHARD: ${{ matrix.specs }}
|
||||
NUM_WORKERS: ${{ steps.cpu-cores.outputs.count }}
|
||||
|
||||
- name: Move coverage files into place
|
||||
if: env.ENABLE_COVERAGE == 'true'
|
||||
run: mv coverage/lcov.info coverage/${{ steps.setupNode.outputs.node-version }}-${{ matrix.specs }}.lcov.info
|
||||
run: mv coverage/lcov.info coverage/${NODE_VERSION}-${{ matrix.specs }}.lcov.info
|
||||
env:
|
||||
NODE_VERSION: ${{ steps.setupNode.outputs.node-version }}
|
||||
|
||||
- name: Upload Artifact
|
||||
if: env.ENABLE_COVERAGE == 'true'
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: coverage-${{ matrix.specs }}-${{ matrix.node == 'lts/*' && 'lts' || matrix.node }}
|
||||
path: |
|
||||
@@ -65,19 +67,19 @@ jobs:
|
||||
!coverage/lcov-report
|
||||
|
||||
# Dummy completion job to simplify branch protections
|
||||
jest-complete:
|
||||
name: Jest tests
|
||||
needs: jest
|
||||
complete:
|
||||
name: Tests
|
||||
needs: test
|
||||
if: always()
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- if: needs.jest.result != 'skipped' && needs.jest.result != 'success'
|
||||
- if: needs.test.result != 'skipped' && needs.test.result != 'success'
|
||||
run: exit 1
|
||||
|
||||
element-web:
|
||||
name: Downstream test element-web
|
||||
if: github.event_name == 'merge_group'
|
||||
uses: element-hq/element-web/.github/workflows/tests.yml@develop
|
||||
uses: element-hq/element-web/.github/workflows/tests.yml@develop # zizmor: ignore[unpinned-uses]
|
||||
permissions:
|
||||
statuses: write
|
||||
with:
|
||||
@@ -87,8 +89,8 @@ jobs:
|
||||
complement-crypto:
|
||||
name: "Run Complement Crypto tests"
|
||||
if: github.event_name == 'merge_group'
|
||||
permissions: read-all
|
||||
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main
|
||||
permissions: read-all # zizmor: ignore[excessive-permissions]
|
||||
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main # zizmor: ignore[unpinned-uses]
|
||||
with:
|
||||
use_js_sdk: "."
|
||||
|
||||
@@ -116,7 +118,7 @@ jobs:
|
||||
steps:
|
||||
- name: Skip SonarCloud on merge queues
|
||||
if: env.ENABLE_COVERAGE == 'false'
|
||||
uses: guibranco/github-status-action-v2@5530c593759f489bba08272e96986ffc571c1ea1
|
||||
uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
automate-project-columns-next:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/add-to-project@main
|
||||
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
||||
with:
|
||||
project-url: https://github.com/orgs/element-hq/projects/120
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
@@ -6,6 +6,6 @@ on:
|
||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||
jobs:
|
||||
call-triage-labelled:
|
||||
uses: element-hq/element-web/.github/workflows/triage-labelled.yml@develop
|
||||
uses: element-hq/element-web/.github/workflows/triage-labelled.yml@6339bcda15c71d209303b18a06a9b1c021220bf9
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10
|
||||
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10
|
||||
with:
|
||||
operations-per-run: 250
|
||||
days-before-issue-stale: -1
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
/.npmrc
|
||||
/*.log
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
.lock-wscript
|
||||
build/Release
|
||||
|
||||
+124
@@ -1,3 +1,127 @@
|
||||
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
|
||||
|
||||
+1
-1
@@ -117,7 +117,7 @@ checks, so please check back after a few minutes.
|
||||
Your PR should include tests.
|
||||
|
||||
For new user facing features in `matrix-js-sdk`, you
|
||||
must include comprehensive unit tests written in Jest.
|
||||
must include comprehensive unit tests written in Vitest.
|
||||
The existing tests can be found under `spec/unit`
|
||||
|
||||
It's good practice to write tests alongside the code as it ensures the code is testable from
|
||||
|
||||
@@ -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
|
||||
> [Authenticated Media](#authenticated-media) section for information on how to enable support for this.
|
||||
|
||||
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
|
||||
if you do not have it already.
|
||||
Using `pnpm` instead of `npm` is recommended. Please see the pnpm [install
|
||||
guide](https://pnpm.io/installation#using-corepack) if you do not have it already.
|
||||
|
||||
`yarn add matrix-js-sdk`
|
||||
`pnpm add matrix-js-sdk`
|
||||
|
||||
```javascript
|
||||
import * as sdk from "matrix-js-sdk";
|
||||
@@ -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:
|
||||
|
||||
```
|
||||
$ yarn gendoc
|
||||
$ pnpm gendoc
|
||||
$ cd docs
|
||||
$ 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:
|
||||
|
||||
```
|
||||
$ yarn install
|
||||
$ pnpm install
|
||||
```
|
||||
|
||||
## Building
|
||||
@@ -461,17 +461,17 @@ First, you need to pull in the right build tools:
|
||||
To build a browser version from scratch when developing:
|
||||
|
||||
```
|
||||
$ yarn build
|
||||
$ pnpm build
|
||||
```
|
||||
|
||||
To run tests (Jest):
|
||||
To run tests:
|
||||
|
||||
```
|
||||
$ yarn test
|
||||
$ pnpm test
|
||||
```
|
||||
|
||||
To run linting:
|
||||
|
||||
```
|
||||
$ yarn lint
|
||||
$ pnpm lint
|
||||
```
|
||||
|
||||
+2
-10
@@ -7,21 +7,13 @@ module.exports = {
|
||||
targets: {
|
||||
esmodules: true,
|
||||
},
|
||||
// We want to output ES modules for the final build (mostly to ensure that
|
||||
// async imports work correctly). However, jest doesn't support ES modules very
|
||||
// well yet (see https://github.com/jestjs/jest/issues/9430), so we use commonjs
|
||||
// when testing.
|
||||
modules: process.env.NODE_ENV === "test" ? "commonjs" : false,
|
||||
modules: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
"@babel/preset-typescript",
|
||||
{
|
||||
// When using the transpiled javascript in `lib`, Node.js requires `.js` extensions on any `import`
|
||||
// specifiers. However, Jest uses the TS source (via babel) and fails to resolve the `.js` names.
|
||||
// To resolve this,we use the `.ts` names in the source, and rewrite the `import` specifiers to use
|
||||
// `.js` during transpilation, *except* when we are targetting Jest.
|
||||
rewriteImportExtensions: process.env.NODE_ENV !== "test",
|
||||
rewriteImportExtensions: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
+4
-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.
|
||||
|
||||
```typescript
|
||||
let errorMessage: Optional<string>;
|
||||
let errorMessage: string;
|
||||
```
|
||||
|
||||
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.
|
||||
27. `import` should be used instead of `require`, as `require` does not have types.
|
||||
28. Export only what can be reused.
|
||||
29. Prefer a type like `Optional<X>` (`type Optional<T> = T | null | undefined`) instead
|
||||
of truly optional parameters.
|
||||
29. Prefer a type like `X | null` instead of truly optional parameters.
|
||||
1. A notable exception is when the likelihood of a bug is minimal, such as when a function
|
||||
takes an argument that is more often not required than required. An example where the
|
||||
`?` operator is inappropriate is when taking a room ID: typically the caller should
|
||||
@@ -161,7 +160,7 @@ Unless otherwise specified, the following applies to all code:
|
||||
```typescript
|
||||
function doThingWithRoom(
|
||||
thing: string,
|
||||
room: Optional<string>, // require the caller to specify
|
||||
room: string | null, // require the caller to specify
|
||||
) {
|
||||
// ...
|
||||
}
|
||||
@@ -214,7 +213,7 @@ Unless otherwise specified, the following applies to all code:
|
||||
## Tests
|
||||
|
||||
1. Tests must be written in TypeScript.
|
||||
2. Jest mocks are declared below imports, but above everything else.
|
||||
2. Mocks are declared below imports, but above everything else.
|
||||
3. Use the following convention template:
|
||||
|
||||
```typescript
|
||||
|
||||
@@ -21,4 +21,4 @@ export PATH="$rootdir/node_modules/.bin:$PATH"
|
||||
|
||||
# now run our checks
|
||||
cd "$tmpdir"
|
||||
yarn lint
|
||||
pnpm lint
|
||||
|
||||
@@ -1,48 +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",
|
||||
transformIgnorePatterns: ["/node_modules/(?!(uuid|p-retry|is-network-error)).+$"],
|
||||
|
||||
// Always print out a summary if there are any failing tests. Normally
|
||||
// a summary is only printed if there are more than 20 test *suites*.
|
||||
reporters: [["default", { summaryThreshold: 0 }]],
|
||||
};
|
||||
|
||||
// if we're running under GHA, enable the GHA reporter
|
||||
if (env["GITHUB_ACTIONS"] !== undefined) {
|
||||
const reporters: Config["reporters"] = [
|
||||
["github-actions", { silent: false }],
|
||||
// as above: always show a summary if there were any failing tests.
|
||||
["summary", { summaryThreshold: 0 }],
|
||||
];
|
||||
|
||||
// if we're running against the develop branch, also enable the slow test reporter
|
||||
if (env["GITHUB_REF"] == "refs/heads/develop") {
|
||||
reporters.push("<rootDir>/spec/slowReporter.cjs");
|
||||
}
|
||||
config.reporters = reporters;
|
||||
}
|
||||
|
||||
export default config;
|
||||
@@ -1,5 +1,8 @@
|
||||
import { KnipConfig } from "knip";
|
||||
|
||||
// Specify this as knip loads config files which may conditionally add reporters, e.g. `vitest-sonar-reporter'
|
||||
process.env.GITHUB_ACTIONS = "1";
|
||||
|
||||
export default {
|
||||
entry: [
|
||||
"src/index.ts",
|
||||
@@ -28,12 +31,6 @@ export default {
|
||||
"husky",
|
||||
// Used in script which only runs in environment with `@octokit/rest` installed
|
||||
"@octokit/rest",
|
||||
// Used by jest
|
||||
"jest-environment-jsdom",
|
||||
"babel-jest",
|
||||
"ts-node",
|
||||
// Used by `@babel/plugin-transform-runtime`
|
||||
"@babel/runtime",
|
||||
],
|
||||
ignoreBinaries: [
|
||||
// Used when available by reusable workflow `.github/workflows/release-make.yml`
|
||||
|
||||
+45
-39
@@ -1,27 +1,26 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "39.3.0",
|
||||
"version": "41.3.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "yarn build",
|
||||
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||
"clean": "rimraf lib",
|
||||
"build": "yarn clean && yarn build:compile && yarn build:types",
|
||||
"prepare": "pnpm build",
|
||||
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel --delete-dir-on-start src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||
"build": "pnpm build:compile && pnpm build:types",
|
||||
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
|
||||
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
|
||||
"build:compile": "babel --delete-dir-on-start -d lib --verbose --extensions \".ts,.js\" src",
|
||||
"gendoc": "typedoc",
|
||||
"lint": "yarn lint:types && yarn lint:js && yarn lint:workflows",
|
||||
"lint": "pnpm lint:types && pnpm lint:js && pnpm lint:workflows",
|
||||
"lint:js": "eslint --max-warnings 0 src spec && prettier --check .",
|
||||
"lint:js-fix": "prettier --log-level=warn --write . && eslint --fix src spec",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'",
|
||||
"lint:knip": "knip",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"coverage": "yarn test --coverage"
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest watch",
|
||||
"coverage": "pnpm test --coverage"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -49,19 +48,18 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^15.3.0",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^18.1.0",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^6.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"loglevel": "^1.9.2",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-widget-api": "^1.14.0",
|
||||
"matrix-widget-api": "^1.16.1",
|
||||
"oidc-client-ts": "^3.0.1",
|
||||
"p-retry": "7",
|
||||
"p-retry": "8",
|
||||
"sdp-transform": "^3.0.0",
|
||||
"unhomoglyph": "^1.0.6",
|
||||
"uuid": "13"
|
||||
"unhomoglyph": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@action-validator/cli": "^0.6.0",
|
||||
@@ -78,18 +76,19 @@
|
||||
"@babel/plugin-transform-runtime": "^7.12.10",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@casualbot/jest-sonar-reporter": "2.2.7",
|
||||
"@fetch-mock/vitest": "^0.2.18",
|
||||
"@matrix-org/olm": "3.2.15",
|
||||
"@peculiar/webcrypto": "^1.4.5",
|
||||
"@stylistic/eslint-plugin": "^5.0.0",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "18",
|
||||
"@types/node": "22",
|
||||
"@types/sdp-transform": "^2.4.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"babel-jest": "^30.0.0",
|
||||
"@vitest/coverage-v8": "^4.0.17",
|
||||
"@vitest/eslint-plugin": "^1.6.6",
|
||||
"@vitest/ui": "^4.0.17",
|
||||
"babel-plugin-search-and-replace": "^1.1.1",
|
||||
"debug": "^4.3.4",
|
||||
"eslint": "8.57.1",
|
||||
@@ -97,36 +96,43 @@
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-import-resolver-typescript": "^4.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jest": "^29.0.0",
|
||||
"eslint-plugin-jsdoc": "^61.0.0",
|
||||
"eslint-plugin-jsdoc": "^62.0.0",
|
||||
"eslint-plugin-matrix-org": "^3.0.0",
|
||||
"eslint-plugin-n": "^14.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.4.0",
|
||||
"eslint-plugin-tsdoc": "^0.5.0",
|
||||
"eslint-plugin-unicorn": "^56.0.0",
|
||||
"fake-indexeddb": "^5.0.2",
|
||||
"fetch-mock": "11.1.5",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
"fetch-mock": "^12.6.0",
|
||||
"happy-dom": "^20.1.0",
|
||||
"husky": "^9.0.0",
|
||||
"jest": "^30.0.0",
|
||||
"jest-environment-jsdom": "^30.0.0",
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jest-mock": "^30.0.0",
|
||||
"knip": "^5.0.0",
|
||||
"knip": "^6.0.0",
|
||||
"lint-staged": "^16.0.0",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"node-fetch": "^2.7.0",
|
||||
"prettier": "3.6.2",
|
||||
"rimraf": "^6.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"prettier": "3.8.3",
|
||||
"typedoc": "^0.28.1",
|
||||
"typedoc-plugin-coverage": "^4.0.0",
|
||||
"typedoc-plugin-mdn-links": "^5.0.0",
|
||||
"typedoc-plugin-missing-exports": "^4.0.0",
|
||||
"typescript": "^5.4.2"
|
||||
"typescript": "^6.0.0",
|
||||
"vitest": "^4.0.17",
|
||||
"vitest-sonar-reporter": "^3.0.0"
|
||||
},
|
||||
"@casualbot/jest-sonar-reporter": {
|
||||
"outputDirectory": "coverage",
|
||||
"outputName": "jest-sonar-report.xml",
|
||||
"relativePaths": true
|
||||
}
|
||||
"pnpm": {
|
||||
"peerDependencyRules": {
|
||||
"allowedVersions": {
|
||||
"eslint": "8"
|
||||
}
|
||||
},
|
||||
"allowedDeprecatedVersions": {
|
||||
"eslint": "8"
|
||||
},
|
||||
"overrides": {
|
||||
"expect": "30.3.0",
|
||||
"flatted@<=3.4.1": "^3.4.2",
|
||||
"picomatch@>=4.0.0 <4.0.4": "^4.0.4",
|
||||
"yaml@>=2.0.0 <2.8.3": "^2.8.3",
|
||||
"vite": "8.0.8"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
|
||||
}
|
||||
|
||||
Generated
+8353
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
nodeLinker: hoisted
|
||||
@@ -11,6 +11,6 @@ sonar.exclusions=docs,examples,git-hooks
|
||||
sonar.typescript.tsconfigPath=./tsconfig.json
|
||||
sonar.javascript.lcov.reportPaths=coverage/lcov.info
|
||||
sonar.coverage.exclusions=spec/**/*
|
||||
sonar.testExecutionReportPaths=coverage/jest-sonar-report.xml
|
||||
sonar.testExecutionReportPaths=coverage/sonar-report.xml
|
||||
|
||||
sonar.lang.patterns.ts=**/*.ts,**/*.tsx
|
||||
|
||||
+2
-2
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
// `expect` is allowed in helper functions which are called within `test`/`it` blocks
|
||||
/* eslint-disable jest/no-standalone-expect */
|
||||
/* eslint-disable @vitest/no-standalone-expect */
|
||||
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
@@ -39,7 +39,7 @@ import { type ISyncResponder } from "./test-utils/SyncResponder";
|
||||
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
|
||||
*
|
||||
* @deprecated Avoid using this; it is tied too tightly to matrix-mock-request and is generally inconvenient to use.
|
||||
* Instead, construct a MatrixClient manually, use fetch-mock-jest to intercept the HTTP requests, and
|
||||
* Instead, construct a MatrixClient manually, use fetch-mock to intercept the HTTP requests, and
|
||||
* use things like {@link E2EKeyReceiver} and {@link SyncResponder} to manage the requests.
|
||||
*/
|
||||
export class TestClient implements IE2EKeyReceiver, ISyncResponder {
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import debug from "debug";
|
||||
@@ -74,7 +74,7 @@ describe("cross-signing", () => {
|
||||
function createCryptoCallbacks(): CryptoCallbacks {
|
||||
return {
|
||||
getSecretStorageKey: (keys, name) => {
|
||||
return Promise.resolve<[string, Uint8Array]>(["key_id", encryptionKey]);
|
||||
return Promise.resolve<[string, Uint8Array<ArrayBuffer>]>(["key_id", encryptionKey]);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -83,7 +83,6 @@ describe("cross-signing", () => {
|
||||
async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
|
||||
const homeserverUrl = "https://alice-server.com";
|
||||
aliceClient = createClient({
|
||||
@@ -113,8 +112,7 @@ describe("cross-signing", () => {
|
||||
);
|
||||
|
||||
afterEach(async () => {
|
||||
await aliceClient.stopClient();
|
||||
fetchMock.mockReset();
|
||||
aliceClient.stopClient();
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -138,27 +136,25 @@ describe("cross-signing", () => {
|
||||
await bootstrapCrossSigning(authDict);
|
||||
|
||||
// check that the cross-signing keys have been uploaded
|
||||
expect(fetchMock.called("upload-cross-signing-keys")).toBeTruthy();
|
||||
const [, keysOpts] = fetchMock.lastCall("upload-cross-signing-keys")!;
|
||||
expect(fetchMock.callHistory.called("upload-cross-signing-keys")).toBeTruthy();
|
||||
const keysOpts = fetchMock.callHistory.lastCall("upload-cross-signing-keys")!.options;
|
||||
const keysBody = JSON.parse(keysOpts!.body as string);
|
||||
expect(keysBody.auth).toEqual(authDict); // check uia dict was passed
|
||||
// there should be a key of each type
|
||||
// master key is signed by the device
|
||||
expect(keysBody).toHaveProperty(`master_key.signatures.[${TEST_USER_ID}].[ed25519:${TEST_DEVICE_ID}]`);
|
||||
expect(keysBody).toHaveProperty(["master_key", "signatures", TEST_USER_ID, `ed25519:${TEST_DEVICE_ID}`]);
|
||||
const masterKeyId = Object.keys(keysBody.master_key.keys)[0];
|
||||
// ssk and usk are signed by the master key
|
||||
expect(keysBody).toHaveProperty(`self_signing_key.signatures.[${TEST_USER_ID}].[${masterKeyId}]`);
|
||||
expect(keysBody).toHaveProperty(`user_signing_key.signatures.[${TEST_USER_ID}].[${masterKeyId}]`);
|
||||
expect(keysBody).toHaveProperty(["self_signing_key", "signatures", TEST_USER_ID, masterKeyId]);
|
||||
expect(keysBody).toHaveProperty(["user_signing_key", "signatures", TEST_USER_ID, masterKeyId]);
|
||||
const sskId = Object.keys(keysBody.self_signing_key.keys)[0];
|
||||
|
||||
// check the publish call
|
||||
expect(fetchMock.called("upload-sigs")).toBeTruthy();
|
||||
const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!;
|
||||
expect(fetchMock.callHistory.called("upload-sigs")).toBeTruthy();
|
||||
const sigsOpts = fetchMock.callHistory.lastCall("upload-sigs")!.options;
|
||||
const body = JSON.parse(sigsOpts!.body as string);
|
||||
// there should be a signature for our device, by our self-signing key.
|
||||
expect(body).toHaveProperty(
|
||||
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[${sskId}]`,
|
||||
);
|
||||
expect(body).toHaveProperty([TEST_USER_ID, TEST_DEVICE_ID, "signatures", TEST_USER_ID, sskId]);
|
||||
});
|
||||
|
||||
it("get cross signing keys from secret storage and import them", async () => {
|
||||
@@ -237,13 +233,17 @@ describe("cross-signing", () => {
|
||||
expect(await userTrustStatusChangedPromise).toBe(aliceClient.getUserId());
|
||||
|
||||
// Expect the signature to be uploaded
|
||||
expect(fetchMock.called("upload-sigs")).toBeTruthy();
|
||||
const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!;
|
||||
expect(fetchMock.callHistory.called("upload-sigs")).toBeTruthy();
|
||||
const sigsOpts = fetchMock.callHistory.lastCall("upload-sigs")!.options;
|
||||
const body = JSON.parse(sigsOpts!.body as string);
|
||||
// the device should have a signature with the public self cross signing keys.
|
||||
expect(body).toHaveProperty(
|
||||
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[ed25519:${SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64}]`,
|
||||
);
|
||||
expect(body).toHaveProperty([
|
||||
TEST_USER_ID,
|
||||
TEST_DEVICE_ID,
|
||||
"signatures",
|
||||
TEST_USER_ID,
|
||||
`ed25519:${SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64}`,
|
||||
]);
|
||||
});
|
||||
|
||||
it("can bootstrapCrossSigning twice", async () => {
|
||||
@@ -255,8 +255,7 @@ describe("cross-signing", () => {
|
||||
// a second call should do nothing except GET requests
|
||||
fetchMock.mockClear();
|
||||
await bootstrapCrossSigning(authDict);
|
||||
const calls = fetchMock.calls((url, opts) => opts.method != "GET");
|
||||
expect(calls.length).toEqual(0);
|
||||
expect(fetchMock).toHaveFetchedTimes(0, "unmatched");
|
||||
});
|
||||
|
||||
it("will upload existing cross-signing keys to an established secret storage", async () => {
|
||||
@@ -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
|
||||
// a *different* device. Then, when we call `bootstrapCrossSigning` again, it should do the honours.
|
||||
|
||||
mockSetupCrossSigningRequests();
|
||||
const accountDataAccumulator = new AccountDataAccumulator(syncResponder);
|
||||
accountDataAccumulator.interceptGetAccountData();
|
||||
|
||||
@@ -282,7 +280,7 @@ describe("cross-signing", () => {
|
||||
});
|
||||
|
||||
// Prepare for the cross-signing keys
|
||||
const p = accountDataAccumulator.interceptSetAccountData(":type(m.cross_signing..*)");
|
||||
const p = accountDataAccumulator.waitForAccountData("m.cross_signing.master");
|
||||
|
||||
await bootstrapCrossSigning(authDict);
|
||||
await p;
|
||||
@@ -403,7 +401,7 @@ describe("cross-signing", () => {
|
||||
const isCrossSigningReady = await aliceClient.getCrypto()!.isCrossSigningReady();
|
||||
|
||||
expect(isCrossSigningReady).toBeFalsy();
|
||||
});
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
describe("getCrossSigningKeyId", () => {
|
||||
@@ -415,22 +413,13 @@ describe("cross-signing", () => {
|
||||
*/
|
||||
function awaitCrossSigningKeysUpload() {
|
||||
return new Promise<any>((resolve) => {
|
||||
fetchMock.post(
|
||||
{
|
||||
url: new URL(
|
||||
"/_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);
|
||||
fetchMock.modifyRoute("upload-cross-signing-keys", {
|
||||
response: (callLog) => {
|
||||
const content = JSON.parse(callLog.options.body as string);
|
||||
resolve(content);
|
||||
return {};
|
||||
},
|
||||
// Override the route defined in E2EKeyReceiver
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -461,9 +450,6 @@ describe("cross-signing", () => {
|
||||
|
||||
describe("crossSignDevice", () => {
|
||||
beforeEach(async () => {
|
||||
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
// make sure that there is another device which we can sign
|
||||
e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
|
||||
|
||||
@@ -477,10 +463,6 @@ describe("cross-signing", () => {
|
||||
expect(devices.get(aliceClient.getSafeUserId())!.has(testData.TEST_DEVICE_ID)).toBeTruthy();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("fails for an unknown device", async () => {
|
||||
await expect(aliceClient.getCrypto()!.crossSignDevice("unknown")).rejects.toThrow("Unknown device");
|
||||
});
|
||||
@@ -493,9 +475,9 @@ describe("cross-signing", () => {
|
||||
await aliceClient.getCrypto()!.crossSignDevice(testData.TEST_DEVICE_ID);
|
||||
|
||||
// check that a sig for the device was uploaded
|
||||
const calls = fetchMock.calls("upload-sigs");
|
||||
const calls = fetchMock.callHistory.calls("upload-sigs");
|
||||
expect(calls.length).toEqual(1);
|
||||
const body = JSON.parse(calls[0][1]!.body as string);
|
||||
const body = JSON.parse(calls[0].options!.body as string);
|
||||
const deviceSig = body[aliceClient.getSafeUserId()][testData.TEST_DEVICE_ID];
|
||||
expect(deviceSig).toHaveProperty("signatures");
|
||||
});
|
||||
|
||||
+203
-129
@@ -16,12 +16,12 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import anotherjson from "another-json";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import Olm from "@matrix-org/olm";
|
||||
import { type RouteResponse } from "fetch-mock";
|
||||
|
||||
import type FetchMock from "fetch-mock";
|
||||
import * as testUtils from "../../test-utils/test-utils";
|
||||
import {
|
||||
emitPromise,
|
||||
@@ -86,6 +86,7 @@ import {
|
||||
encryptGroupSessionKey,
|
||||
encryptMegolmEvent,
|
||||
encryptMegolmEventRawPlainText,
|
||||
encryptOlmEvent,
|
||||
establishOlmSession,
|
||||
getTestOlmAccountKeys,
|
||||
expectSendRoomKey,
|
||||
@@ -104,7 +105,7 @@ afterEach(() => {
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = new IDBFactory();
|
||||
|
||||
jest.useRealTimers();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe("crypto", () => {
|
||||
@@ -154,13 +155,8 @@ describe("crypto", () => {
|
||||
return response;
|
||||
}
|
||||
const rootRegexp = escapeRegExp(new URL("/_matrix/client/", aliceClient.getHomeserverUrl()).toString());
|
||||
fetchMock.postOnce(
|
||||
new RegExp(rootRegexp + "(r0|v3)/keys/query"),
|
||||
(url: string, opts: RequestInit) => onQueryRequest(JSON.parse(opts.body as string)),
|
||||
{
|
||||
// append to the list of intercepts on this path
|
||||
overwriteRoutes: false,
|
||||
},
|
||||
fetchMock.postOnce(new RegExp(rootRegexp + "(r0|v3)/keys/query"), (callLog) =>
|
||||
onQueryRequest(JSON.parse(callLog.options.body as string)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -170,7 +166,7 @@ describe("crypto", () => {
|
||||
* @param response - the response to return from the request. Normally an {@link IClaimOTKsResult}
|
||||
* (or a function that returns one).
|
||||
*/
|
||||
function expectAliceKeyClaim(response: FetchMock.MockResponse | FetchMock.MockResponseFunction) {
|
||||
function expectAliceKeyClaim(response: RouteResponse) {
|
||||
const rootRegexp = escapeRegExp(new URL("/_matrix/client/", aliceClient.getHomeserverUrl()).toString());
|
||||
fetchMock.postOnce(new RegExp(rootRegexp + "(r0|v3)/keys/claim"), response);
|
||||
}
|
||||
@@ -223,15 +219,20 @@ describe("crypto", () => {
|
||||
*/
|
||||
function createCryptoCallbacks(): CryptoCallbacks {
|
||||
// Store the cached secret storage key and return it when `getSecretStorageKey` is called
|
||||
let cachedKey: { keyId: string; key: Uint8Array };
|
||||
const cacheSecretStorageKey = (keyId: string, keyInfo: SecretStorageKeyDescription, key: Uint8Array) => {
|
||||
let cachedKey: { keyId: string; key: Uint8Array<ArrayBuffer> };
|
||||
const cacheSecretStorageKey = (
|
||||
keyId: string,
|
||||
keyInfo: SecretStorageKeyDescription,
|
||||
key: Uint8Array<ArrayBuffer>,
|
||||
) => {
|
||||
cachedKey = {
|
||||
keyId,
|
||||
key,
|
||||
};
|
||||
};
|
||||
|
||||
const getSecretStorageKey = () => Promise.resolve<[string, Uint8Array]>([cachedKey.keyId, cachedKey.key]);
|
||||
const getSecretStorageKey = () =>
|
||||
Promise.resolve<[string, Uint8Array<ArrayBuffer>]>([cachedKey.keyId, cachedKey.key]);
|
||||
|
||||
return {
|
||||
cacheSecretStorageKey,
|
||||
@@ -243,7 +244,6 @@ describe("crypto", () => {
|
||||
async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
|
||||
const homeserverUrl = "https://alice-server.com";
|
||||
aliceClient = createClient({
|
||||
@@ -265,18 +265,20 @@ describe("crypto", () => {
|
||||
testOlmAccount = await createOlmAccount();
|
||||
const testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
|
||||
testSenderKey = testE2eKeys.curve25519;
|
||||
|
||||
vi.useRealTimers();
|
||||
},
|
||||
/* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */
|
||||
10000,
|
||||
);
|
||||
|
||||
afterEach(async () => {
|
||||
await aliceClient.stopClient();
|
||||
aliceClient.stopClient();
|
||||
|
||||
// Allow in-flight things to complete before we tear down the test
|
||||
await jest.runAllTimersAsync();
|
||||
|
||||
fetchMock.mockReset();
|
||||
if (vi.isFakeTimers()) {
|
||||
await vi.runAllTimersAsync();
|
||||
}
|
||||
});
|
||||
|
||||
it("MatrixClient.getCrypto returns a CryptoApi", () => {
|
||||
@@ -343,7 +345,7 @@ describe("crypto", () => {
|
||||
|
||||
describe("Unable to decrypt error codes", function () {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
it("Decryption fails with UISI error", async () => {
|
||||
@@ -719,7 +721,7 @@ describe("crypto", () => {
|
||||
syncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
await awaitDecryptionError;
|
||||
await expect(awaitDecryptionError).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -870,6 +872,7 @@ describe("crypto", () => {
|
||||
await expectSendRoomKey("@bob:xyz", testOlmAccount);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("Alice sends a megolm message", async () => {
|
||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
@@ -897,6 +900,7 @@ describe("crypto", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("We should start a new megolm session after forceDiscardSession", async () => {
|
||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
@@ -997,9 +1001,7 @@ describe("crypto", () => {
|
||||
await startClientAndAwaitFirstSync();
|
||||
const p2pSession = await establishOlmSession(aliceClient, keyReceiver, syncResponder, testOlmAccount);
|
||||
|
||||
// We need to fake the timers to advance the time, but the wasm bindings of matrix-sdk-crypto rely on a
|
||||
// working `queueMicrotask`
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
vi.useFakeTimers();
|
||||
|
||||
const syncResponse = getSyncResponse(["@bob:xyz"]);
|
||||
|
||||
@@ -1031,7 +1033,7 @@ describe("crypto", () => {
|
||||
expect(sessionId).toBeDefined();
|
||||
|
||||
// Advance the time by 1h
|
||||
jest.advanceTimersByTime(oneHourInMs);
|
||||
vi.advanceTimersByTime(oneHourInMs);
|
||||
|
||||
// Send a second message to bob and get the encrypted message
|
||||
const [secondEncryptedMessage] = await Promise.all([
|
||||
@@ -1152,7 +1154,7 @@ describe("crypto", () => {
|
||||
// it probably won't be decrypted yet, because it takes a while to process the olm keys
|
||||
const decryptedEvent = await testUtils.awaitDecryption(event, { waitOnDecryptionFailure: true });
|
||||
expect(decryptedEvent.getRoomId()).toEqual(ROOM_ID);
|
||||
expect(decryptedEvent.getContent()).toEqual({});
|
||||
expect(decryptedEvent.getContent<IContent>()).toEqual({});
|
||||
expect(decryptedEvent.getClearContent()).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -1176,20 +1178,12 @@ describe("crypto", () => {
|
||||
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount);
|
||||
|
||||
// ... and finally, send the room key. We block the response until `sendRoomMessageDefer` completes.
|
||||
const sendRoomMessageResolvers = Promise.withResolvers<FetchMock.MockResponse>();
|
||||
const sendRoomMessageResolvers = Promise.withResolvers<RouteResponse>();
|
||||
const reqProm = new Promise<IContent>((resolve) => {
|
||||
fetchMock.putOnce(
|
||||
new RegExp("/send/m.room.encrypted/"),
|
||||
async (url: string, opts: RequestInit): Promise<FetchMock.MockResponse> => {
|
||||
resolve(JSON.parse(opts.body as string));
|
||||
return await sendRoomMessageResolvers.promise;
|
||||
},
|
||||
{
|
||||
// append to the list of intercepts on this path (since we have some tests that call
|
||||
// this function multiple times)
|
||||
overwriteRoutes: false,
|
||||
},
|
||||
);
|
||||
fetchMock.putOnce(new RegExp("/send/m.room.encrypted/"), async (callLog): Promise<RouteResponse> => {
|
||||
resolve(JSON.parse(callLog.options.body as string));
|
||||
return await sendRoomMessageResolvers.promise;
|
||||
});
|
||||
});
|
||||
|
||||
// Now we start to send the message
|
||||
@@ -1272,6 +1266,7 @@ describe("crypto", () => {
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("Sending an event initiates a member list sync", async () => {
|
||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
@@ -1295,6 +1290,7 @@ describe("crypto", () => {
|
||||
await Promise.all([sendPromise, megolmMessagePromise, memberListPromise]);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("loading the membership list inhibits a later load", async () => {
|
||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
@@ -1404,33 +1400,26 @@ describe("crypto", () => {
|
||||
|
||||
describe("key upload request", () => {
|
||||
beforeEach(() => {
|
||||
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
function awaitKeyUploadRequest(): Promise<{ keysCount: number; fallbackKeysCount: number }> {
|
||||
return new Promise((resolve) => {
|
||||
const listener = (url: string, options: RequestInit) => {
|
||||
const content = JSON.parse(options.body as string);
|
||||
const keysCount = Object.keys(content?.one_time_keys || {}).length;
|
||||
const fallbackKeysCount = Object.keys(content?.fallback_keys || {}).length;
|
||||
if (keysCount) resolve({ keysCount, fallbackKeysCount });
|
||||
return {
|
||||
one_time_key_counts: {
|
||||
// The matrix client does `/upload` requests until 50 keys are uploaded
|
||||
// We return here 60 to avoid the `/upload` request loop
|
||||
signed_curve25519: keysCount ? 60 : keysCount,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
for (const path of ["/_matrix/client/v3/keys/upload", "/_matrix/client/v3/keys/upload"]) {
|
||||
fetchMock.post(new URL(path, aliceClient.getHomeserverUrl()).toString(), listener, {
|
||||
// These routes are already defined in the E2EKeyReceiver
|
||||
// We want to overwrite the behaviour of the E2EKeyReceiver
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
}
|
||||
fetchMock.modifyRoute("keys-upload", {
|
||||
response: (callLog) => {
|
||||
const content = JSON.parse(callLog.options.body as string);
|
||||
const keysCount = Object.keys(content?.one_time_keys || {}).length;
|
||||
const fallbackKeysCount = Object.keys(content?.fallback_keys || {}).length;
|
||||
if (keysCount) resolve({ keysCount, fallbackKeysCount });
|
||||
return {
|
||||
one_time_key_counts: {
|
||||
// The matrix client does `/upload` requests until 50 keys are uploaded
|
||||
// We return here 60 to avoid the `/upload` request loop
|
||||
signed_curve25519: keysCount ? 60 : keysCount,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1443,7 +1432,7 @@ describe("crypto", () => {
|
||||
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// Verify that `/upload` is called on Alice's homesever
|
||||
// Verify that `/upload` is called on Alice's homeserver
|
||||
const { keysCount, fallbackKeysCount } = await uploadPromise;
|
||||
expect(keysCount).toBeGreaterThan(0);
|
||||
expect(fallbackKeysCount).toBe(0);
|
||||
@@ -1457,7 +1446,7 @@ describe("crypto", () => {
|
||||
|
||||
// Advance local date to 2 minutes
|
||||
// The old crypto only runs the upload every 60 seconds
|
||||
jest.setSystemTime(Date.now() + 2 * 60 * 1000);
|
||||
vi.setSystemTime(Date.now() + 2 * 60 * 1000);
|
||||
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
@@ -1548,18 +1537,16 @@ describe("crypto", () => {
|
||||
|
||||
function awaitKeyQueryRequest(): Promise<Record<string, []>> {
|
||||
return new Promise((resolve) => {
|
||||
const listener = (url: string, options: RequestInit) => {
|
||||
const content = JSON.parse(options.body as string);
|
||||
// Resolve with request payload
|
||||
resolve(content.device_keys);
|
||||
|
||||
// Return response of `/keys/query`
|
||||
return queryResponseBody;
|
||||
};
|
||||
|
||||
fetchMock.post(
|
||||
new URL("/_matrix/client/v3/keys/query", aliceClient.getHomeserverUrl()).toString(),
|
||||
listener,
|
||||
(callLog) => {
|
||||
const content = JSON.parse(callLog.options.body as string);
|
||||
// Resolve with request payload
|
||||
resolve(content.device_keys);
|
||||
|
||||
// Return response of `/keys/query`
|
||||
return queryResponseBody;
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1598,8 +1585,7 @@ describe("crypto", () => {
|
||||
});
|
||||
|
||||
it("Get devices from tracked users", async () => {
|
||||
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
vi.useFakeTimers();
|
||||
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
@@ -1611,12 +1597,12 @@ describe("crypto", () => {
|
||||
|
||||
// Advance local date to 2 minutes
|
||||
// The old crypto only runs the upload every 60 seconds
|
||||
jest.setSystemTime(Date.now() + 2 * 60 * 1000);
|
||||
vi.setSystemTime(Date.now() + 2 * 60 * 1000);
|
||||
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// Old crypto: for alice: run over the `sleep(5)` in `doQueuedQueries` of `DeviceList`
|
||||
jest.runAllTimers();
|
||||
vi.runAllTimers();
|
||||
// Old crypto: for alice: run the `processQueryResponseForUser` in `doQueuedQueries` of `DeviceList`
|
||||
await flushPromises();
|
||||
|
||||
@@ -1624,7 +1610,7 @@ describe("crypto", () => {
|
||||
await queryPromise;
|
||||
|
||||
// Old crypto: for `user`: run over the `sleep(5)` in `doQueuedQueries` of `DeviceList`
|
||||
jest.runAllTimers();
|
||||
vi.runAllTimers();
|
||||
// Old crypto: for `user`: run the `processQueryResponseForUser` in `doQueuedQueries` of `DeviceList`
|
||||
// It will add `@testing_florian1:matrix.org` devices to the DeviceList
|
||||
await flushPromises();
|
||||
@@ -1648,7 +1634,7 @@ describe("crypto", () => {
|
||||
* Create a fake secret storage key
|
||||
* Async because `bootstrapSecretStorage` expect an async method
|
||||
*/
|
||||
const createSecretStorageKey = jest.fn().mockResolvedValue({
|
||||
const createSecretStorageKey = vi.fn().mockResolvedValue({
|
||||
keyInfo: {}, // Returning undefined here used to cause a crash
|
||||
privateKey: Uint8Array.of(32, 33),
|
||||
});
|
||||
@@ -1666,7 +1652,7 @@ describe("crypto", () => {
|
||||
* https://spec.matrix.org/v1.6/client-server-api/#put_matrixclientv3useruseridaccount_datatype
|
||||
*/
|
||||
async function awaitCrossSigningKeyUpload(key: string): Promise<Record<string, {}>> {
|
||||
const content = await accountDataAccumulator.interceptSetAccountData(`m.cross_signing.${key}`);
|
||||
const content = await accountDataAccumulator.waitForAccountData(`m.cross_signing.${key}`);
|
||||
return content.encrypted;
|
||||
}
|
||||
|
||||
@@ -1678,10 +1664,7 @@ describe("crypto", () => {
|
||||
async function awaitSecretStorageKeyStoredInAccountData(): Promise<string> {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const content = await accountDataAccumulator.interceptSetAccountData(":type(m.secret_storage.*)", {
|
||||
repeat: 1,
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
const content = await accountDataAccumulator.waitForAccountData("m.secret_storage.*");
|
||||
if (content.key) {
|
||||
return content.key;
|
||||
}
|
||||
@@ -1689,10 +1672,7 @@ describe("crypto", () => {
|
||||
}
|
||||
|
||||
async function awaitMegolmBackupKeyUpload(): Promise<Record<string, {}>> {
|
||||
const content = await accountDataAccumulator.interceptSetAccountData("m.megolm_backup.v1", {
|
||||
repeat: 1,
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
const content = await accountDataAccumulator.waitForAccountData("m.megolm_backup.v1");
|
||||
return content.encrypted;
|
||||
}
|
||||
|
||||
@@ -1713,7 +1693,6 @@ describe("crypto", () => {
|
||||
* @param backupVersion - The version of the created backup
|
||||
*/
|
||||
async function bootstrapSecurity(backupVersion: string): Promise<void> {
|
||||
mockSetupCrossSigningRequests();
|
||||
mockSetupMegolmBackupRequests(backupVersion);
|
||||
|
||||
// promise which will resolve when a `KeyBackupStatus` event is emitted with `enabled: true`
|
||||
@@ -1792,6 +1771,7 @@ describe("crypto", () => {
|
||||
|
||||
it("Should create a 4S key", async () => {
|
||||
accountDataAccumulator.interceptGetAccountData();
|
||||
accountDataAccumulator.interceptSetAccountData();
|
||||
|
||||
const awaitAccountData = awaitAccountDataUpdate("m.secret_storage.default_key");
|
||||
|
||||
@@ -1943,8 +1923,7 @@ describe("crypto", () => {
|
||||
|
||||
describe("Manage Key Backup", () => {
|
||||
beforeEach(async () => {
|
||||
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
it("Should be able to restore from 4S after bootstrap", async () => {
|
||||
@@ -1961,29 +1940,23 @@ describe("crypto", () => {
|
||||
const newKey = testData.MEGOLM_SESSION_DATA;
|
||||
|
||||
const awaitKeyUploaded = new Promise<KeyBackup>((resolve) => {
|
||||
fetchMock.put(
|
||||
"path:/_matrix/client/v3/room_keys/keys",
|
||||
(url, request) => {
|
||||
const uploadPayload: KeyBackup = JSON.parse((request.body as string) ?? "{}");
|
||||
resolve(uploadPayload);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
count: 1,
|
||||
etag: "abcdefg",
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
);
|
||||
fetchMock.put("path:/_matrix/client/v3/room_keys/keys", (callLog) => {
|
||||
const uploadPayload: KeyBackup = JSON.parse((callLog.options.body as string) ?? "{}");
|
||||
resolve(uploadPayload);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
count: 1,
|
||||
etag: "abcdefg",
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
await aliceClient.getCrypto()!.importRoomKeys([newKey]);
|
||||
|
||||
// The backup loop waits a random amount of time to avoid different clients firing at the same time.
|
||||
jest.runAllTimers();
|
||||
vi.runAllTimers();
|
||||
|
||||
const keyBackupData = await awaitKeyUploaded;
|
||||
|
||||
@@ -2011,40 +1984,33 @@ describe("crypto", () => {
|
||||
fetchMock.delete(
|
||||
"express:/_matrix/client/v3/room_keys/version/:version",
|
||||
(url: string, options: RequestInit) => {
|
||||
fetchMock.get(
|
||||
"path:/_matrix/client/v3/room_keys/version",
|
||||
{
|
||||
fetchMock.modifyRoute("room-keys-version", {
|
||||
response: {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
});
|
||||
resolve();
|
||||
return {};
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
});
|
||||
|
||||
const newVersion = "2";
|
||||
fetchMock.post(
|
||||
"path:/_matrix/client/v3/room_keys/version",
|
||||
(url, request) => {
|
||||
const backupData: KeyBackupInfo = JSON.parse((request.body as string) ?? "{}");
|
||||
fetchMock.modifyRoute("post-room-keys-version", {
|
||||
response: (callLog) => {
|
||||
const backupData: KeyBackupInfo = JSON.parse((callLog.options.body as string) ?? "{}");
|
||||
backupData.version = newVersion;
|
||||
backupData.count = 0;
|
||||
backupData.etag = "zer";
|
||||
|
||||
// update get call with new version
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
fetchMock.modifyRoute("room-keys-version", { response: backupData });
|
||||
return {
|
||||
version: backupVersion,
|
||||
};
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
});
|
||||
|
||||
const newBackupStatusUpdate = new Promise<void>((resolve) => {
|
||||
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
|
||||
@@ -2099,6 +2065,7 @@ describe("crypto", () => {
|
||||
expect(hasCrossSigningKeysForUser).toBe(true);
|
||||
|
||||
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
|
||||
expect(verificationStatus.known).toBe(false); // We haven't actually stashed a copy of Alice's identity
|
||||
expect(verificationStatus.isVerified()).toBe(false);
|
||||
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
||||
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
|
||||
@@ -2106,15 +2073,15 @@ describe("crypto", () => {
|
||||
});
|
||||
|
||||
it("Cross signing keys are available for a tracked user", async () => {
|
||||
// Process Alice keys, old crypto has a sleep(5ms) during the process
|
||||
await jest.advanceTimersByTimeAsync(5);
|
||||
// Process Alice keys
|
||||
await flushPromises();
|
||||
|
||||
// Alice is the local user and should be tracked !
|
||||
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys(TEST_USER_ID);
|
||||
expect(hasCrossSigningKeysForUser).toBe(true);
|
||||
|
||||
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
|
||||
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(TEST_USER_ID);
|
||||
expect(verificationStatus.known).toBe(true);
|
||||
expect(verificationStatus.isVerified()).toBe(false);
|
||||
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
||||
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
|
||||
@@ -2125,7 +2092,8 @@ describe("crypto", () => {
|
||||
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys("@unknown:xyz");
|
||||
expect(hasCrossSigningKeysForUser).toBe(false);
|
||||
|
||||
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
|
||||
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus("@unknown:xyz");
|
||||
expect(verificationStatus.known).toBe(false);
|
||||
expect(verificationStatus.isVerified()).toBe(false);
|
||||
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
||||
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
|
||||
@@ -2155,6 +2123,7 @@ describe("crypto", () => {
|
||||
|
||||
{
|
||||
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
|
||||
expect(verificationStatus.known).toBe(true);
|
||||
expect(verificationStatus.isVerified()).toBe(false);
|
||||
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
||||
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
|
||||
@@ -2195,6 +2164,7 @@ describe("crypto", () => {
|
||||
client2?.stopClient();
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
test("Sending a message in a room where the server is hiding the state event does not send a plaintext event", async () => {
|
||||
// Alice is in an encrypted room
|
||||
const encryptionState = mkEncryptionEvent({ algorithm: "m.megolm.v1.aes-sha2" });
|
||||
@@ -2252,6 +2222,7 @@ describe("crypto", () => {
|
||||
expect(msg3Content.session_id).not.toEqual(msg1Content.session_id);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
test("Changes to the rotation period should be ignored after a client restart", async () => {
|
||||
// Alice is in an encrypted room, where the rotation period is set to 2 messages
|
||||
const encryptionState = mkEncryptionEvent({ algorithm: "m.megolm.v1.aes-sha2", rotation_period_msgs: 2 });
|
||||
@@ -2345,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 fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import { type CallLog } from "fetch-mock";
|
||||
import debug from "debug";
|
||||
|
||||
import { ClientEvent, createClient, DebugLogger, type MatrixClient, MatrixEvent } from "../../../src";
|
||||
@@ -28,7 +29,7 @@ import { emitPromise, EventCounter } from "../../test-utils/test-utils";
|
||||
|
||||
describe("Device dehydration", () => {
|
||||
it("should rehydrate and dehydrate a device", async () => {
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
vi.useFakeTimers();
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
@@ -59,28 +60,35 @@ describe("Device dehydration", () => {
|
||||
});
|
||||
|
||||
const crypto = matrixClient.getCrypto()!;
|
||||
fetchMock.config.overwriteRoutes = true;
|
||||
|
||||
// start dehydration -- we start with no dehydrated device, and we
|
||||
// store the dehydrated device that we create
|
||||
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "Not found",
|
||||
fetchMock.get(
|
||||
"path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",
|
||||
{
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "Not found",
|
||||
},
|
||||
},
|
||||
});
|
||||
{ name: "get-dehydrated-device" },
|
||||
);
|
||||
let dehydratedDeviceBody: any;
|
||||
let dehydrationCount = 0;
|
||||
let resolveDehydrationPromise: () => void;
|
||||
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", (_, opts) => {
|
||||
dehydratedDeviceBody = JSON.parse(opts.body as string);
|
||||
dehydrationCount++;
|
||||
if (resolveDehydrationPromise) {
|
||||
resolveDehydrationPromise();
|
||||
}
|
||||
return {};
|
||||
});
|
||||
fetchMock.put(
|
||||
"path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",
|
||||
(callLog) => {
|
||||
dehydratedDeviceBody = JSON.parse(callLog.options.body as string);
|
||||
dehydrationCount++;
|
||||
if (resolveDehydrationPromise) {
|
||||
resolveDehydrationPromise();
|
||||
}
|
||||
return {};
|
||||
},
|
||||
{ name: "put-dehydrated-device" },
|
||||
);
|
||||
await crypto.startDehydration();
|
||||
|
||||
expect(dehydrationCount).toEqual(1);
|
||||
@@ -91,7 +99,7 @@ describe("Device dehydration", () => {
|
||||
const dehydrationPromise = new Promise<void>((resolve, reject) => {
|
||||
resolveDehydrationPromise = resolve;
|
||||
});
|
||||
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
|
||||
vi.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
|
||||
await dehydrationPromise;
|
||||
|
||||
expect(dehydrationKeyCachedEventCounter.counter).toEqual(1);
|
||||
@@ -101,16 +109,18 @@ describe("Device dehydration", () => {
|
||||
// restart dehydration -- rehydrate the device that we created above,
|
||||
// and create a new dehydrated device. We also set `createNewKey`, so
|
||||
// a new dehydration key will be set
|
||||
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
|
||||
device_id: dehydratedDeviceBody.device_id,
|
||||
device_data: dehydratedDeviceBody.device_data,
|
||||
fetchMock.modifyRoute("get-dehydrated-device", {
|
||||
response: {
|
||||
device_id: dehydratedDeviceBody.device_id,
|
||||
device_data: dehydratedDeviceBody.device_data,
|
||||
},
|
||||
});
|
||||
const eventsResponse = jest.fn((url, opts) => {
|
||||
const eventsResponse = vi.fn((callLog: CallLog) => {
|
||||
// rehydrating should make two calls to the /events endpoint.
|
||||
// The first time will return a single event, and the second
|
||||
// time will return no events (which will signal to the
|
||||
// rehydration function that it can stop)
|
||||
const body = JSON.parse(opts.body as string);
|
||||
const body = JSON.parse(callLog.options.body as string);
|
||||
const nextBatch = body.next_batch ?? "0";
|
||||
const events = nextBatch === "0" ? [{ sender: "@alice:localhost", type: "m.dummy", content: {} }] : [];
|
||||
return {
|
||||
@@ -135,28 +145,30 @@ describe("Device dehydration", () => {
|
||||
expect(dehydrationKeyCachedEventCounter.counter).toEqual(2);
|
||||
|
||||
// test that if we get an error when we try to rotate, it emits an event
|
||||
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
|
||||
status: 500,
|
||||
body: {
|
||||
errcode: "M_UNKNOWN",
|
||||
error: "Unknown error",
|
||||
fetchMock.modifyRoute("put-dehydrated-device", {
|
||||
response: {
|
||||
status: 500,
|
||||
body: {
|
||||
errcode: "M_UNKNOWN",
|
||||
error: "Unknown error",
|
||||
},
|
||||
},
|
||||
});
|
||||
const rotationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.DehydratedDeviceRotationError);
|
||||
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
|
||||
vi.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
|
||||
await rotationErrorEventPromise;
|
||||
|
||||
// Restart dehydration, but return an error for GET /dehydrated_device so that rehydration fails.
|
||||
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
|
||||
status: 500,
|
||||
body: {
|
||||
errcode: "M_UNKNOWN",
|
||||
error: "Unknown error",
|
||||
fetchMock.modifyRoute("get-dehydrated-device", {
|
||||
response: {
|
||||
status: 500,
|
||||
body: {
|
||||
errcode: "M_UNKNOWN",
|
||||
error: "Unknown error",
|
||||
},
|
||||
},
|
||||
});
|
||||
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", (_, opts) => {
|
||||
return {};
|
||||
});
|
||||
fetchMock.modifyRoute("put-dehydrated-device", { response: { body: {} } });
|
||||
const rehydrationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.RehydrationError);
|
||||
await crypto.startDehydration(true);
|
||||
await rehydrationErrorEventPromise;
|
||||
@@ -182,8 +194,8 @@ async function initializeSecretStorage(
|
||||
const e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
e2eKeyResponder.addKeyReceiver(userId, e2eKeyReceiver);
|
||||
const accountData: Map<string, object> = new Map();
|
||||
fetchMock.get("glob:http://*/_matrix/client/v3/user/*/account_data/*", (url, opts) => {
|
||||
const name = url.split("/").pop()!;
|
||||
fetchMock.get("glob:http://*/_matrix/client/v3/user/*/account_data/*", (callLog) => {
|
||||
const name = callLog.url.split("/").pop()!;
|
||||
const value = accountData.get(name);
|
||||
if (value) {
|
||||
return value;
|
||||
@@ -197,9 +209,9 @@ async function initializeSecretStorage(
|
||||
};
|
||||
}
|
||||
});
|
||||
fetchMock.put("glob:http://*/_matrix/client/v3/user/*/account_data/*", (url, opts) => {
|
||||
const name = url.split("/").pop()!;
|
||||
const value = JSON.parse(opts.body as string);
|
||||
fetchMock.put("glob:http://*/_matrix/client/v3/user/*/account_data/*", (callLog) => {
|
||||
const name = callLog.url.split("/").pop()!;
|
||||
const value = JSON.parse(callLog.options.body as string);
|
||||
accountData.set(name, value);
|
||||
matrixClient.emit(ClientEvent.AccountData, new MatrixEvent({ type: name, content: value }));
|
||||
return {};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,14 +14,15 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import { type Mocked } from "jest-mock";
|
||||
import { type Mocked } from "vitest";
|
||||
|
||||
import {
|
||||
createClient,
|
||||
encodeBase64,
|
||||
type IContent,
|
||||
type ICreateClientOpts,
|
||||
type IEvent,
|
||||
type IMegolmSessionData,
|
||||
@@ -36,7 +37,13 @@ import { advanceTimersUntil, awaitDecryption, syncPromise } from "../../test-uti
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
import { type KeyBackupInfo, type KeyBackupSession } from "../../../src/crypto-api/keybackup";
|
||||
import { flushPromises } from "../../test-utils/flushPromises";
|
||||
import { decodeRecoveryKey, DecryptionFailureCode, CryptoEvent, type CryptoApi } from "../../../src/crypto-api";
|
||||
import {
|
||||
decodeRecoveryKey,
|
||||
DecryptionFailureCode,
|
||||
CryptoEvent,
|
||||
type CryptoApi,
|
||||
DecryptionKeyDoesNotMatchError,
|
||||
} from "../../../src/crypto-api";
|
||||
import { type KeyBackup } from "../../../src/rust-crypto/backup.ts";
|
||||
|
||||
const ROOM_ID = testData.TEST_ROOM_ID;
|
||||
@@ -69,10 +76,11 @@ function mockUploadEmitter(
|
||||
expectedVersion: string,
|
||||
): TypedEventEmitter<MockKeyUploadEvent, MockKeyUploadEventHandlerMap> {
|
||||
const emitter = new TypedEventEmitter();
|
||||
fetchMock.removeRoute("mock-upload-emitter");
|
||||
fetchMock.put(
|
||||
"path:/_matrix/client/v3/room_keys/keys",
|
||||
(url, request) => {
|
||||
const version = new URLSearchParams(new URL(url).search).get("version");
|
||||
(callLog) => {
|
||||
const version = new URLSearchParams(new URL(callLog.url).search).get("version");
|
||||
if (version != expectedVersion) {
|
||||
return {
|
||||
status: 403,
|
||||
@@ -83,7 +91,7 @@ function mockUploadEmitter(
|
||||
},
|
||||
};
|
||||
}
|
||||
const uploadPayload: KeyBackup = JSON.parse((request.body as string) ?? "{}");
|
||||
const uploadPayload: KeyBackup = JSON.parse((callLog.options.body as string) ?? "{}");
|
||||
let count = 0;
|
||||
for (const [roomId, value] of Object.entries(uploadPayload.rooms)) {
|
||||
for (const sessionId of Object.keys(value.sessions)) {
|
||||
@@ -99,9 +107,7 @@ function mockUploadEmitter(
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
{ name: "mock-upload-emitter" },
|
||||
);
|
||||
return emitter;
|
||||
}
|
||||
@@ -117,12 +123,10 @@ describe("megolm-keys backup", () => {
|
||||
let e2eKeyResponder: E2EKeyResponder;
|
||||
|
||||
beforeEach(async () => {
|
||||
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
vi.useFakeTimers();
|
||||
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
|
||||
mockInitialApiRequests(TEST_HOMESERVER_URL);
|
||||
syncResponder = new SyncResponder(TEST_HOMESERVER_URL);
|
||||
@@ -133,15 +137,12 @@ describe("megolm-keys backup", () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (aliceClient !== undefined) {
|
||||
await aliceClient.stopClient();
|
||||
}
|
||||
aliceClient?.stopClient();
|
||||
|
||||
// Allow in-flight things to complete before we tear down the test
|
||||
await jest.runAllTimersAsync();
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
fetchMock.mockReset();
|
||||
jest.restoreAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
async function initTestClient(opts: Partial<ICreateClientOpts> = {}): Promise<MatrixClient> {
|
||||
@@ -207,9 +208,9 @@ describe("megolm-keys backup", () => {
|
||||
);
|
||||
|
||||
it("Alice checks key backups when receiving a message she can't decrypt", async () => {
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id", (url, request) => {
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id", (callLog) => {
|
||||
// check that the version is correct
|
||||
const version = new URLSearchParams(new URL(url).search).get("version");
|
||||
const version = new URLSearchParams(new URL(callLog.url).search).get("version");
|
||||
if (version == "1") {
|
||||
return testData.CURVE25519_KEY_BACKUP_DATA;
|
||||
} else {
|
||||
@@ -237,11 +238,11 @@ describe("megolm-keys backup", () => {
|
||||
|
||||
// Eventually, decryption succeeds.
|
||||
await awaitDecryption(event, { waitOnDecryptionFailure: true });
|
||||
expect(event.getContent()).toEqual(testData.CLEAR_EVENT.content);
|
||||
expect(event.getContent<IContent>()).toEqual(testData.CLEAR_EVENT.content);
|
||||
});
|
||||
|
||||
it("handles error on backup query gracefully", async () => {
|
||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
fetchMock.get(
|
||||
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
|
||||
@@ -253,9 +254,9 @@ describe("megolm-keys backup", () => {
|
||||
syncResponder.sendOrQueueSyncResponse(SYNC_RESPONSE);
|
||||
await flushBackupRequest();
|
||||
|
||||
const calls = fetchMock.calls("getKey");
|
||||
const calls = fetchMock.callHistory.calls("getKey");
|
||||
expect(calls.length).toEqual(1);
|
||||
expect(calls[0][0]).toEqual(EXPECTED_URL);
|
||||
expect(calls[0].url).toEqual(EXPECTED_URL);
|
||||
|
||||
await flushBackupRequest();
|
||||
|
||||
@@ -274,11 +275,11 @@ describe("megolm-keys backup", () => {
|
||||
// Send Alice a message that she won't be able to decrypt
|
||||
syncResponder.sendOrQueueSyncResponse(SYNC_RESPONSE);
|
||||
await flushBackupRequest();
|
||||
const calls = fetchMock.calls("getKey");
|
||||
const calls = fetchMock.callHistory.calls("getKey");
|
||||
expect(calls.length).toEqual(1);
|
||||
expect(calls[0][0]).toEqual(EXPECTED_URL);
|
||||
expect(calls[0].url).toEqual(EXPECTED_URL);
|
||||
|
||||
fetchMock.resetHistory();
|
||||
fetchMock.clearHistory();
|
||||
|
||||
// another message
|
||||
const event2 = { ...testData.ENCRYPTED_EVENT, event_id: "$event2" };
|
||||
@@ -288,7 +289,7 @@ describe("megolm-keys backup", () => {
|
||||
};
|
||||
syncResponder.sendOrQueueSyncResponse(syncResponse2);
|
||||
await flushBackupRequest();
|
||||
expect(fetchMock.calls("getKey").length).toEqual(0);
|
||||
expect(fetchMock.callHistory.calls("getKey").length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -364,9 +365,9 @@ describe("megolm-keys backup", () => {
|
||||
}
|
||||
|
||||
it("Should import full backup in chunks", async function () {
|
||||
const importMockImpl = jest.fn();
|
||||
const importMockImpl = vi.fn();
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
jest.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
|
||||
vi.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
|
||||
|
||||
// We need several rooms with several sessions to test chunking
|
||||
const { response, expectedTotal } = createBackupDownloadResponse([45, 300, 345, 12, 130]);
|
||||
@@ -380,7 +381,7 @@ describe("megolm-keys backup", () => {
|
||||
check!.backupInfo!.version!,
|
||||
);
|
||||
|
||||
const progressCallback = jest.fn();
|
||||
const progressCallback = vi.fn();
|
||||
const result = await aliceCrypto.restoreKeyBackup({
|
||||
progressCallback,
|
||||
});
|
||||
@@ -417,7 +418,7 @@ describe("megolm-keys backup", () => {
|
||||
});
|
||||
|
||||
it("Should continue to process backup if a chunk import fails and report failures", async function () {
|
||||
const importMockImpl = jest
|
||||
const importMockImpl = vi
|
||||
.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
// Fail to import first chunk
|
||||
@@ -427,7 +428,7 @@ describe("megolm-keys backup", () => {
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
jest.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
|
||||
vi.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
|
||||
|
||||
const { response, expectedTotal } = createBackupDownloadResponse([100, 300]);
|
||||
|
||||
@@ -439,7 +440,7 @@ describe("megolm-keys backup", () => {
|
||||
check!.backupInfo!.version!,
|
||||
);
|
||||
|
||||
const progressCallback = jest.fn();
|
||||
const progressCallback = vi.fn();
|
||||
const result = await aliceCrypto.restoreKeyBackup({ progressCallback });
|
||||
|
||||
expect(result.total).toStrictEqual(expectedTotal);
|
||||
@@ -463,13 +464,13 @@ describe("megolm-keys backup", () => {
|
||||
|
||||
it("Should continue if some keys fails to decrypt", async function () {
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
aliceCrypto.importBackedUpRoomKeys = jest.fn();
|
||||
aliceCrypto.importBackedUpRoomKeys = vi.fn();
|
||||
|
||||
const decryptionFailureCount = 2;
|
||||
|
||||
const mockDecryptor = {
|
||||
// DecryptSessions does not reject on decryption failure, but just skip the key
|
||||
decryptSessions: jest.fn().mockImplementation((sessions) => {
|
||||
decryptSessions: vi.fn().mockImplementation((sessions) => {
|
||||
// simulate fail to decrypt 2 keys out of all
|
||||
const decrypted = [];
|
||||
const keys = Object.keys(sessions);
|
||||
@@ -480,11 +481,11 @@ describe("megolm-keys backup", () => {
|
||||
}
|
||||
return decrypted;
|
||||
}),
|
||||
free: jest.fn(),
|
||||
free: vi.fn(),
|
||||
};
|
||||
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
aliceCrypto.getBackupDecryptor = jest.fn().mockResolvedValue(mockDecryptor);
|
||||
aliceCrypto.getBackupDecryptor = vi.fn().mockResolvedValue(mockDecryptor);
|
||||
|
||||
const { response, expectedTotal } = createBackupDownloadResponse([100]);
|
||||
|
||||
@@ -505,17 +506,12 @@ describe("megolm-keys backup", () => {
|
||||
|
||||
it("Should get the decryption key from the secret storage and restore the key backup", async function () {
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
jest.spyOn(aliceCrypto.secretStorage, "get").mockResolvedValue(testData.BACKUP_DECRYPTION_KEY_BASE64);
|
||||
vi.spyOn(aliceCrypto.secretStorage, "get").mockResolvedValue(testData.BACKUP_DECRYPTION_KEY_BASE64);
|
||||
|
||||
const fullBackup = {
|
||||
rooms: {
|
||||
[ROOM_ID]: {
|
||||
sessions: {
|
||||
[testData.MEGOLM_SESSION_DATA.session_id]: testData.CURVE25519_KEY_BACKUP_DATA,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const fullBackup = createFullBackup(
|
||||
testData.MEGOLM_SESSION_DATA.session_id,
|
||||
testData.CURVE25519_KEY_BACKUP_DATA,
|
||||
);
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
|
||||
|
||||
await aliceCrypto.loadSessionBackupPrivateKeyFromSecretStorage();
|
||||
@@ -526,15 +522,44 @@ describe("megolm-keys backup", () => {
|
||||
expect(result.imported).toStrictEqual(1);
|
||||
});
|
||||
|
||||
it("Should throw an error if the decryption key does not match the backup", async function () {
|
||||
// Given the stored backup decryption key does not match the public backup info
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
vi.spyOn(aliceCrypto.secretStorage, "get").mockResolvedValue(testData.BACKUP_DECRYPTION_KEY_BASE64_ALT);
|
||||
|
||||
const fullBackup = createFullBackup(
|
||||
testData.MEGOLM_SESSION_DATA.session_id,
|
||||
testData.CURVE25519_KEY_BACKUP_DATA,
|
||||
);
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
|
||||
|
||||
// When we load that key, we throw because the keys don't match
|
||||
await expect(aliceCrypto.loadSessionBackupPrivateKeyFromSecretStorage()).rejects.toThrow(
|
||||
DecryptionKeyDoesNotMatchError,
|
||||
);
|
||||
});
|
||||
|
||||
it("Should throw an error if the decryption key is not found in cache", async () => {
|
||||
await expect(aliceCrypto.restoreKeyBackup()).rejects.toThrow("No decryption key found in crypto store");
|
||||
});
|
||||
|
||||
function createFullBackup(sessionId: string, data: KeyBackupSession) {
|
||||
return {
|
||||
rooms: {
|
||||
[ROOM_ID]: {
|
||||
sessions: {
|
||||
[sessionId]: data,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
describe("backupLoop", () => {
|
||||
it("Alice should upload known keys when backup is enabled", async function () {
|
||||
// 404 means that there is no active backup
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", 404);
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", 404, { name: "room-keys-version" });
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
@@ -571,8 +596,8 @@ describe("megolm-keys backup", () => {
|
||||
});
|
||||
});
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
|
||||
overwriteRoutes: true,
|
||||
fetchMock.modifyRoute("room-keys-version", {
|
||||
response: { status: 200, body: testData.SIGNED_BACKUP_DATA },
|
||||
});
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
@@ -581,7 +606,7 @@ describe("megolm-keys backup", () => {
|
||||
await aliceCrypto.importRoomKeys(someRoomKeys);
|
||||
|
||||
// The backup loop is waiting a random amount of time to avoid different clients firing at the same time.
|
||||
jest.runAllTimers();
|
||||
vi.runAllTimers();
|
||||
|
||||
await Promise.all(uploadPromises);
|
||||
|
||||
@@ -605,7 +630,7 @@ describe("megolm-keys backup", () => {
|
||||
|
||||
await aliceCrypto.importRoomKeys([newKey]);
|
||||
|
||||
jest.runAllTimers();
|
||||
vi.runAllTimers();
|
||||
await newKeyUploadPromise;
|
||||
});
|
||||
|
||||
@@ -630,7 +655,7 @@ describe("megolm-keys backup", () => {
|
||||
const someRoomKeys = testData.MEGOLM_SESSION_DATA_ARRAY;
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
|
||||
overwriteRoutes: true,
|
||||
name: "room-keys-version",
|
||||
});
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
@@ -640,7 +665,7 @@ describe("megolm-keys backup", () => {
|
||||
await aliceCrypto.importRoomKeys(someRoomKeys);
|
||||
|
||||
// The backup loop is waiting a random amount of time to avoid different clients firing at the same time.
|
||||
jest.runAllTimers();
|
||||
vi.runAllTimers();
|
||||
|
||||
// wait for all keys to be backed up
|
||||
await remainingZeroPromise;
|
||||
@@ -651,10 +676,7 @@ describe("megolm-keys backup", () => {
|
||||
newBackup.version = newBackupVersion;
|
||||
|
||||
// Let's simulate that a new backup is available by returning error code on key upload
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
fetchMock.modifyRoute("room-keys-version", { response: newBackup });
|
||||
|
||||
// If we import a new key the loop will try to upload to old version, it will
|
||||
// fail then check the current version and switch if trusted
|
||||
@@ -697,12 +719,12 @@ describe("megolm-keys backup", () => {
|
||||
|
||||
await aliceCrypto.importRoomKeys([newKey]);
|
||||
|
||||
jest.runAllTimers();
|
||||
vi.runAllTimers();
|
||||
|
||||
await disableOldBackup;
|
||||
await enableNewBackup;
|
||||
|
||||
jest.runAllTimers();
|
||||
vi.runAllTimers();
|
||||
|
||||
await Promise.all(uploadPromises);
|
||||
await newKeyUploadPromise;
|
||||
@@ -717,22 +739,14 @@ describe("megolm-keys backup", () => {
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
// on the first key upload attempt, simulate a network failure
|
||||
const failurePromise = new Promise((resolve) => {
|
||||
fetchMock.put(
|
||||
"path:/_matrix/client/v3/room_keys/keys",
|
||||
() => {
|
||||
resolve(undefined);
|
||||
throw new TypeError(`Failed to fetch`);
|
||||
},
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
);
|
||||
fetchMock.putOnce("path:/_matrix/client/v3/room_keys/keys", () => {
|
||||
resolve(undefined);
|
||||
throw new TypeError(`Failed to fetch`);
|
||||
});
|
||||
});
|
||||
|
||||
// kick the import loop off and wait for the failed request
|
||||
@@ -741,27 +755,21 @@ describe("megolm-keys backup", () => {
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
jest.advanceTimersByTime(10 * 60 * 1000);
|
||||
vi.advanceTimersByTime(10 * 60 * 1000);
|
||||
await failurePromise;
|
||||
|
||||
// Fix the endpoint to do successful uploads
|
||||
const successPromise = new Promise((resolve) => {
|
||||
fetchMock.put(
|
||||
"path:/_matrix/client/v3/room_keys/keys",
|
||||
() => {
|
||||
resolve(undefined);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
count: 2,
|
||||
etag: "abcdefg",
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
);
|
||||
fetchMock.putOnce("path:/_matrix/client/v3/room_keys/keys", () => {
|
||||
resolve(undefined);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
count: 2,
|
||||
etag: "abcdefg",
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// check that a `KeyBackupSessionsRemaining` event is emitted with `remaining == 0`
|
||||
@@ -774,7 +782,7 @@ describe("megolm-keys backup", () => {
|
||||
});
|
||||
|
||||
// run the timers, which will make the backup loop redo the request
|
||||
await jest.advanceTimersByTimeAsync(10 * 60 * 1000);
|
||||
await vi.advanceTimersByTimeAsync(10 * 60 * 1000);
|
||||
await successPromise;
|
||||
await allKeysUploadedPromise;
|
||||
});
|
||||
@@ -782,7 +790,7 @@ describe("megolm-keys backup", () => {
|
||||
|
||||
it("getActiveSessionBackupVersion() should give correct result", async function () {
|
||||
// 404 means that there is no active backup
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/version", 404);
|
||||
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", 404);
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
@@ -801,9 +809,7 @@ describe("megolm-keys backup", () => {
|
||||
// Serve a backup with no trusted signature
|
||||
const unsignedBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
|
||||
delete unsignedBackup.auth_data.signatures;
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/version", unsignedBackup, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", unsignedBackup);
|
||||
|
||||
const checked = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(checked?.backupInfo?.version).toStrictEqual(unsignedBackup.version);
|
||||
@@ -813,9 +819,7 @@ describe("megolm-keys backup", () => {
|
||||
expect(backupStatus).toBeNull();
|
||||
|
||||
// Add a valid signature to the backup
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
// check that signalling is working
|
||||
const backupPromise = new Promise<void>((resolve, reject) => {
|
||||
@@ -837,7 +841,7 @@ describe("megolm-keys backup", () => {
|
||||
|
||||
it("getKeyBackupInfo() should not return a backup if the active backup has been deleted", async () => {
|
||||
// 404 means that there is no active backup
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/version", 404);
|
||||
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", 404);
|
||||
fetchMock.delete(`express:/_matrix/client/v3/room_keys/version/${testData.SIGNED_BACKUP_DATA.version}`, {});
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
@@ -853,11 +857,9 @@ describe("megolm-keys backup", () => {
|
||||
expect(await aliceCrypto.getKeyBackupInfo()).toBeNull();
|
||||
|
||||
// Return now the backup
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
expect(await aliceCrypto.getKeyBackupInfo()).toStrictEqual(testData.SIGNED_BACKUP_DATA);
|
||||
expect(await aliceCrypto.getKeyBackupInfo()).toMatchObject(testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
// Delete the backup and we are expecting the key backup to be disabled
|
||||
const keyBackupStatus = Promise.withResolvers<boolean>();
|
||||
@@ -991,7 +993,7 @@ describe("megolm-keys backup", () => {
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
@@ -1001,9 +1003,7 @@ describe("megolm-keys backup", () => {
|
||||
delete unsignedBackup.auth_data.signatures;
|
||||
unsignedBackup.version = "2";
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", unsignedBackup, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", unsignedBackup);
|
||||
|
||||
await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
|
||||
@@ -1018,7 +1018,7 @@ describe("megolm-keys backup", () => {
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
@@ -1028,9 +1028,7 @@ describe("megolm-keys backup", () => {
|
||||
const newBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
|
||||
newBackup.version = newBackupVersion;
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", newBackup);
|
||||
|
||||
await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(newBackupVersion);
|
||||
@@ -1045,25 +1043,19 @@ describe("megolm-keys backup", () => {
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
|
||||
|
||||
fetchMock.get(
|
||||
"path:/_matrix/client/v3/room_keys/version",
|
||||
{
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "No backup found",
|
||||
},
|
||||
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "No backup found",
|
||||
},
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
const noResult = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(noResult).toBeNull();
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
|
||||
@@ -1072,10 +1064,12 @@ describe("megolm-keys backup", () => {
|
||||
|
||||
describe("Backup Changed from other sessions", () => {
|
||||
beforeEach(async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
|
||||
name: "room-keys-version",
|
||||
});
|
||||
|
||||
// ignore requests to send room key requests
|
||||
fetchMock.put("express:/_matrix/client/v3/sendToDevice/m.room_key_request/:request_id", {});
|
||||
fetchMock.getOnce("express:/_matrix/client/v3/sendToDevice/m.room_key_request/:request_id", {});
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
@@ -1108,9 +1102,9 @@ describe("megolm-keys backup", () => {
|
||||
|
||||
fetchMock.get(
|
||||
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
|
||||
(url, request) => {
|
||||
(callLog) => {
|
||||
// check that the version is correct
|
||||
const version = new URLSearchParams(new URL(url).search).get("version");
|
||||
const version = new URLSearchParams(new URL(callLog.url).search).get("version");
|
||||
if (version == "1") {
|
||||
return testData.CURVE25519_KEY_BACKUP_DATA;
|
||||
} else {
|
||||
@@ -1124,7 +1118,7 @@ describe("megolm-keys backup", () => {
|
||||
};
|
||||
}
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
{ name: "room-keys" },
|
||||
);
|
||||
|
||||
// Send Alice a message that she won't be able to decrypt, and check that she fetches the key from the backup.
|
||||
@@ -1135,7 +1129,7 @@ describe("megolm-keys backup", () => {
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
await advanceTimersUntil(awaitDecryption(event, { waitOnDecryptionFailure: true }));
|
||||
|
||||
expect(event.getContent()).toEqual(testData.CLEAR_EVENT.content);
|
||||
expect(event.getContent<IContent>()).toEqual(testData.CLEAR_EVENT.content);
|
||||
|
||||
// =====
|
||||
// Second suppose now that the backup has changed to version 2
|
||||
@@ -1146,7 +1140,7 @@ describe("megolm-keys backup", () => {
|
||||
version: "2",
|
||||
};
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, { overwriteRoutes: true });
|
||||
fetchMock.modifyRoute("room-keys-version", { response: newBackup });
|
||||
// suppose the new key is now known
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||
@@ -1159,11 +1153,10 @@ describe("megolm-keys backup", () => {
|
||||
|
||||
const awaitHasQueriedNewBackup: PromiseWithResolvers<void> = Promise.withResolvers<void>();
|
||||
|
||||
fetchMock.get(
|
||||
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
|
||||
(url, request) => {
|
||||
fetchMock.modifyRoute("room-keys", {
|
||||
response: (callLog) => {
|
||||
// check that the version is correct
|
||||
const version = new URLSearchParams(new URL(url).search).get("version");
|
||||
const version = new URLSearchParams(new URL(callLog.url).search).get("version");
|
||||
if (version == newBackup.version) {
|
||||
awaitHasQueriedNewBackup.resolve();
|
||||
return testData.CURVE25519_KEY_BACKUP_DATA;
|
||||
@@ -1179,8 +1172,7 @@ describe("megolm-keys backup", () => {
|
||||
};
|
||||
}
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
});
|
||||
|
||||
// Send Alice a message that she won't be able to decrypt, and check that she fetches the key from the new backup.
|
||||
const newMessage: Partial<IEvent> = {
|
||||
@@ -1216,7 +1208,7 @@ describe("megolm-keys backup", () => {
|
||||
// user will be one).
|
||||
syncResponder.sendOrQueueSyncResponse({});
|
||||
// DeviceList has a sleep(5) which we need to make happen
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
// The client should now know about the dummy device
|
||||
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
|
||||
|
||||
@@ -16,7 +16,8 @@ limitations under the License.
|
||||
|
||||
import Olm from "@matrix-org/olm";
|
||||
import anotherjson from "another-json";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import { type RouteResponse } from "fetch-mock";
|
||||
|
||||
import {
|
||||
type IContent,
|
||||
@@ -32,7 +33,6 @@ import { type ISyncResponder } from "../../test-utils/SyncResponder";
|
||||
import { syncPromise } from "../../test-utils/test-utils";
|
||||
import { type KeyBackupInfo } from "../../../src/crypto-api";
|
||||
import { logger } from "../../../src/logger";
|
||||
import type FetchMock from "fetch-mock";
|
||||
|
||||
/**
|
||||
* @module
|
||||
@@ -305,7 +305,9 @@ export function encryptMegolmEventRawPlainText(opts: {
|
||||
},
|
||||
type: "m.room.encrypted",
|
||||
unsigned: {},
|
||||
state_key: opts.plaintext.state_key ? `${opts.plaintext.type}:${opts.plaintext.state_key}` : undefined,
|
||||
state_key: opts.plaintext.hasOwnProperty("state_key")
|
||||
? `${opts.plaintext.type}:${opts.plaintext.state_key}`
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -460,19 +462,11 @@ export async function expectSendRoomKey(
|
||||
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,
|
||||
},
|
||||
);
|
||||
fetchMock.putOnce(new RegExp("/sendToDevice/m.room.encrypted/"), (callLog): RouteResponse => {
|
||||
const content = JSON.parse(callLog.options.body as string);
|
||||
resolve(onSendRoomKey(content));
|
||||
return {};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -483,17 +477,11 @@ export async function expectSendRoomKey(
|
||||
*/
|
||||
export function expectEncryptedSendMessageEvent() {
|
||||
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 },
|
||||
);
|
||||
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" };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -504,17 +492,11 @@ export function expectEncryptedSendMessageEvent() {
|
||||
*/
|
||||
function expectEncryptedSendStateEvent() {
|
||||
return new Promise<IContent>((resolve) => {
|
||||
fetchMock.putOnce(
|
||||
new RegExp("/state/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 },
|
||||
);
|
||||
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" };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
|
||||
import { createClient, IndexedDBCryptoStore } from "../../../src";
|
||||
import { populateStore } from "../../test-utils/test_indexeddb_cryptostore_dump";
|
||||
@@ -26,7 +26,7 @@ import { FULL_ACCOUNT_DATASET } from "../../test-utils/test_indexeddb_cryptostor
|
||||
import { EMPTY_ACCOUNT_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/empty_account";
|
||||
import { CryptoEvent } from "../../../src/crypto-api";
|
||||
|
||||
jest.setTimeout(15000);
|
||||
vi.setConfig({ testTimeout: 15000 });
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
@@ -122,6 +122,7 @@ describe("MatrixClient.initRustCrypto", () => {
|
||||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should ignore a second call", async () => {
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
@@ -134,10 +135,6 @@ describe("MatrixClient.initRustCrypto", () => {
|
||||
});
|
||||
|
||||
describe("Libolm Migration", () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.reset();
|
||||
});
|
||||
|
||||
it("should migrate from libolm", async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", FULL_ACCOUNT_DATASET.backupResponse);
|
||||
|
||||
@@ -155,7 +152,7 @@ describe("MatrixClient.initRustCrypto", () => {
|
||||
pickleKey: FULL_ACCOUNT_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
const progressListener = jest.fn();
|
||||
const progressListener = vi.fn();
|
||||
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
@@ -326,7 +323,7 @@ describe("MatrixClient.initRustCrypto", () => {
|
||||
});
|
||||
|
||||
// When we start Rust crypto, potentially triggering an upgrade
|
||||
const progressListener = jest.fn();
|
||||
const progressListener = vi.fn();
|
||||
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
@@ -478,6 +475,7 @@ describe("MatrixClient.clearStores", () => {
|
||||
expect(await indexedDB.databases()).toHaveLength(0);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should not fail in environments without indexedDB", async () => {
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = undefined!;
|
||||
|
||||
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import anotherjson from "another-json";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import "fake-indexeddb/auto";
|
||||
import Olm from "@matrix-org/olm";
|
||||
|
||||
@@ -23,7 +23,13 @@ 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, PendingEventOrdering, type IStartClientOpts, type MatrixClient } from "../../../src/matrix";
|
||||
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";
|
||||
@@ -72,7 +78,6 @@ describe("Encrypted State Events", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
|
||||
const homeserverUrl = "https://alice-server.com";
|
||||
aliceClient = createClient({
|
||||
@@ -96,15 +101,11 @@ describe("Encrypted State Events", () => {
|
||||
}, 10000);
|
||||
|
||||
afterEach(async () => {
|
||||
await aliceClient.stopClient();
|
||||
await jest.runAllTimersAsync();
|
||||
fetchMock.mockReset();
|
||||
aliceClient.stopClient();
|
||||
});
|
||||
|
||||
function expectAliceKeyQuery(response: any) {
|
||||
fetchMock.postOnce(new RegExp("/keys/query"), (url: string, opts: RequestInit) => response, {
|
||||
overwriteRoutes: false,
|
||||
});
|
||||
fetchMock.postOnce(new RegExp("/keys/query"), (callLog) => response);
|
||||
}
|
||||
|
||||
function expectAliceKeyClaim(response: any) {
|
||||
@@ -190,6 +191,7 @@ describe("Encrypted State Events", () => {
|
||||
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);
|
||||
@@ -201,7 +203,7 @@ describe("Encrypted State Events", () => {
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
// Alice shares a room with Bob
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"], ROOM_ID, true));
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"], HistoryVisibility.Joined, ROOM_ID, true));
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// ... and claim one of Bob's OTKs ...
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import Olm from "@matrix-org/olm";
|
||||
@@ -59,7 +59,6 @@ describe("to-device-messages", () => {
|
||||
async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
|
||||
const homeserverUrl = "https://server.com";
|
||||
aliceClient = createClient({
|
||||
@@ -100,7 +99,6 @@ describe("to-device-messages", () => {
|
||||
|
||||
afterEach(async () => {
|
||||
aliceClient.stopClient();
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
describe("encryptToDeviceMessages", () => {
|
||||
|
||||
@@ -18,12 +18,12 @@ import "fake-indexeddb/auto";
|
||||
|
||||
import anotherjson from "another-json";
|
||||
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 { createHash } from "crypto";
|
||||
import Olm from "@matrix-org/olm";
|
||||
|
||||
import type FetchMock from "fetch-mock";
|
||||
import {
|
||||
createClient,
|
||||
DebugLogger,
|
||||
@@ -92,10 +92,7 @@ beforeAll(async () => {
|
||||
}, 10000);
|
||||
|
||||
beforeEach(() => {
|
||||
// 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"] });
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -130,7 +127,6 @@ describe("verification", () => {
|
||||
beforeEach(async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
|
||||
e2eKeyReceiver = new E2EKeyReceiver(TEST_HOMESERVER_URL);
|
||||
e2eKeyResponder = new E2EKeyResponder(TEST_HOMESERVER_URL);
|
||||
@@ -141,14 +137,12 @@ describe("verification", () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (aliceClient !== undefined) {
|
||||
await aliceClient.stopClient();
|
||||
}
|
||||
aliceClient?.stopClient();
|
||||
|
||||
// Allow in-flight things to complete before we tear down the test
|
||||
await jest.runAllTimersAsync();
|
||||
|
||||
fetchMock.mockReset();
|
||||
if (vi.isFakeTimers()) {
|
||||
await vi.runAllTimersAsync();
|
||||
}
|
||||
});
|
||||
|
||||
describe("Outgoing verification requests for another device", () => {
|
||||
@@ -156,11 +150,10 @@ describe("verification", () => {
|
||||
// pretend that we have another device, which we will verify
|
||||
e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
|
||||
|
||||
fetchMock.put(
|
||||
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp("m.secret.request")}`),
|
||||
{ ok: false, status: 404 },
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
fetchMock.put(new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp("m.secret.request")}`), {
|
||||
ok: false,
|
||||
status: 404,
|
||||
});
|
||||
});
|
||||
|
||||
// test with (1) the default verification method list, (2) a custom verification method list.
|
||||
@@ -212,7 +205,7 @@ describe("verification", () => {
|
||||
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
|
||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||
if (methods !== undefined) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
// eslint-disable-next-line @vitest/no-conditional-expect
|
||||
expect(new Set(toDeviceMessage.methods)).toEqual(new Set(methods));
|
||||
}
|
||||
|
||||
@@ -245,7 +238,7 @@ describe("verification", () => {
|
||||
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.accept");
|
||||
const verificationPromise = verifier.verify();
|
||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||
jest.advanceTimersByTime(10);
|
||||
vi.advanceTimersByTime(10);
|
||||
|
||||
requestBody = await sendToDevicePromise;
|
||||
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
@@ -323,7 +316,7 @@ describe("verification", () => {
|
||||
expect(request.otherPartySupportsMethod("m.sas.v1")).toBe(true);
|
||||
|
||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
// And now Alice starts a SAS verification
|
||||
let sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start");
|
||||
@@ -516,7 +509,7 @@ describe("verification", () => {
|
||||
// Rust crypto waits for the 'done' to arrive from the other side.
|
||||
if (request.phase === VerificationPhase.Done) {
|
||||
const userVerificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(TEST_USER_ID);
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
// eslint-disable-next-line @vitest/no-conditional-expect
|
||||
expect(userVerificationStatus.isCrossSigningVerified()).toBeTruthy();
|
||||
await verificationPromise;
|
||||
}
|
||||
@@ -639,7 +632,7 @@ describe("verification", () => {
|
||||
expect(request.verifier).toBeUndefined();
|
||||
|
||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
// ... but Alice wants to do an SAS verification
|
||||
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start");
|
||||
@@ -684,7 +677,7 @@ describe("verification", () => {
|
||||
expect(request.verifier).toBeUndefined();
|
||||
|
||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
// ... but the dummy device wants to do an SAS verification
|
||||
returnToDeviceMessageFromSync(buildSasStartMessage(transactionId));
|
||||
@@ -792,7 +785,7 @@ describe("verification", () => {
|
||||
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.accept");
|
||||
const verificationPromise = verifier.verify();
|
||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||
jest.advanceTimersByTime(10);
|
||||
vi.advanceTimersByTime(10);
|
||||
await sendToDevicePromise;
|
||||
|
||||
// now we unceremoniously cancel. We expect the verificatationPromise to reject.
|
||||
@@ -937,19 +930,16 @@ describe("verification", () => {
|
||||
function awaitRoomMessageRequest(): Promise<IContent> {
|
||||
return new Promise((resolve) => {
|
||||
// Case of unencrypted message of the new crypto
|
||||
fetchMock.put(
|
||||
"express:/_matrix/client/v3/rooms/:roomId/send/m.room.message/:txId",
|
||||
(url: string, options: RequestInit) => {
|
||||
resolve(JSON.parse(options.body as string));
|
||||
return { event_id: "$YUwRidLecu:example.com" };
|
||||
},
|
||||
);
|
||||
fetchMock.put("express:/_matrix/client/v3/rooms/:roomId/send/m.room.message/:txId", (callLog) => {
|
||||
resolve(JSON.parse(callLog.options.body as string));
|
||||
return { event_id: "$YUwRidLecu:example.com" };
|
||||
});
|
||||
|
||||
// Case of encrypted message of the old crypto
|
||||
fetchMock.put(
|
||||
"express:/_matrix/client/v3/rooms/:roomId/send/m.room.encrypted/:txId",
|
||||
async (url: string, options: RequestInit) => {
|
||||
const encryptedMessage = JSON.parse(options.body as string);
|
||||
async (callLog) => {
|
||||
const encryptedMessage = JSON.parse(callLog.options.body as string);
|
||||
const event = new MatrixEvent({
|
||||
content: encryptedMessage,
|
||||
type: "m.room.encrypted",
|
||||
@@ -972,7 +962,7 @@ describe("verification", () => {
|
||||
|
||||
// In `DeviceList#doQueuedQueries`, the key download response is processed every 5ms
|
||||
// 5ms by users, ie Bob and Alice
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
const messageRequestPromise = awaitRoomMessageRequest();
|
||||
const verificationRequest = await aliceClient
|
||||
@@ -1082,14 +1072,14 @@ describe("verification", () => {
|
||||
});
|
||||
|
||||
it("ignores old verification requests", async () => {
|
||||
const debug = jest.fn();
|
||||
const info = jest.fn();
|
||||
const warn = 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 = jest.fn();
|
||||
const eventHandler = vi.fn();
|
||||
aliceClient.on(CryptoEvent.VerificationRequestReceived, eventHandler);
|
||||
|
||||
const verificationRequestEvent = createVerificationRequestEvent();
|
||||
@@ -1105,7 +1095,7 @@ describe("verification", () => {
|
||||
|
||||
// Wait until the request has been processed. We use a real sleep()
|
||||
// here to make sure any background async tasks are completed.
|
||||
jest.useRealTimers();
|
||||
vi.useRealTimers();
|
||||
await waitFor(async () => {
|
||||
expect(info).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/^Ignoring just-received verification request/),
|
||||
@@ -1187,7 +1177,7 @@ describe("verification", () => {
|
||||
returnToDeviceMessageFromSync(toDeviceEvent);
|
||||
|
||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
// Wait for the request to be decrypted
|
||||
const request1 = await requestEventPromise;
|
||||
@@ -1224,7 +1214,7 @@ describe("verification", () => {
|
||||
expect(matrixEvent.getContent().msgtype).toEqual("m.bad.encrypted");
|
||||
|
||||
// Advance time by 5mins, the verification request should be ignored after that
|
||||
jest.advanceTimersByTime(5 * 60 * 1000);
|
||||
vi.advanceTimersByTime(5 * 60 * 1000);
|
||||
|
||||
// Send Bob the room keys
|
||||
returnToDeviceMessageFromSync(toDeviceEvent);
|
||||
@@ -1290,7 +1280,7 @@ describe("verification", () => {
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse([TEST_USER_ID]));
|
||||
await syncPromise(aliceClient);
|
||||
// DeviceList has a sleep(5) which we need to make happen
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
// The client should now know about the olm device
|
||||
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
|
||||
@@ -1302,11 +1292,10 @@ describe("verification", () => {
|
||||
testOlmAccount?.free();
|
||||
|
||||
// Allow in-flight things to complete before we tear down the test
|
||||
await jest.runAllTimersAsync();
|
||||
|
||||
fetchMock.mockReset();
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("Should request cross signing keys after verification", async () => {
|
||||
const requestPromises = mockSecretRequestAndGetPromises();
|
||||
|
||||
@@ -1424,11 +1413,11 @@ describe("verification", () => {
|
||||
*/
|
||||
async function retrieveBackupPrivateKeyWithDelay(): Promise<Uint8Array | null> {
|
||||
// We are lacking a way to signal that the secret has been received, so we wait a bit..
|
||||
jest.useRealTimers();
|
||||
vi.useRealTimers();
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
vi.useFakeTimers();
|
||||
|
||||
return aliceClient.getCrypto()!.getSessionBackupPrivateKey();
|
||||
}
|
||||
@@ -1461,27 +1450,21 @@ describe("verification", () => {
|
||||
});
|
||||
|
||||
const expectBackupCheck = new Promise((resolve) => {
|
||||
fetchMock.get(
|
||||
"express:/_matrix/client/v3/room_keys/version",
|
||||
(url, request) => {
|
||||
resolve(undefined);
|
||||
if (expectBackup instanceof MatrixError) {
|
||||
return {
|
||||
status: expectBackup.httpStatus,
|
||||
body: expectBackup.data,
|
||||
};
|
||||
}
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/version", (callLog) => {
|
||||
resolve(undefined);
|
||||
if (expectBackup instanceof MatrixError) {
|
||||
return {
|
||||
status: expectBackup.httpStatus,
|
||||
body: expectBackup.data,
|
||||
};
|
||||
}
|
||||
|
||||
if (expectBackup instanceof Error) {
|
||||
return Promise.reject(expectBackup);
|
||||
}
|
||||
if (expectBackup instanceof Error) {
|
||||
return Promise.reject(expectBackup);
|
||||
}
|
||||
|
||||
return expectBackup;
|
||||
},
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
);
|
||||
return expectBackup;
|
||||
});
|
||||
});
|
||||
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", CURVE25519_KEY_BACKUP_DATA);
|
||||
@@ -1562,7 +1545,7 @@ describe("verification", () => {
|
||||
// user will be one).
|
||||
syncResponder.sendOrQueueSyncResponse({});
|
||||
// DeviceList has a sleep(5) which we need to make happen
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
// The client should now know about the dummy device
|
||||
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
|
||||
@@ -1596,8 +1579,8 @@ function expectSendToDeviceMessage(msgtype: string): Promise<{ messages: any }>
|
||||
return new Promise((resolve) => {
|
||||
fetchMock.putOnce(
|
||||
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp(msgtype)}`),
|
||||
(url: string, opts: RequestInit): FetchMock.MockResponse => {
|
||||
resolve(JSON.parse(opts.body as string));
|
||||
(callLog): RouteResponse => {
|
||||
resolve(JSON.parse(callLog.options.body as string));
|
||||
return {};
|
||||
},
|
||||
);
|
||||
@@ -1618,29 +1601,25 @@ function mockSecretRequestAndGetPromises(): Map<string, Promise<string>> {
|
||||
const uskRequestResolvers = Promise.withResolvers<string>();
|
||||
const backupKeyRequestResolvers = Promise.withResolvers<string>();
|
||||
|
||||
fetchMock.put(
|
||||
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/m.secret.request`),
|
||||
(url: string, opts: RequestInit): FetchMock.MockResponse => {
|
||||
const messages = JSON.parse(opts.body as string).messages[TEST_USER_ID];
|
||||
// rust crypto broadcasts to all devices, old crypto to a specific device, take the first one
|
||||
const content = Object.values(messages)[0] as any;
|
||||
if (content.action == "request") {
|
||||
const name = content.name;
|
||||
const requestId = content.request_id;
|
||||
if (name == "m.cross_signing.user_signing") {
|
||||
uskRequestResolvers.resolve(requestId);
|
||||
} else if (name == "m.cross_signing.master") {
|
||||
mskRequestResolvers.resolve(requestId);
|
||||
} else if (name == "m.cross_signing.self_signing") {
|
||||
sskRequestResolvers.resolve(requestId);
|
||||
} else if (name == "m.megolm_backup.v1") {
|
||||
backupKeyRequestResolvers.resolve(requestId);
|
||||
}
|
||||
fetchMock.put(new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/m.secret.request`), (callLog): RouteResponse => {
|
||||
const messages = JSON.parse(callLog.options.body as string).messages[TEST_USER_ID];
|
||||
// rust crypto broadcasts to all devices, old crypto to a specific device, take the first one
|
||||
const content = Object.values(messages)[0] as any;
|
||||
if (content.action == "request") {
|
||||
const name = content.name;
|
||||
const requestId = content.request_id;
|
||||
if (name == "m.cross_signing.user_signing") {
|
||||
uskRequestResolvers.resolve(requestId);
|
||||
} else if (name == "m.cross_signing.master") {
|
||||
mskRequestResolvers.resolve(requestId);
|
||||
} else if (name == "m.cross_signing.self_signing") {
|
||||
sskRequestResolvers.resolve(requestId);
|
||||
} else if (name == "m.megolm_backup.v1") {
|
||||
backupKeyRequestResolvers.resolve(requestId);
|
||||
}
|
||||
return {};
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
const promiseMap = new Map<string, Promise<string>>();
|
||||
promiseMap.set("m.cross_signing.master", mskRequestResolvers.promise);
|
||||
|
||||
@@ -672,7 +672,7 @@ describe("MatrixClient event timelines", function () {
|
||||
expect(timeline!.getEvents().find((e) => e.getId() === THREAD_ROOT.event_id!)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return undefined when event is not in the thread that the given timelineSet is representing", () => {
|
||||
it("should return null when event is not in the thread that the given timelineSet is representing", () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
@@ -696,12 +696,12 @@ describe("MatrixClient event timelines", function () {
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
expect(client.getEventTimeline(timelineSet, EVENTS[0].event_id!)).resolves.toBeUndefined(),
|
||||
expect(client.getEventTimeline(timelineSet, EVENTS[0].event_id!)).resolves.toBeNull(),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return undefined when event is within a thread but timelineSet is not", () => {
|
||||
it("should return null when event is within a thread but timelineSet is not", () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
@@ -723,7 +723,7 @@ describe("MatrixClient event timelines", function () {
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
expect(client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!)).resolves.toBeUndefined(),
|
||||
expect(client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!)).resolves.toBeNull(),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
@@ -2044,6 +2044,7 @@ describe("MatrixClient event timelines", function () {
|
||||
expect(timeline!.getEvents()[1]!.event).toEqual(THREAD_REPLY);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("in stable mode", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.threadSupport = true;
|
||||
|
||||
@@ -347,6 +347,7 @@ describe("MatrixClient", function () {
|
||||
expect((await prom).room_id).toBe(roomId);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should no-op if you've already knocked a room", function () {
|
||||
const room = new Room(roomId, client, userId);
|
||||
|
||||
@@ -380,23 +381,16 @@ describe("MatrixClient", function () {
|
||||
[
|
||||
403,
|
||||
{ errcode: "M_FORBIDDEN", error: "You don't have permission to knock" },
|
||||
"[M_FORBIDDEN: MatrixError: [403] You don't have permission to knock]",
|
||||
],
|
||||
[
|
||||
500,
|
||||
{ errcode: "INTERNAL_SERVER_ERROR" },
|
||||
"[INTERNAL_SERVER_ERROR: MatrixError: [500] Unknown message]",
|
||||
"MatrixError: [403] You don't have permission to knock",
|
||||
],
|
||||
[500, { errcode: "INTERNAL_SERVER_ERROR" }, "MatrixError: [500] Unknown message"],
|
||||
];
|
||||
|
||||
it.each(testCases)("should handle %s error", async (code, { errcode, error }, snapshot) => {
|
||||
httpBackend.when("POST", "/knock/" + encodeURIComponent(roomId)).respond(code, { errcode, error });
|
||||
|
||||
const prom = client.knockRoom(roomId);
|
||||
await Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
expect(prom).rejects.toMatchInlineSnapshot(snapshot),
|
||||
]);
|
||||
await Promise.all([httpBackend.flushAllExpected(), expect(prom).rejects.toThrow(snapshot)]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1198,7 +1192,7 @@ describe("MatrixClient", function () {
|
||||
describe("logout", () => {
|
||||
it("should abort pending requests when called with stopClient=true", async () => {
|
||||
httpBackend.when("POST", "/logout").respond(200, {});
|
||||
const fn = jest.fn();
|
||||
const fn = vi.fn();
|
||||
client.http.request(Method.Get, "/test").catch(fn);
|
||||
client.logout(true);
|
||||
await httpBackend.flush(undefined);
|
||||
@@ -1326,7 +1320,7 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("should always fetch capabilities and then cache", async () => {
|
||||
@@ -1397,6 +1391,7 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
|
||||
describe("publicRooms", () => {
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should use GET request if no server or filter is specified", () => {
|
||||
httpBackend.when("GET", "/publicRooms").respond(200, {});
|
||||
client.publicRooms({});
|
||||
@@ -1585,7 +1580,7 @@ describe("MatrixClient", function () {
|
||||
|
||||
describe("setSyncPresence", () => {
|
||||
it("should pass calls through to the underlying sync api", () => {
|
||||
const setPresence = jest.fn();
|
||||
const setPresence = vi.fn();
|
||||
// @ts-ignore
|
||||
client.syncApi = { setPresence };
|
||||
client.setSyncPresence(SetPresence.Unavailable);
|
||||
@@ -1594,6 +1589,7 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
|
||||
describe("sendTyping", () => {
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should bail early for guests", async () => {
|
||||
client.setGuest(true);
|
||||
await client.sendTyping("!room:server", true, 100);
|
||||
@@ -1851,6 +1847,7 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
|
||||
describe("setRoomMutePushRule", () => {
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should set room push rule to muted", async () => {
|
||||
const roomId = "!roomId:server";
|
||||
const client = new MatrixClient({
|
||||
|
||||
@@ -159,7 +159,7 @@ describe("MatrixClient opts", function () {
|
||||
|
||||
await expect(
|
||||
Promise.all([client.sendTextMessage("!foo:bar", "a body", "txn1"), httpBackend.flush("/txn1", 1)]),
|
||||
).rejects.toThrow("MatrixError: [500] Unknown message");
|
||||
).rejects.toThrow("MatrixError: [500] Ruh roh");
|
||||
});
|
||||
|
||||
it("shouldn't queue events", async () => {
|
||||
|
||||
@@ -720,7 +720,7 @@ describe("MatrixClient room timelines", function () {
|
||||
} else {
|
||||
reject(new Error("TestError: Timed out while waiting for `RoomEvent.TimelineReset` to fire."));
|
||||
}
|
||||
}, 4000 /* FIXME: Is there a way to reference the current timeout of this test in Jest? */);
|
||||
}, 4000 /* FIXME: Is there a way to reference the current timeout of this test in Vitest? */);
|
||||
|
||||
room.on(RoomEvent.TimelineReset, async () => {
|
||||
try {
|
||||
|
||||
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import "fake-indexeddb/auto";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
|
||||
import { type MatrixClient, ClientEvent, createClient, SyncState } from "../../src";
|
||||
|
||||
@@ -83,8 +83,7 @@ describe("MatrixClient syncing errors", () => {
|
||||
});
|
||||
|
||||
it("should retry, until errors are solved.", async () => {
|
||||
jest.useFakeTimers();
|
||||
fetchMock.config.overwriteRoutes = false;
|
||||
vi.useFakeTimers();
|
||||
fetchMock
|
||||
.getOnce("end:versions", {}) // first version check without credentials needs to succeed
|
||||
.getOnce("end:versions", 429) // second version check fails with 429 triggering another retry
|
||||
@@ -105,19 +104,18 @@ describe("MatrixClient syncing errors", () => {
|
||||
|
||||
await client!.startClient();
|
||||
expect(await syncEvents[0].promise).toBe(SyncState.Error);
|
||||
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
|
||||
vi.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
|
||||
expect(await syncEvents[1].promise).toBe(SyncState.Error);
|
||||
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
|
||||
vi.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
|
||||
expect(await syncEvents[2].promise).toBe(SyncState.Prepared);
|
||||
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
|
||||
vi.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
|
||||
expect(await syncEvents[3].promise).toBe(SyncState.Syncing);
|
||||
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
|
||||
vi.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
|
||||
expect(await syncEvents[4].promise).toBe(SyncState.Syncing);
|
||||
});
|
||||
|
||||
it("should stop sync keep alive when client is stopped.", async () => {
|
||||
jest.useFakeTimers();
|
||||
fetchMock.config.overwriteRoutes = false;
|
||||
vi.useFakeTimers();
|
||||
fetchMock
|
||||
.get("end:capabilities", {})
|
||||
.getOnce("end:versions", {}) // first version check without credentials needs to succeed
|
||||
@@ -146,9 +144,9 @@ describe("MatrixClient syncing errors", () => {
|
||||
|
||||
const syntState = await firstSyncEvent.promise;
|
||||
expect(syntState).toBe(SyncState.Error);
|
||||
jest.runAllTimers(); // this will skip forward to trigger the keepAlive
|
||||
vi.runAllTimers(); // this will skip forward to trigger the keepAlive
|
||||
|
||||
jest.useRealTimers(); // we need real timer for the setTimout below to work
|
||||
vi.useRealTimers(); // we need real timer for the setTimout below to work
|
||||
|
||||
const timeoutPromise = makeQueryablePromise(new Promise<void>((res) => setTimeout(res, 1)));
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ describe("MatrixClient syncing", () => {
|
||||
presence: {},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should /sync after /pushrules and /filter.", async () => {
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
@@ -501,7 +502,7 @@ describe("MatrixClient syncing", () => {
|
||||
})
|
||||
.respond(200, syncData);
|
||||
|
||||
client!.store.getSavedSyncToken = jest.fn().mockResolvedValue("this-is-a-token");
|
||||
client!.store.getSavedSyncToken = vi.fn().mockResolvedValue("this-is-a-token");
|
||||
client!.startClient({ initialSyncLimit: 1 });
|
||||
|
||||
return httpBackend!.flushAllExpected();
|
||||
@@ -994,7 +995,7 @@ describe("MatrixClient syncing", () => {
|
||||
roomVersion: "org.matrix.msc2716v3",
|
||||
},
|
||||
].forEach((testMeta) => {
|
||||
// eslint-disable-next-line jest/valid-title
|
||||
// eslint-disable-next-line @vitest/valid-title
|
||||
describe(testMeta.label, () => {
|
||||
const roomCreateEvent = utils.mkEvent({
|
||||
type: "m.room.create",
|
||||
@@ -1835,7 +1836,7 @@ describe("MatrixClient syncing", () => {
|
||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||
|
||||
const room = client!.getRoom(roomOne);
|
||||
room!.hasEncryptionStateEvent = jest.fn().mockReturnValue(true);
|
||||
room!.hasEncryptionStateEvent = vi.fn().mockReturnValue(true);
|
||||
|
||||
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(5);
|
||||
|
||||
@@ -2519,7 +2520,7 @@ describe("MatrixClient syncing", () => {
|
||||
const eventB2 = new MatrixEvent({ type: "b", content: { body: "2" } });
|
||||
|
||||
client!.store.storeAccountDataEvents([eventA1, eventB1]);
|
||||
const fn = jest.fn();
|
||||
const fn = vi.fn();
|
||||
client!.on(ClientEvent.AccountData, fn);
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, {
|
||||
|
||||
@@ -62,7 +62,7 @@ describe("Notification count fixing", () => {
|
||||
|
||||
client!.startClient({ threadSupport: true });
|
||||
const room = new Room(roomId, client!, selfUserId);
|
||||
jest.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
|
||||
vi.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
|
||||
|
||||
const event = new MatrixEvent({
|
||||
room_id: roomId,
|
||||
@@ -77,7 +77,7 @@ describe("Notification count fixing", () => {
|
||||
},
|
||||
});
|
||||
|
||||
jest.spyOn(event, "getPushActions").mockReturnValue({
|
||||
vi.spyOn(event, "getPushActions").mockReturnValue({
|
||||
notify: true,
|
||||
tweaks: {},
|
||||
});
|
||||
@@ -123,7 +123,7 @@ describe("MatrixClient syncing", () => {
|
||||
]);
|
||||
|
||||
const room = new Room(roomId, client!, selfUserId);
|
||||
jest.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
|
||||
vi.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
|
||||
|
||||
const thread = mkThread({ room, client: client!, authorId: selfUserId, participantUserIds: [selfUserId] });
|
||||
const threadReply = thread.events.at(-1)!;
|
||||
@@ -143,7 +143,7 @@ describe("MatrixClient syncing", () => {
|
||||
|
||||
const reactionEventId = `$9-${Math.random()}-${Math.random()}`;
|
||||
let lastEvent: MatrixEvent | null = null;
|
||||
jest.spyOn(client! as any, "sendEventHttpRequest").mockImplementation((event) => {
|
||||
vi.spyOn(client! as any, "sendEventHttpRequest").mockImplementation((event) => {
|
||||
lastEvent = event as MatrixEvent;
|
||||
return { event_id: reactionEventId };
|
||||
});
|
||||
@@ -195,7 +195,7 @@ describe("MatrixClient syncing", () => {
|
||||
})
|
||||
.respond(200, syncData);
|
||||
|
||||
client!.store.getSavedSyncToken = jest.fn().mockResolvedValue("this-is-a-token");
|
||||
client!.store.getSavedSyncToken = vi.fn().mockResolvedValue("this-is-a-token");
|
||||
client!.startClient({ initialSyncLimit: 1 });
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
@@ -14,9 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { QrCodeData, QrCodeMode } from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
import { mocked } from "jest-mock";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import { QrCodeData, QrCodeIntent } from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
|
||||
import {
|
||||
MSC4108FailureReason,
|
||||
@@ -40,7 +39,7 @@ import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
|
||||
function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled: boolean }): MatrixClient {
|
||||
const baseUrl = "https://example.com";
|
||||
const crypto = {
|
||||
exportSecretsForQrLogin: jest.fn(),
|
||||
exportSecretsForQrLogin: vi.fn(),
|
||||
};
|
||||
const client = {
|
||||
doesServerSupportUnstableFeature(feature: string) {
|
||||
@@ -54,9 +53,9 @@ function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled
|
||||
},
|
||||
baseUrl,
|
||||
getDomain: () => "example.com",
|
||||
getDevice: jest.fn(),
|
||||
getCrypto: jest.fn(() => crypto),
|
||||
getAuthMetadata: jest.fn().mockResolvedValue(makeDelegatedAuthConfig("https://issuer/", [DEVICE_CODE_SCOPE])),
|
||||
getDevice: vi.fn(),
|
||||
getCrypto: vi.fn(() => crypto),
|
||||
getAuthMetadata: vi.fn().mockResolvedValue(makeDelegatedAuthConfig("https://issuer/", [DEVICE_CODE_SCOPE])),
|
||||
} as unknown as MatrixClient;
|
||||
client.http = new MatrixHttpApi<IHttpOpts & { onlyData: true }>(client, {
|
||||
baseUrl: client.baseUrl,
|
||||
@@ -77,10 +76,6 @@ describe("MSC4108SignInWithQR", () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
});
|
||||
|
||||
const url = "https://fallbackserver/rz/123";
|
||||
const deviceId = "DEADB33F";
|
||||
const verificationUri = "https://example.com/verify";
|
||||
@@ -115,10 +110,10 @@ describe("MSC4108SignInWithQR", () => {
|
||||
let opponentData = Promise.withResolvers<string>();
|
||||
|
||||
const ourMockSession = {
|
||||
send: jest.fn(async (newData) => {
|
||||
send: vi.fn(async (newData) => {
|
||||
ourData.resolve(newData);
|
||||
}),
|
||||
receive: jest.fn(() => {
|
||||
receive: vi.fn(() => {
|
||||
const prom = opponentData.promise;
|
||||
prom.then(() => {
|
||||
opponentData = Promise.withResolvers();
|
||||
@@ -134,10 +129,10 @@ describe("MSC4108SignInWithQR", () => {
|
||||
},
|
||||
} as unknown as MSC4108RendezvousSession;
|
||||
const opponentMockSession = {
|
||||
send: jest.fn(async (newData) => {
|
||||
send: vi.fn(async (newData) => {
|
||||
opponentData.resolve(newData);
|
||||
}),
|
||||
receive: jest.fn(() => {
|
||||
receive: vi.fn(() => {
|
||||
const prom = ourData.promise;
|
||||
prom.then(() => {
|
||||
ourData = Promise.withResolvers();
|
||||
@@ -151,7 +146,7 @@ describe("MSC4108SignInWithQR", () => {
|
||||
|
||||
const ourChannel = new MSC4108SecureChannel(ourMockSession);
|
||||
const qrCodeData = QrCodeData.fromBytes(
|
||||
await ourChannel.generateCode(QrCodeMode.Reciprocate, client.getDomain()!),
|
||||
await ourChannel.generateCode(QrCodeIntent.Reciprocate, client.getDomain()!),
|
||||
);
|
||||
const opponentChannel = new MSC4108SecureChannel(opponentMockSession, qrCodeData.publicKey);
|
||||
|
||||
@@ -171,7 +166,7 @@ describe("MSC4108SignInWithQR", () => {
|
||||
it("should be able to connect with opponent and share verificationUri", async () => {
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
|
||||
mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404));
|
||||
vi.mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404));
|
||||
|
||||
await Promise.all([
|
||||
expect(ourLogin.deviceAuthorizationGrant()).resolves.toEqual({
|
||||
@@ -194,7 +189,7 @@ describe("MSC4108SignInWithQR", () => {
|
||||
it("should abort if device already exists", async () => {
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
|
||||
mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
|
||||
vi.mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
|
||||
|
||||
await Promise.all([
|
||||
expect(ourLogin.deviceAuthorizationGrant()).rejects.toThrow("Specified device ID already exists"),
|
||||
@@ -244,12 +239,12 @@ describe("MSC4108SignInWithQR", () => {
|
||||
// @ts-ignore
|
||||
await opponentLogin.receive();
|
||||
|
||||
mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
|
||||
vi.mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
|
||||
|
||||
const secrets = {
|
||||
cross_signing: { master_key: "mk", user_signing_key: "usk", self_signing_key: "ssk" },
|
||||
};
|
||||
client.getCrypto()!.exportSecretsBundle = jest.fn().mockResolvedValue(secrets);
|
||||
client.getCrypto()!.exportSecretsBundle = vi.fn().mockResolvedValue(secrets);
|
||||
|
||||
const payload = {
|
||||
secrets: expect.objectContaining(secrets),
|
||||
@@ -261,13 +256,13 @@ describe("MSC4108SignInWithQR", () => {
|
||||
});
|
||||
|
||||
it("should abort if device doesn't come up by timeout", async () => {
|
||||
jest.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
|
||||
vi.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
|
||||
fn();
|
||||
// TODO: mock timers properly
|
||||
return -1 as any;
|
||||
});
|
||||
jest.spyOn(Date, "now").mockImplementation(() => {
|
||||
return 12345678 + mocked(setTimeout).mock.calls.length * 1000;
|
||||
vi.spyOn(Date, "now").mockImplementation(() => {
|
||||
return 12345678 + vi.mocked(setTimeout).mock.calls.length * 1000;
|
||||
});
|
||||
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
@@ -280,7 +275,7 @@ describe("MSC4108SignInWithQR", () => {
|
||||
await opponentLogin.send({
|
||||
type: PayloadType.Success,
|
||||
});
|
||||
mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404));
|
||||
vi.mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404));
|
||||
|
||||
const ourProm = ourLogin.shareSecrets();
|
||||
await expect(ourProm).rejects.toThrow("New device not found");
|
||||
@@ -297,7 +292,7 @@ describe("MSC4108SignInWithQR", () => {
|
||||
await opponentLogin.send({
|
||||
type: PayloadType.Success,
|
||||
});
|
||||
mocked(client.getDevice).mockRejectedValue(
|
||||
vi.mocked(client.getDevice).mockRejectedValue(
|
||||
new MatrixError({ errcode: "M_UNKNOWN", error: "The message" }, 500),
|
||||
);
|
||||
|
||||
@@ -314,7 +309,7 @@ describe("MSC4108SignInWithQR", () => {
|
||||
});
|
||||
|
||||
it("should not send secrets if user cancels", async () => {
|
||||
jest.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
|
||||
vi.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
|
||||
fn();
|
||||
// TODO: mock timers properly
|
||||
return -1 as any;
|
||||
@@ -334,7 +329,7 @@ describe("MSC4108SignInWithQR", () => {
|
||||
await opponentLogin.receive();
|
||||
|
||||
const deviceResolvers = Promise.withResolvers<IMyDevice>();
|
||||
mocked(client.getDevice).mockReturnValue(deviceResolvers.promise);
|
||||
vi.mocked(client.getDevice).mockReturnValue(deviceResolvers.promise);
|
||||
|
||||
ourLogin.cancel(MSC4108FailureReason.UserCancelled).catch(() => {});
|
||||
deviceResolvers.resolve({} as IMyDevice);
|
||||
@@ -342,7 +337,7 @@ describe("MSC4108SignInWithQR", () => {
|
||||
const secrets = {
|
||||
cross_signing: { master_key: "mk", user_signing_key: "usk", self_signing_key: "ssk" },
|
||||
};
|
||||
client.getCrypto()!.exportSecretsBundle = jest.fn().mockResolvedValue(secrets);
|
||||
client.getCrypto()!.exportSecretsBundle = vi.fn().mockResolvedValue(secrets);
|
||||
|
||||
await Promise.all([
|
||||
expect(ourProm).rejects.toThrow("User cancelled"),
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
type Extension,
|
||||
} from "../../src/sliding-sync";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { type IRoomEvent, type IStateEvent } from "../../src";
|
||||
import { type IContent, type IRoomEvent, type IStateEvent } from "../../src";
|
||||
import {
|
||||
type MatrixClient,
|
||||
type MatrixEvent,
|
||||
@@ -68,17 +68,17 @@ describe("SlidingSyncSdk", () => {
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
|
||||
const mockifySlidingSync = (s: SlidingSync): SlidingSync => {
|
||||
s.getListParams = jest.fn();
|
||||
s.getListData = jest.fn();
|
||||
s.getRoomSubscriptions = jest.fn();
|
||||
s.modifyRoomSubscriptionInfo = jest.fn();
|
||||
s.modifyRoomSubscriptions = jest.fn();
|
||||
s.registerExtension = jest.fn();
|
||||
s.setList = jest.fn();
|
||||
s.setListRanges = jest.fn();
|
||||
s.start = jest.fn();
|
||||
s.stop = jest.fn();
|
||||
s.resend = jest.fn();
|
||||
s.getListParams = vi.fn();
|
||||
s.getListData = vi.fn();
|
||||
s.getRoomSubscriptions = vi.fn();
|
||||
s.modifyRoomSubscriptionInfo = vi.fn();
|
||||
s.modifyRoomSubscriptions = vi.fn();
|
||||
s.registerExtension = vi.fn();
|
||||
s.setList = vi.fn();
|
||||
s.setListRanges = vi.fn();
|
||||
s.start = vi.fn();
|
||||
s.stop = vi.fn();
|
||||
s.resend = vi.fn();
|
||||
return s;
|
||||
};
|
||||
|
||||
@@ -111,7 +111,7 @@ describe("SlidingSyncSdk", () => {
|
||||
expect(m.getType()).toEqual(want[i].type);
|
||||
expect(m.getSender()).toEqual(want[i].sender);
|
||||
expect(m.getId()).toEqual(want[i].event_id);
|
||||
expect(m.getContent()).toEqual(want[i].content);
|
||||
expect(m.getContent<IContent>()).toEqual(want[i].content);
|
||||
expect(m.getTs()).toEqual(want[i].origin_server_ts);
|
||||
if (want[i].unsigned) {
|
||||
expect(m.getUnsigned()).toEqual(want[i].unsigned);
|
||||
@@ -150,7 +150,7 @@ describe("SlidingSyncSdk", () => {
|
||||
// find an extension on a SlidingSyncSdk instance
|
||||
const findExtension = (name: string): Extension<any, any> => {
|
||||
expect(mockSlidingSync!.registerExtension).toHaveBeenCalled();
|
||||
const mockFn = mockSlidingSync!.registerExtension as jest.Mock;
|
||||
const mockFn = vi.mocked(mockSlidingSync!.registerExtension);
|
||||
// find the extension
|
||||
for (let i = 0; i < mockFn.mock.calls.length; i++) {
|
||||
const calledExtension = mockFn.mock.calls[i][0] as Extension<any, any>;
|
||||
@@ -658,7 +658,7 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("can update device lists", () => {
|
||||
syncCryptoCallback!.processDeviceLists = jest.fn();
|
||||
syncCryptoCallback!.processDeviceLists = vi.fn();
|
||||
ext.onResponse({
|
||||
device_lists: {
|
||||
changed: ["@alice:localhost"],
|
||||
@@ -672,7 +672,7 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("can update OTK counts and unused fallback keys", () => {
|
||||
syncCryptoCallback!.processKeyCounts = jest.fn();
|
||||
syncCryptoCallback!.processKeyCounts = vi.fn();
|
||||
ext.onResponse({
|
||||
device_one_time_keys_count: {
|
||||
signed_curve25519: 42,
|
||||
@@ -722,7 +722,7 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
globalData = client!.getAccountData(globalType)!;
|
||||
expect(globalData).toBeTruthy();
|
||||
expect(globalData.getContent()).toEqual(globalContent);
|
||||
expect(globalData.getContent<IContent>()).toEqual(globalContent);
|
||||
});
|
||||
|
||||
it("processes rooms account data", async () => {
|
||||
@@ -757,7 +757,7 @@ describe("SlidingSyncSdk", () => {
|
||||
expect(room).toBeTruthy();
|
||||
const event = room.getAccountData(roomType)!;
|
||||
expect(event).toBeTruthy();
|
||||
expect(event.getContent()).toEqual(roomContent);
|
||||
expect(event.getContent<IContent>()).toEqual(roomContent);
|
||||
});
|
||||
|
||||
it("doesn't crash for unknown room account data", async () => {
|
||||
@@ -847,6 +847,7 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("can handle missing fields", async () => {
|
||||
ext.onResponse({
|
||||
next_batch: "23456",
|
||||
@@ -861,7 +862,7 @@ describe("SlidingSyncSdk", () => {
|
||||
};
|
||||
let called = false;
|
||||
client!.once(ClientEvent.ToDeviceEvent, (ev) => {
|
||||
expect(ev.getContent()).toEqual(toDeviceContent);
|
||||
expect(ev.getContent<IContent>()).toEqual(toDeviceContent);
|
||||
expect(ev.getType()).toEqual(toDeviceType);
|
||||
called = true;
|
||||
});
|
||||
@@ -1095,6 +1096,7 @@ describe("SlidingSyncSdk", () => {
|
||||
expect(receipt?.data.thread_id).toBeFalsy();
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("gracefully handles missing rooms when receiving receipts", async () => {
|
||||
const roomId = "!room:id";
|
||||
const alice = "@alice:alice";
|
||||
|
||||
@@ -82,6 +82,7 @@ describe("SlidingSync", () => {
|
||||
await p;
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should stop the sync loop upon calling stop()", () => {
|
||||
slidingSync.stop();
|
||||
httpBackend!.verifyNoOutstandingExpectation();
|
||||
|
||||
+13
-4
@@ -14,13 +14,22 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
jest.mock("../src/http-api/utils", () => ({
|
||||
...jest.requireActual("../src/http-api/utils"),
|
||||
import fetchMock, { manageFetchMockGlobally } from "@fetch-mock/vitest";
|
||||
|
||||
vi.mock("../src/http-api/utils", async () => ({
|
||||
...(await vi.importActual("../src/http-api/utils")),
|
||||
// We mock timeoutSignal otherwise it causes tests to leave timers running
|
||||
timeoutSignal: () => new AbortController().signal,
|
||||
}));
|
||||
|
||||
// Dont make test fail too soon due to timeouts while debugging.
|
||||
manageFetchMockGlobally();
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.hardReset();
|
||||
fetchMock.mockGlobal();
|
||||
});
|
||||
|
||||
// Don't make test fail too soon due to timeouts while debugging.
|
||||
if (process.env.VSCODE_INSPECTOR_OPTIONS) {
|
||||
jest.setTimeout(60 * 1000 * 5); // 5 minutes
|
||||
vi.setConfig({ testTimeout: 60 * 1000 * 5 }); // 5 minutes
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
|
||||
import { type ISyncResponder } from "./SyncResponder";
|
||||
|
||||
@@ -36,65 +36,80 @@ export class AccountDataAccumulator {
|
||||
|
||||
public constructor(private syncResponder: ISyncResponder) {}
|
||||
|
||||
private accountDataResolvers = new Map<string, PromiseWithResolvers<any>>();
|
||||
private setInterceptRunning = false;
|
||||
|
||||
/**
|
||||
* Intercept requests to set a particular type of account data.
|
||||
* Intercept setting of account data.
|
||||
*
|
||||
* Once it is set, its data is stored (for future return by `interceptGetAccountData` etc) and the resolved promise is
|
||||
* resolved.
|
||||
*
|
||||
* @param accountDataType - type of account data to be intercepted
|
||||
* @param opts - options to pass to fetchMock
|
||||
* @returns a Promise which will resolve (with the content of the account data) once it is set.
|
||||
*/
|
||||
public interceptSetAccountData(
|
||||
accountDataType: string,
|
||||
opts?: Parameters<(typeof fetchMock)["put"]>[2],
|
||||
): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
// Called when the cross signing key is uploaded
|
||||
fetchMock.put(
|
||||
`express:/_matrix/client/v3/user/:userId/account_data/${accountDataType}`,
|
||||
(url: string, options: RequestInit) => {
|
||||
const content = JSON.parse(options.body as string);
|
||||
const type = url.split("/").pop();
|
||||
// update account data for sync response
|
||||
this.accountDataEvents.set(type!, content);
|
||||
resolve(content);
|
||||
public interceptSetAccountData(): void {
|
||||
if (this.setInterceptRunning) return;
|
||||
this.setInterceptRunning = true;
|
||||
|
||||
// return a sync response
|
||||
this.sendSyncResponseWithUpdatedAccountData();
|
||||
return {};
|
||||
},
|
||||
opts,
|
||||
);
|
||||
fetchMock.put(`express:/_matrix/client/v3/user/:userId/account_data/:type`, (callLog) => {
|
||||
const content = JSON.parse(callLog.options.body as string);
|
||||
const type = callLog.url.split("/").pop();
|
||||
// update account data for sync response
|
||||
this.accountDataEvents.set(type!, content);
|
||||
|
||||
this.accountDataResolvers.get(type!)?.resolve(content);
|
||||
if (!this.accountDataResolvers.delete(type!)) {
|
||||
// Check for a wildcard matcher
|
||||
for (const [key, resolver] of this.accountDataResolvers) {
|
||||
if (key.endsWith("*") && type?.startsWith(key.slice(0, -1))) {
|
||||
resolver.resolve(content);
|
||||
this.accountDataResolvers.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return a sync response
|
||||
this.sendSyncResponseWithUpdatedAccountData();
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a particular type of account data.
|
||||
*
|
||||
* Once it is set, its data is stored (for future return by `interceptGetAccountData` etc) and the resolved promise is
|
||||
* resolved.
|
||||
*
|
||||
* @returns a Promise which will resolve (with the content of the account data) once it is set.
|
||||
*/
|
||||
public waitForAccountData(type: string): Promise<any> {
|
||||
const resolvers = Promise.withResolvers<any>();
|
||||
this.accountDataResolvers.set(type, resolvers);
|
||||
this.interceptSetAccountData();
|
||||
return resolvers.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercept all requests to get account data
|
||||
*/
|
||||
public interceptGetAccountData(): void {
|
||||
fetchMock.get(
|
||||
`express:/_matrix/client/v3/user/:userId/account_data/:type`,
|
||||
(url) => {
|
||||
const type = url.split("/").pop();
|
||||
const existing = this.accountDataEvents.get(type!);
|
||||
if (existing) {
|
||||
// return it
|
||||
return {
|
||||
status: 200,
|
||||
body: existing,
|
||||
};
|
||||
} else {
|
||||
// 404
|
||||
return {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
|
||||
};
|
||||
}
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
fetchMock.get(`express:/_matrix/client/v3/user/:userId/account_data/:type`, (callLog) => {
|
||||
const type = callLog.url.split("/").pop();
|
||||
const existing = this.accountDataEvents.get(type!);
|
||||
if (existing) {
|
||||
// return it
|
||||
return {
|
||||
status: 200,
|
||||
body: existing,
|
||||
};
|
||||
} else {
|
||||
// 404
|
||||
return {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
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 { CrossSigningKeys, ISignedKey, KeySignatures } from "../../src";
|
||||
@@ -81,26 +81,27 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
|
||||
|
||||
// set up a listener for /keys/upload.
|
||||
this.oneTimeKeysPromise = new Promise((resolveOneTimeKeys) => {
|
||||
const listener = (url: string, options: RequestInit) =>
|
||||
this.onKeyUploadRequest(resolveOneTimeKeys, options);
|
||||
|
||||
fetchMock.post(new URL("/_matrix/client/v3/keys/upload", homeserverUrl).toString(), listener);
|
||||
fetchMock.post(
|
||||
new URL("/_matrix/client/v3/keys/upload", homeserverUrl).toString(),
|
||||
(callLog) => this.onKeyUploadRequest(resolveOneTimeKeys, callLog.options),
|
||||
{ name: routeNamePrefix + "keys-upload" },
|
||||
);
|
||||
});
|
||||
|
||||
fetchMock.post(
|
||||
new URL("/_matrix/client/v3/keys/signatures/upload", homeserverUrl).toString(),
|
||||
(callLog) => this.onSignaturesUploadRequest(callLog.options),
|
||||
{
|
||||
url: new URL("/_matrix/client/v3/keys/signatures/upload", homeserverUrl).toString(),
|
||||
name: routeNamePrefix + "upload-sigs",
|
||||
},
|
||||
(url, options) => this.onSignaturesUploadRequest(options),
|
||||
);
|
||||
|
||||
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",
|
||||
},
|
||||
(url, options) => this.onSigningKeyUploadRequest(options),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
|
||||
import { MapWithDefault } from "../../src/utils";
|
||||
import { type IDownloadKeyResult, type SigningKeys } from "../../src";
|
||||
@@ -42,8 +42,9 @@ export class E2EKeyResponder {
|
||||
*/
|
||||
public constructor(homeserverUrl: string) {
|
||||
// set up a listener for /keys/query.
|
||||
const listener = (url: string, options: RequestInit) => this.onKeyQueryRequest(options);
|
||||
fetchMock.post(new URL("/_matrix/client/v3/keys/query", homeserverUrl).toString(), listener);
|
||||
fetchMock.post(new URL("/_matrix/client/v3/keys/query", homeserverUrl).toString(), (callLog) =>
|
||||
this.onKeyQueryRequest(callLog.options),
|
||||
);
|
||||
}
|
||||
|
||||
private onKeyQueryRequest(options: RequestInit) {
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
|
||||
import { MapWithDefault } from "../../src/utils";
|
||||
import { type E2EKeyReceiver } from "./E2EKeyReceiver";
|
||||
@@ -36,8 +36,9 @@ export class E2EOTKClaimResponder {
|
||||
* @param homeserverUrl - the Homeserver Url of the client under test.
|
||||
*/
|
||||
public constructor(homeserverUrl: string) {
|
||||
const listener = (url: string, options: RequestInit) => this.onKeyClaimRequest(options);
|
||||
fetchMock.post(new URL("/_matrix/client/v3/keys/claim", homeserverUrl).toString(), listener);
|
||||
fetchMock.post(new URL("/_matrix/client/v3/keys/claim", homeserverUrl).toString(), (callLog) =>
|
||||
this.onKeyClaimRequest(callLog.options),
|
||||
);
|
||||
}
|
||||
|
||||
private onKeyClaimRequest(options: RequestInit) {
|
||||
|
||||
@@ -16,9 +16,8 @@ limitations under the License.
|
||||
|
||||
import debugFunc from "debug";
|
||||
import { type Debugger } from "debug";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import type FetchMock from "fetch-mock";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import { type RouteResponse } from "fetch-mock";
|
||||
|
||||
/** Interface implemented by classes that intercept `/sync` requests from test clients
|
||||
*
|
||||
@@ -76,12 +75,12 @@ export class SyncResponder implements ISyncResponder {
|
||||
*/
|
||||
public constructor(homeserverUrl: string) {
|
||||
this.debug = debugFunc(`sync-responder:[${homeserverUrl}]`);
|
||||
fetchMock.get("begin:" + new URL("/_matrix/client/v3/sync?", homeserverUrl).toString(), (_url, _options) =>
|
||||
fetchMock.get("begin:" + new URL("/_matrix/client/v3/sync?", homeserverUrl).toString(), (callLog) =>
|
||||
this.onSyncRequest(),
|
||||
);
|
||||
}
|
||||
|
||||
private async onSyncRequest(): Promise<FetchMock.MockResponse> {
|
||||
private async onSyncRequest(): Promise<RouteResponse> {
|
||||
switch (this.state) {
|
||||
case SyncResponderState.IDLE: {
|
||||
this.debug("Got /sync request: waiting for response to be ready");
|
||||
|
||||
+23
-17
@@ -14,12 +14,18 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type MethodLikeKeys, mocked, type MockedObject } from "jest-mock";
|
||||
import { type MockedObject } from "vitest";
|
||||
|
||||
import { type ClientEventHandlerMap, type EmittedEvents, type MatrixClient } from "../../src/client";
|
||||
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
|
||||
import { User } from "../../src/models/user";
|
||||
|
||||
// Cribbed from https://github.com/jestjs/jest/blob/94830794dc5dfca1b49bc435b7b031b27838a798/packages/jest-mock/src/index.ts
|
||||
type FunctionLike = (...args: any) => any;
|
||||
type MethodLikeKeys<T> = keyof {
|
||||
[K in keyof T as Required<T>[K] extends FunctionLike ? K : never]: T[K];
|
||||
};
|
||||
|
||||
/**
|
||||
* Mock client with real event emitter
|
||||
* useful for testing code that listens
|
||||
@@ -34,19 +40,19 @@ export class MockClientWithEventEmitter extends TypedEventEmitter<EmittedEvents,
|
||||
|
||||
/**
|
||||
* - make a mock client
|
||||
* - cast the type to mocked(MatrixClient)
|
||||
* - cast the type to vi.mocked(MatrixClient)
|
||||
* - spy on MatrixClientPeg.get to return the mock
|
||||
* eg
|
||||
* ```
|
||||
* const mockClient = getMockClientWithEventEmitter({
|
||||
getUserId: jest.fn().mockReturnValue(aliceId),
|
||||
getUserId: vi.fn().mockReturnValue(aliceId),
|
||||
});
|
||||
* ```
|
||||
*/
|
||||
export const getMockClientWithEventEmitter = (
|
||||
mockProperties: Partial<Record<MethodLikeKeys<MatrixClient>, unknown>>,
|
||||
): MockedObject<MatrixClient> => {
|
||||
const mock = mocked(new MockClientWithEventEmitter(mockProperties) as unknown as MatrixClient);
|
||||
const mock = vi.mocked(new MockClientWithEventEmitter(mockProperties) as unknown as MatrixClient);
|
||||
return mock;
|
||||
};
|
||||
|
||||
@@ -59,14 +65,14 @@ export const getMockClientWithEventEmitter = (
|
||||
* ```
|
||||
*/
|
||||
export const mockClientMethodsUser = (userId = "@alice:domain") => ({
|
||||
getUserId: jest.fn().mockReturnValue(userId),
|
||||
getSafeUserId: jest.fn().mockReturnValue(userId),
|
||||
getUser: jest.fn().mockReturnValue(new User(userId)),
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
|
||||
getUserId: vi.fn().mockReturnValue(userId),
|
||||
getSafeUserId: vi.fn().mockReturnValue(userId),
|
||||
getUser: vi.fn().mockReturnValue(new User(userId)),
|
||||
isGuest: vi.fn().mockReturnValue(false),
|
||||
mxcUrlToHttp: vi.fn().mockReturnValue("mock-mxcUrlToHttp"),
|
||||
credentials: { userId },
|
||||
getThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
|
||||
getAccessToken: jest.fn(),
|
||||
getThreePids: vi.fn().mockResolvedValue({ threepids: [] }),
|
||||
getAccessToken: vi.fn(),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -78,16 +84,16 @@ export const mockClientMethodsUser = (userId = "@alice:domain") => ({
|
||||
* ```
|
||||
*/
|
||||
export const mockClientMethodsEvents = () => ({
|
||||
decryptEventIfNeeded: jest.fn(),
|
||||
getPushActionsForEvent: jest.fn(),
|
||||
decryptEventIfNeeded: vi.fn(),
|
||||
getPushActionsForEvent: vi.fn(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns basic mocked client methods related to server support
|
||||
*/
|
||||
export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({
|
||||
getIdentityServerUrl: jest.fn(),
|
||||
getHomeserverUrl: jest.fn(),
|
||||
getCachedCapabilities: jest.fn().mockReturnValue({}),
|
||||
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
|
||||
getIdentityServerUrl: vi.fn(),
|
||||
getHomeserverUrl: vi.fn(),
|
||||
getCachedCapabilities: vi.fn().mockReturnValue({}),
|
||||
doesServerSupportUnstableFeature: vi.fn().mockResolvedValue(false),
|
||||
});
|
||||
|
||||
@@ -14,15 +14,17 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type MockInstance } from "vitest";
|
||||
|
||||
/**
|
||||
* Filter emitter.emit mock calls to find relevant events
|
||||
* eg:
|
||||
* ```
|
||||
* const emitSpy = jest.spyOn(state, 'emit');
|
||||
* const emitSpy = vi.spyOn(state, 'emit');
|
||||
* << actions >>
|
||||
* const beaconLivenessEmits = emitCallsByEventType(BeaconEvent.New, emitSpy);
|
||||
* expect(beaconLivenessEmits.length).toBe(1);
|
||||
* ```
|
||||
*/
|
||||
export const filterEmitCallsByEventType = (eventType: string, spy: jest.SpyInstance<any, any[]>) =>
|
||||
export const filterEmitCallsByEventType = (eventType: string, spy: MockInstance<(...args: any[]) => any>) =>
|
||||
spy.mock.calls.filter((args) => args[0] === eventType);
|
||||
|
||||
@@ -14,12 +14,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Jest now uses @sinonjs/fake-timers which exposes tickAsync() and a number of
|
||||
// other async methods which break the event loop, letting scheduled promise
|
||||
// callbacks run. Unfortunately, Jest doesn't expose these, so we have to do
|
||||
// it manually (this is what sinon does under the hood). We do both in a loop
|
||||
// until the thing we expect happens: hopefully this is the least flakey way
|
||||
// and avoids assuming anything about the app's behaviour.
|
||||
// Vitest lacks tickAsync() and a number of other async methods which break the event loop,
|
||||
// letting scheduled promise callbacks run. So we have to do it manually
|
||||
// (this is what sinon does under the hood). We do both in a loop until the thing we expect happens:
|
||||
// hopefully this is the least flakey way and avoids assuming anything about the app's behaviour.
|
||||
const realSetTimeout = setTimeout;
|
||||
export function flushPromises() {
|
||||
return new Promise((r) => {
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
|
||||
import { type KeyBackupInfo } from "../../src/crypto-api";
|
||||
|
||||
@@ -25,20 +25,15 @@ import { type KeyBackupInfo } from "../../src/crypto-api";
|
||||
* @param userId - the local user's ID. Defaults to `@alice:localhost`.
|
||||
*/
|
||||
export function mockInitialApiRequests(homeserverUrl: string, userId: string = "@alice:localhost") {
|
||||
fetchMock.getOnce(
|
||||
new URL("/_matrix/client/versions", homeserverUrl).toString(),
|
||||
{ versions: ["v1.1"] },
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
fetchMock.getOnce(
|
||||
new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(),
|
||||
{},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
fetchMock.getOnce(new URL("/_matrix/client/versions", homeserverUrl).toString(), { versions: ["v1.1"] });
|
||||
fetchMock.getOnce(new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(), {});
|
||||
fetchMock.postOnce(
|
||||
new URL(`/_matrix/client/v3/user/${encodeURIComponent(userId)}/filter`, homeserverUrl).toString(),
|
||||
{ filter_id: "fid" },
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
fetchMock.getOnce(
|
||||
new URL(`/_matrix/client/v3/user/${encodeURIComponent(userId)}/filter/fid`, homeserverUrl).toString(),
|
||||
{ filter_id: "fid" },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,24 +60,30 @@ export function mockSetupCrossSigningRequests(): void {
|
||||
* @param backupVersion - The backup version that will be returned by `POST room_keys/version`.
|
||||
*/
|
||||
export function mockSetupMegolmBackupRequests(backupVersion: string): void {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "No current backup version",
|
||||
fetchMock.get(
|
||||
"path:/_matrix/client/v3/room_keys/version",
|
||||
{
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "No current backup version",
|
||||
},
|
||||
},
|
||||
});
|
||||
{ name: "room-keys-version" },
|
||||
);
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/room_keys/version", (url, request) => {
|
||||
const backupData: KeyBackupInfo = JSON.parse((request.body as string) ?? "{}");
|
||||
backupData.version = backupVersion;
|
||||
backupData.count = 0;
|
||||
backupData.etag = "zer";
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
return {
|
||||
version: backupVersion,
|
||||
};
|
||||
});
|
||||
fetchMock.post(
|
||||
"path:/_matrix/client/v3/room_keys/version",
|
||||
(callLog) => {
|
||||
const backupData: KeyBackupInfo = JSON.parse((callLog.options.body as string) ?? "{}");
|
||||
backupData.version = backupVersion;
|
||||
backupData.count = 0;
|
||||
backupData.etag = "zer";
|
||||
fetchMock.modifyRoute("room-keys-version", { response: backupData });
|
||||
return {
|
||||
version: backupVersion,
|
||||
};
|
||||
},
|
||||
{ name: "post-room-keys-version" },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -84,9 +84,9 @@ def main() -> None:
|
||||
* Do not edit by hand! This file is generated by `./generate-test-data.py`
|
||||
*/
|
||||
|
||||
import {{ IDeviceKeys, IMegolmSessionData }} from "../../../src/@types/crypto";
|
||||
import {{ IDownloadKeyResult, IEvent }} from "../../../src";
|
||||
import {{ KeyBackupSession, KeyBackupInfo }} from "../../../src/crypto-api/keybackup";
|
||||
import type {{ IDeviceKeys, IMegolmSessionData }} from "../../../src/@types/crypto";
|
||||
import type {{ IDownloadKeyResult, IEvent }} from "../../../src";
|
||||
import type {{ KeyBackupSession, KeyBackupInfo, KeyBackupRoomSessions }} from "../../../src/crypto-api/keybackup";
|
||||
|
||||
/* eslint-disable comma-dangle */
|
||||
|
||||
@@ -246,15 +246,6 @@ export const {prefix}SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult>
|
||||
/** Signed OTKs, returned by `POST /keys/claim` */
|
||||
export const {prefix}ONE_TIME_KEYS = { json.dumps(otks, indent=4) };
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const {prefix}BACKUP_DECRYPTION_KEY_BASE64 = "{ user_data['B64_BACKUP_DECRYPTION_KEY'] }";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const {prefix}BACKUP_DECRYPTION_KEY_BASE58 = "{ backup_recovery_key }";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}/{{sessionId}}` */
|
||||
export const {prefix}SIGNED_BACKUP_DATA: KeyBackupInfo = { json.dumps(backup_data, indent=4) };
|
||||
|
||||
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
|
||||
export const {prefix}MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = {
|
||||
json.dumps(set_of_exported_room_keys, indent=4)
|
||||
@@ -278,6 +269,23 @@ export const {prefix}CLEAR_EVENT: Partial<IEvent> = {json.dumps(clear_event, ind
|
||||
|
||||
/** The encrypted CLEAR_EVENT by MEGOLM_SESSION_DATA */
|
||||
export const {prefix}ENCRYPTED_EVENT: Partial<IEvent> = {json.dumps(encrypted_event, indent=4)};
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const {prefix}BACKUP_DECRYPTION_KEY_BASE64 = "{ user_data['B64_BACKUP_DECRYPTION_KEY'] }";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const {prefix}BACKUP_DECRYPTION_KEY_BASE58 = "{ backup_recovery_key }";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}/{{sessionId}}` */
|
||||
export const {prefix}SIGNED_BACKUP_DATA: KeyBackupInfo = { json.dumps(backup_data, indent=4) };
|
||||
|
||||
/**
|
||||
* Per-room backup data, (supposedly) suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}`.
|
||||
* Contains the key from {prefix}MEGOLM_SESSION_DATA.
|
||||
*/
|
||||
export const {prefix}PER_ROOM_CURVE25519_KEY_BACKUP_DATA: KeyBackupRoomSessions = {{
|
||||
[{prefix}MEGOLM_SESSION_DATA.session_id]: {prefix}CURVE25519_KEY_BACKUP_DATA
|
||||
}};
|
||||
"""
|
||||
|
||||
alt_master_key = user_data.get("ALT_MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES")
|
||||
@@ -385,7 +393,7 @@ def sign_json(json_object: dict, private_key: ed25519.Ed25519PrivateKey) -> str:
|
||||
def build_exported_megolm_key(device_curve_key: x25519.X25519PrivateKey) -> tuple[dict, ed25519.Ed25519PrivateKey]:
|
||||
"""
|
||||
Creates an exported megolm room key, as per https://gitlab.matrix.org/matrix-org/olm/blob/master/docs/megolm.md#session-export-format
|
||||
that can be imported via importRoomKeys API.
|
||||
that can be imported via importRoomKeys API, or shared via MSC4268 room history sharing.
|
||||
Returns the exported key, the matching privat edKey (needed to encrypt)
|
||||
"""
|
||||
index = 0
|
||||
@@ -409,11 +417,12 @@ def build_exported_megolm_key(device_curve_key: x25519.X25519PrivateKey) -> tupl
|
||||
"session_id": encode_base64(
|
||||
private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
),
|
||||
"session_key": encode_base64(exported_key),
|
||||
"session_key": encode_base64(bytes(exported_key)),
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": encode_base64(ed25519.Ed25519PrivateKey.from_private_bytes(randbytes(32)).public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)),
|
||||
},
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"m.shared_history": True,
|
||||
}
|
||||
|
||||
return megolm_export, private_key
|
||||
@@ -458,7 +467,7 @@ def symetric_ratchet_step_of_megolm_key(previous: dict , megolm_private_key: ed2
|
||||
"room_id": "!room:id",
|
||||
"sender_key": previous["sender_key"],
|
||||
"session_id": previous["session_id"],
|
||||
"session_key": encode_base64(exported_key),
|
||||
"session_key": encode_base64(bytes(exported_key)),
|
||||
"sender_claimed_keys": previous["sender_claimed_keys"],
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
}
|
||||
@@ -609,7 +618,7 @@ def generate_encrypted_event_content(exported_key: dict, ed_key: ed25519.Ed25519
|
||||
|
||||
message += signature
|
||||
|
||||
cipher_text = encode_base64(message)
|
||||
cipher_text = encode_base64(bytes(message))
|
||||
|
||||
encrypted_payload = {
|
||||
"algorithm" : "m.megolm.v1.aes-sha2",
|
||||
@@ -653,7 +662,7 @@ def export_recovery_key(key_b64: str) -> str:
|
||||
export_bytes += parity_byte.to_bytes(1, 'big')
|
||||
|
||||
# The byte string is encoded using base58
|
||||
recovery_key = base58.b58encode(export_bytes).decode('utf-8')
|
||||
recovery_key = base58.b58encode(bytes(export_bytes)).decode('utf-8')
|
||||
|
||||
split = [recovery_key[i:i + 4] for i in range(0, len(recovery_key), 4)]
|
||||
return ' '.join(split)
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* Do not edit by hand! This file is generated by `./generate-test-data.py`
|
||||
*/
|
||||
|
||||
import { type IDeviceKeys, type IMegolmSessionData } from "../../../src/@types/crypto";
|
||||
import { type IDownloadKeyResult, type IEvent } from "../../../src";
|
||||
import { type KeyBackupSession, type KeyBackupInfo } from "../../../src/crypto-api/keybackup";
|
||||
import type { IDeviceKeys, IMegolmSessionData } from "../../../src/@types/crypto";
|
||||
import type { IDownloadKeyResult, IEvent } from "../../../src";
|
||||
import type { KeyBackupSession, KeyBackupInfo, KeyBackupRoomSessions } from "../../../src/crypto-api/keybackup";
|
||||
|
||||
/* eslint-disable comma-dangle */
|
||||
|
||||
@@ -118,26 +118,6 @@ export const ONE_TIME_KEYS = {
|
||||
}
|
||||
};
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const BACKUP_DECRYPTION_KEY_BASE64 = "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const BACKUP_DECRYPTION_KEY_BASE58 = "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
|
||||
export const SIGNED_BACKUP_DATA: KeyBackupInfo = {
|
||||
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
"version": "1",
|
||||
"auth_data": {
|
||||
"public_key": "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
"signatures": {
|
||||
"@alice:localhost": {
|
||||
"ed25519:test_device": "KDSNeumirTsd8piI0oVfv/wzg4J4HlEc7rs5XhODFcJ/YAcUdg65ajsZG+rLI0TQOSSGjorJqcrSiSB1HRSCAA"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
|
||||
export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||
{
|
||||
@@ -149,7 +129,8 @@ export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "QdgHgdpDgihgovpPzUiThXur1fbErTFh7paFvNKSgN0"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"org.matrix.msc3061.shared_history": true
|
||||
},
|
||||
{
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
@@ -160,7 +141,8 @@ export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "IrkbT6H+0urDf6wKDSyVC1fh1t84Vz6T62snni86Cog"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"org.matrix.msc3061.shared_history": true
|
||||
}
|
||||
];
|
||||
|
||||
@@ -174,7 +156,8 @@ export const MEGOLM_SESSION_DATA: IMegolmSessionData = {
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "Bhbpt6hqMZlSH4sJV7xiEEEiPVeTWz4Vkujl1EMdIPI"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"org.matrix.msc3061.shared_history": true
|
||||
};
|
||||
|
||||
/** A ratcheted version of MEGOLM_SESSION_DATA */
|
||||
@@ -196,7 +179,7 @@ export const CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {
|
||||
"forwarded_count": 0,
|
||||
"is_verified": false,
|
||||
"session_data": {
|
||||
"ciphertext": "r6HRk2/Im2yJe5cLP8R81aVjFWjYWPHpw7TVxphiSK1cdIDZTTK57r6MfU+0i/mTPn+/PosT74OvYwCnehy2d1BPGxhDl8AhPcBu3//Kzlq2o5CssPsw+88gRehkAsPg9Zp5G9sL9to6giltvTWTbsaQpmvv3HLmBOYSFIxvyZrOT/Ffqu325f0IEsKcyV2BdIkw8Ob9Xt+VWoe4MYEGG6y1T8W125zeFgKWI4Ow76uput64H9zZjIo+Cc+hCTO9Ea4EnosSjizCotevkNck7C/zGgfhBikiohROb6SbaZgxicSsEDZ+f7brnri9yP3iXS3PMDHHpa1+XzG2VOG/Y9OQZpkPq+pbLrCC+NWJeJPslDAK5i+RURwzjnPmaHKCRHTq86CwhFyiCDf61MGwCY3xjrmBJg44BCdxWqCx0YJvwsvVqqnl4vTieUfrwThNPsQ81aVkDHvlmrgrTt8icDa8jTJhu34jem+pbRSEM5aJikV4B+zYiLz+dH/v6UpYA2eG8ReOvwpPXp6CAcIlplRPpWbMBeLFVcPkT4KAXTp9exFpB4on4pf8OsaDomlt4qAA0rhAZmhPWPKcU/A0Tz4gyMu54OivVtw1SPj+5Iq+YDQ8jB6Po3ApzMf6fwF9x/FjevbboFB05X2Jr0NrbFqXMOUwXHMgDAGiIWX8+gkmmbaiNWqg2etjN94pobQSGZelb18XGN7kuwMk+Zwk7A",
|
||||
"ciphertext": "r6HRk2/Im2yJe5cLP8R81aVjFWjYWPHpw7TVxphiSK1cdIDZTTK57r6MfU+0i/mTPn+/PosT74OvYwCnehy2d/r0NTff1SQt+1GopZkT0nq6jF5Wh/oX+8iwtYjHvTxMpN1UQoXAvRF40O+EVg+Q3efJXh1t45cMco8EWU64VerOir+k7cQ3C9FtcgQw3kmz3s3HeVY10o13X/w6+rc8n6vXqxuIxYHnFxanxX8B6TgTMZNajNfVsmJV0aC1aezim7E2gsftc+6+zW5G+rCFaEsWV/IuSOUz0+Hh0U+7hzSrz9/4qXPEVmPy1f6Ll4hhquPAlXPVDwddqlJDYj7kmvzr1g3bKVpk+TtKDbWlVQDPaJx2DEI2jGkPYjhYb7okpTFKpUny94dZmFIQqCeSGPIniaq8Y+/CanugQ1ZRVQcThuXrTewqWhXcpVvkVHT9i4ImcpBl95HzCBXuiwSUv6FKvO25fp++w555rbn2piFtilrUwnkrZPW32jFuaQcKZF4mZwcLeH7POL5UCuS4TWyaKyArp7bRzXwWuIq1wPET2nAMUmUVL7ge2+tAevk1WOIsjLgSaz/g55wO3Yma7yhXRFKcnzTjS0hUQOZ3GfTNwCM4pjzAtIPzvVd4Fp0b1emWZS5WyOYdXsceEDi3c6WtkoHWOKhPU0zBzn8hA9TdlFFqKzf2QFbN5Zgg0gprDLnLWgpc3/ieI4C7ndEQ7ZeTNMXbT/Y10APFk3qO+IGkLXJ97/qTF41EXFDhlsL0",
|
||||
"ephemeral": "q+P1WdRtEiPIEtNuuGrRcueZxUbLnSKdsuTAkxewXgU",
|
||||
"mac": "OibmACbORhI"
|
||||
}
|
||||
@@ -229,6 +212,37 @@ export const ENCRYPTED_EVENT: Partial<IEvent> = {
|
||||
"origin_server_ts": 1507753886000
|
||||
};
|
||||
|
||||
/** base64-encoded backup decryption (private) key that matches the public key in CURVE25519_KEY_BACKUP_DATA */
|
||||
export const BACKUP_DECRYPTION_KEY_BASE64 = "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
|
||||
|
||||
/** base64-encoded backup decryption (private) key that does not match the public key in CURVE25519_KEY_BACKUP_DATA */
|
||||
export const BACKUP_DECRYPTION_KEY_BASE64_ALT = "dh4fP2LITyJusgnb0dEq/SQK253WGObvLxXF5FEX6qc";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const BACKUP_DECRYPTION_KEY_BASE58 = "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
|
||||
export const SIGNED_BACKUP_DATA: KeyBackupInfo = {
|
||||
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
"version": "1",
|
||||
"auth_data": {
|
||||
"public_key": "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
"signatures": {
|
||||
"@alice:localhost": {
|
||||
"ed25519:test_device": "KDSNeumirTsd8piI0oVfv/wzg4J4HlEc7rs5XhODFcJ/YAcUdg65ajsZG+rLI0TQOSSGjorJqcrSiSB1HRSCAA"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-room backup data, (supposedly) suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}`.
|
||||
* Contains the key from MEGOLM_SESSION_DATA.
|
||||
*/
|
||||
export const PER_ROOM_CURVE25519_KEY_BACKUP_DATA: KeyBackupRoomSessions = {
|
||||
[MEGOLM_SESSION_DATA.session_id]: CURVE25519_KEY_BACKUP_DATA
|
||||
};
|
||||
|
||||
// Bob data
|
||||
|
||||
export const BOB_TEST_USER_ID = "@bob:xyz";
|
||||
@@ -338,26 +352,6 @@ export const BOB_ONE_TIME_KEYS = {
|
||||
}
|
||||
};
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const BOB_BACKUP_DECRYPTION_KEY_BASE64 = "DwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const BOB_BACKUP_DECRYPTION_KEY_BASE58 = "EsT5 Sd5m mEXs NQYE ibRe 3q9E 4aXW rHih 5f9J 6rU6 AfwY mASR";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
|
||||
export const BOB_SIGNED_BACKUP_DATA: KeyBackupInfo = {
|
||||
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
"version": "1",
|
||||
"auth_data": {
|
||||
"public_key": "ZRuVWcWlDuvOwZRygccUCD4Avtnt130800I+WQNwwRY",
|
||||
"signatures": {
|
||||
"@bob:xyz": {
|
||||
"ed25519:bob_device": "lDIMj3VC0WazE2FamGHpmbiqKf9Z4pO4qapZ5TL5BnD3c+dvb+2waOEd6pgay/pmrQ6MW4Eu2KDEpe1fnHc3BA"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
|
||||
export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||
{
|
||||
@@ -369,7 +363,8 @@ export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "F4P7f1Z0RjbiZMgHk1xBCG3KC4/Ng9PmxLJ4hQ13sHA"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"org.matrix.msc3061.shared_history": true
|
||||
},
|
||||
{
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
@@ -380,7 +375,8 @@ export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "OsZMdC1gQ5nPr+L9tuT6xXsaFJkVPkgxP2FexHF1/QM"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"org.matrix.msc3061.shared_history": true
|
||||
}
|
||||
];
|
||||
|
||||
@@ -394,7 +390,8 @@ export const BOB_MEGOLM_SESSION_DATA: IMegolmSessionData = {
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "zBdpQwWYyz1MkZuEUhXqcdMfUNN/B9psLFDDDTJOg64"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"org.matrix.msc3061.shared_history": true
|
||||
};
|
||||
|
||||
/** A ratcheted version of BOB_MEGOLM_SESSION_DATA */
|
||||
@@ -416,7 +413,7 @@ export const BOB_CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {
|
||||
"forwarded_count": 0,
|
||||
"is_verified": false,
|
||||
"session_data": {
|
||||
"ciphertext": "d7UVOK17WEVky/8hK0h3HsTQrFMEbKbfqMcl2KtyTWcI9S5gGFWK9Git5BzVRxRggvxQ0c8PDfqL+dr3zHytAMW+71BJqIPQW910vV7SX3IcGylnoUcS3doVkJZiprXytXMP89AKcgv5Dj7mS2ZdvNGE+Atro74bzZ5yot5BrE0ZE5SjoUBPLaLMMu9HopLIV+qx01Rc3F0wmkocSPo51N0nv6wvO5Cst0FiOGHDK6r1pFlgDEJLmBkOyC4e8oMVbKTJzsSQVbJ8tJ37xuhI+T5P0ZlmiqKDqYRp8uh50w+txLEixYhEUunFgCTt1DAmiS9pLNYhLyl1ggwuQjzZe+AV6timbRxNJy18/AEcPomJw7z/pxYIiNLHRKOC13Wp8kGWx9cOgfMQ5KmBuLS8psGiLTBkfWPLOfNYqjbeqAR+OGZQoS6hUjbBYU7QuFa4FOYBHkNB2UqNsdsMb9qB/qs7QGTSb8Lok5YjW1c81BUpmIyKvuqnKma0MZskrpTYGQD2eJDABFCZwLFm+LgDyUTeSiV5xguYztLrHOk8LHKo9M8dIZgoBjeFVJxyjbcXKsVS3aQkMXKCrRlKLqhZTws/ZJwVfW9DbktZ9dT+tRZQvI7tjJofojcLX61AGJDnqUf5+2Gv1tEnmUI953gIzc8NlcFabPOsDsZEODt7MdOCTPT3w29umyhKbCsslpb64LoS/AB2QRPRCgkJS7snRA",
|
||||
"ciphertext": "d7UVOK17WEVky/8hK0h3HsTQrFMEbKbfqMcl2KtyTWcI9S5gGFWK9Git5BzVRxRggvxQ0c8PDfqL+dr3zHytAA7TEpHlx8Ks23hCqXmVW710VjqK2K9xnWCyJvkHfE8x0w6AYvffDj+tRVP8C8M7t4849rD2itn0uma+YMkvjG/nANUTxG1dBf3oUOZ673vflCPoaz7s7x9ZNhYDVSVH5JTdMgNwwN42R5dqqxnGTu516tJzJh/9BWvyD9oIPWJ8X0rt1sbzEJ3PZeBXcSy8GTlZ1SgSFjeiXlwYxOZCaX2sxprk4N1oI1db6g+wCDBhbCGGucJIlTDJna/h9/C5J4drGd/fkisG3SidUmJXXCyInhs/BhwjGAtTGeQS8j7R8UnJxhMulYBHSckzj0Kas71LElPp8W8M4Jq81APA03n5UfYB+U6jbxjDgf8OJnxGQyrteq9F2+SEvS/TwHe1pE3t6EM2mDYRoYDTpU5pTNYSJkGIQMfWJKRxxuWUGs29o1twewJ6dhHgm+SlCII0M7ESoVdV54vxZCvHZnPcR0NXDzal7ils7zBKJmamHfPQBuaqNPU3KmSo+5R8ngFPaWU5LbWqYp/WxSBfNCoLZ7Jf8Io5uitjXTATR2qy2r6l/RJmk3RlfP51kliQqI2TWqRF96oaB96IGgUGSFCX/2pv0psOBGc1SjfmMB3d7gYis+2iBYVbG3xmnpeXbqvlD0Lw9TiTIPkjhJkTW1+lXyhy1xVH9ZmcFamcL7bX15Jx",
|
||||
"ephemeral": "oO0VX84OUIzm2i/12zAhTWOZT5IFRH5mXaKZ8fXkCgU",
|
||||
"mac": "lEfHlqfJQwU"
|
||||
}
|
||||
@@ -449,6 +446,34 @@ export const BOB_ENCRYPTED_EVENT: Partial<IEvent> = {
|
||||
"origin_server_ts": 1507753886000
|
||||
};
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const BOB_BACKUP_DECRYPTION_KEY_BASE64 = "DwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const BOB_BACKUP_DECRYPTION_KEY_BASE58 = "EsT5 Sd5m mEXs NQYE ibRe 3q9E 4aXW rHih 5f9J 6rU6 AfwY mASR";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
|
||||
export const BOB_SIGNED_BACKUP_DATA: KeyBackupInfo = {
|
||||
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
"version": "1",
|
||||
"auth_data": {
|
||||
"public_key": "ZRuVWcWlDuvOwZRygccUCD4Avtnt130800I+WQNwwRY",
|
||||
"signatures": {
|
||||
"@bob:xyz": {
|
||||
"ed25519:bob_device": "lDIMj3VC0WazE2FamGHpmbiqKf9Z4pO4qapZ5TL5BnD3c+dvb+2waOEd6pgay/pmrQ6MW4Eu2KDEpe1fnHc3BA"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-room backup data, (supposedly) suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}`.
|
||||
* Contains the key from BOB_MEGOLM_SESSION_DATA.
|
||||
*/
|
||||
export const BOB_PER_ROOM_CURVE25519_KEY_BACKUP_DATA: KeyBackupRoomSessions = {
|
||||
[BOB_MEGOLM_SESSION_DATA.session_id]: BOB_CURVE25519_KEY_BACKUP_DATA
|
||||
};
|
||||
|
||||
/** A second set of signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
|
||||
export const BOB_ALT_SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
|
||||
"master_keys": {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import {
|
||||
ClientEvent,
|
||||
EventType,
|
||||
HistoryVisibility,
|
||||
type IJoinedRoom,
|
||||
type IPusher,
|
||||
type ISyncResponse,
|
||||
@@ -57,14 +58,19 @@ export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a sync response which contains a single room (by default TEST_ROOM_ID), with the members given
|
||||
* @param roomMembers
|
||||
* @param roomId
|
||||
* Return a sync response which contains a single room (by default `TEST_ROOM_ID`), with the members given
|
||||
* and history visibility set to `shared`.
|
||||
*
|
||||
* @returns the sync response
|
||||
* @param roomMembers - An array of user IDs representing the members of the room.
|
||||
* @param roomHistoryVisibility - The history visibility setting for the room. Defaults to `shared`.
|
||||
* @param roomId - The ID of the room. Defaults to `TEST_ROOM_ID`.
|
||||
* @param encryptStateEvents - A boolean indicating whether state events should be encrypted. Defaults to `false`.
|
||||
*
|
||||
* @returns The sync response object containing the room data.
|
||||
*/
|
||||
export function getSyncResponse(
|
||||
roomMembers: string[],
|
||||
roomHistoryVisibility: HistoryVisibility = HistoryVisibility.Shared,
|
||||
roomId = TEST_ROOM_ID,
|
||||
encryptStateEvents = false,
|
||||
): ISyncResponse {
|
||||
@@ -82,7 +88,15 @@ export function getSyncResponse(
|
||||
state_key: "",
|
||||
content: {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"io.element.msc3414.encrypt_state_events": encryptStateEvents,
|
||||
"io.element.msc4362.encrypt_state_events": encryptStateEvents,
|
||||
},
|
||||
}),
|
||||
mkEventCustom({
|
||||
sender: roomMembers[0],
|
||||
type: "m.room.history_visibility",
|
||||
state_key: "",
|
||||
content: {
|
||||
history_visibility: roomHistoryVisibility,
|
||||
},
|
||||
}),
|
||||
],
|
||||
@@ -136,7 +150,7 @@ export function mock<T>(constr: { new (...args: any[]): T }, name: string): T {
|
||||
// eslint-disable-line guard-for-in
|
||||
try {
|
||||
if (constr.prototype[key] instanceof Function) {
|
||||
result[key] = jest.fn();
|
||||
result[key] = vi.fn();
|
||||
}
|
||||
} catch {
|
||||
// Direct access to some non-function fields of DOM prototypes may
|
||||
@@ -592,7 +606,7 @@ export async function advanceTimersUntil<T>(promise: Promise<T>): Promise<T> {
|
||||
});
|
||||
|
||||
while (!resolved) {
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
}
|
||||
|
||||
return await promise;
|
||||
@@ -641,7 +655,7 @@ export function waitFor<T>(
|
||||
checkCallback();
|
||||
|
||||
while (!finished) {
|
||||
jest.advanceTimersByTime(interval);
|
||||
vi.advanceTimersByTime(interval);
|
||||
|
||||
// Could have timed-out
|
||||
if (finished) break;
|
||||
|
||||
+51
-92
@@ -17,21 +17,17 @@ limitations under the License.
|
||||
import {
|
||||
type ClientEvent,
|
||||
type ClientEventHandlerMap,
|
||||
type EmptyObject,
|
||||
EventType,
|
||||
type GroupCall,
|
||||
GroupCallIntent,
|
||||
GroupCallType,
|
||||
type IContent,
|
||||
type ISendEventResponse,
|
||||
type MatrixClient,
|
||||
type MatrixEvent,
|
||||
type Room,
|
||||
RoomMember,
|
||||
type RoomState,
|
||||
RoomStateEvent,
|
||||
type RoomStateEventHandlerMap,
|
||||
type SendToDeviceContentMap,
|
||||
} from "../../src";
|
||||
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
|
||||
import { ReEmitter } from "../../src/ReEmitter";
|
||||
@@ -269,19 +265,20 @@ export class MockRTCRtpTransceiver {
|
||||
this.peerConn.needsNegotiation = true;
|
||||
}
|
||||
|
||||
public setCodecPreferences = jest.fn<void, RTCRtpCodec[]>();
|
||||
public setCodecPreferences = vi.fn<RTCRtpTransceiver["setCodecPreferences"]>();
|
||||
}
|
||||
|
||||
export class MockMediaStreamTrack {
|
||||
export class MockMediaStreamTrack extends EventTarget {
|
||||
constructor(
|
||||
public readonly id: string,
|
||||
public readonly kind: "audio" | "video",
|
||||
public enabled = true,
|
||||
) {}
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public stop = jest.fn<void, []>();
|
||||
public stop = vi.fn<() => void>();
|
||||
|
||||
public listeners: [string, (...args: any[]) => any][] = [];
|
||||
public isStopped = false;
|
||||
public settings?: MediaTrackSettings;
|
||||
|
||||
@@ -289,45 +286,21 @@ export class MockMediaStreamTrack {
|
||||
return this.settings!;
|
||||
}
|
||||
|
||||
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
|
||||
// implementation
|
||||
public dispatchEvent(eventType: string) {
|
||||
this.listeners.forEach(([t, c]) => {
|
||||
if (t !== eventType) return;
|
||||
c();
|
||||
});
|
||||
}
|
||||
public addEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
this.listeners.push([eventType, callback]);
|
||||
}
|
||||
public removeEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
this.listeners.filter(([t, c]) => {
|
||||
return t !== eventType || c !== callback;
|
||||
});
|
||||
}
|
||||
|
||||
public typed(): MediaStreamTrack {
|
||||
return this as unknown as MediaStreamTrack;
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
|
||||
// implementation
|
||||
export class MockMediaStream {
|
||||
export class MockMediaStream extends EventTarget {
|
||||
constructor(
|
||||
public id: string,
|
||||
private tracks: MockMediaStreamTrack[] = [],
|
||||
) {}
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public listeners: [string, (...args: any[]) => any][] = [];
|
||||
public isStopped = false;
|
||||
|
||||
public dispatchEvent(eventType: string) {
|
||||
this.listeners.forEach(([t, c]) => {
|
||||
if (t !== eventType) return;
|
||||
c();
|
||||
});
|
||||
}
|
||||
public getTracks() {
|
||||
return this.tracks;
|
||||
}
|
||||
@@ -337,17 +310,9 @@ export class MockMediaStream {
|
||||
public getVideoTracks() {
|
||||
return this.tracks.filter((track) => track.kind === "video");
|
||||
}
|
||||
public addEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
this.listeners.push([eventType, callback]);
|
||||
}
|
||||
public removeEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
this.listeners.filter(([t, c]) => {
|
||||
return t !== eventType || c !== callback;
|
||||
});
|
||||
}
|
||||
public addTrack(track: MockMediaStreamTrack) {
|
||||
this.tracks.push(track);
|
||||
this.dispatchEvent("addtrack");
|
||||
this.dispatchEvent(new Event("addtrack"));
|
||||
}
|
||||
public removeTrack(track: MockMediaStreamTrack) {
|
||||
this.tracks.splice(this.tracks.indexOf(track), 1);
|
||||
@@ -391,7 +356,7 @@ export class MockMediaHandler {
|
||||
public stopUserMediaStream(stream: MockMediaStream) {
|
||||
stream.isStopped = true;
|
||||
}
|
||||
public getScreensharingStream = jest.fn((opts?: IScreensharingOpts) => {
|
||||
public getScreensharingStream = vi.fn((opts?: IScreensharingOpts) => {
|
||||
const tracks = [new MockMediaStreamTrack("screenshare_video_track", "video")];
|
||||
if (opts?.audio) tracks.push(new MockMediaStreamTrack("screenshare_audio_track", "audio"));
|
||||
|
||||
@@ -416,19 +381,19 @@ export class MockMediaHandler {
|
||||
}
|
||||
|
||||
export class MockMediaDevices {
|
||||
public enumerateDevices = jest
|
||||
.fn<Promise<MediaDeviceInfo[]>, []>()
|
||||
public enumerateDevices = vi
|
||||
.fn<MediaDevices["enumerateDevices"]>()
|
||||
.mockResolvedValue([
|
||||
new MockMediaDeviceInfo("audioinput").typed(),
|
||||
new MockMediaDeviceInfo("videoinput").typed(),
|
||||
]);
|
||||
|
||||
public getUserMedia = jest
|
||||
.fn<Promise<MediaStream>, [MediaStreamConstraints]>()
|
||||
public getUserMedia = vi
|
||||
.fn<MediaDevices["getUserMedia"]>()
|
||||
.mockReturnValue(Promise.resolve(new MockMediaStream("local_stream").typed()));
|
||||
|
||||
public getDisplayMedia = jest
|
||||
.fn<Promise<MediaStream>, [MediaStreamConstraints]>()
|
||||
public getDisplayMedia = vi
|
||||
.fn<MediaDevices["getDisplayMedia"]>()
|
||||
.mockReturnValue(Promise.resolve(new MockMediaStream("local_display_stream").typed()));
|
||||
|
||||
public typed(): MediaDevices {
|
||||
@@ -462,14 +427,8 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
|
||||
calls: new Map<string, MatrixCall>(),
|
||||
};
|
||||
|
||||
public sendStateEvent = jest.fn<
|
||||
Promise<ISendEventResponse>,
|
||||
[roomId: string, eventType: EventType, content: any, statekey: string]
|
||||
>();
|
||||
public sendToDevice = jest.fn<
|
||||
Promise<EmptyObject>,
|
||||
[eventType: string, contentMap: SendToDeviceContentMap, txnId?: string]
|
||||
>();
|
||||
public sendStateEvent = vi.fn<MatrixClient["sendStateEvent"]>();
|
||||
public sendToDevice = vi.fn<MatrixClient["sendToDevice"]>();
|
||||
|
||||
public isInitialSyncComplete(): boolean {
|
||||
return false;
|
||||
@@ -499,11 +458,11 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
|
||||
public getUseE2eForGroupCall = () => false;
|
||||
public checkTurnServers = () => null;
|
||||
|
||||
public getSyncState = jest.fn<SyncState | null, []>().mockReturnValue(SyncState.Syncing);
|
||||
public getSyncState = vi.fn<MatrixClient["getSyncState"]>().mockReturnValue(SyncState.Syncing);
|
||||
|
||||
public getRooms = jest.fn<Room[], []>().mockReturnValue([]);
|
||||
public getRoom = jest.fn();
|
||||
public getFoci = jest.fn();
|
||||
public getRooms = vi.fn<MatrixClient["getRooms"]>().mockReturnValue([]);
|
||||
public getRoom = vi.fn();
|
||||
public getFoci = vi.fn();
|
||||
|
||||
public supportsThreads(): boolean {
|
||||
return true;
|
||||
@@ -534,20 +493,20 @@ export class MockMatrixCall extends TypedEventEmitter<CallEvent, CallEventHandle
|
||||
public opponentMember = { userId: this.opponentUserId };
|
||||
public callId = "1";
|
||||
public localUsermediaFeed = {
|
||||
setAudioVideoMuted: jest.fn<void, [boolean, boolean]>(),
|
||||
isAudioMuted: jest.fn().mockReturnValue(false),
|
||||
isVideoMuted: jest.fn().mockReturnValue(false),
|
||||
setAudioVideoMuted: vi.fn<CallFeed["setAudioVideoMuted"]>(),
|
||||
isAudioMuted: vi.fn().mockReturnValue(false),
|
||||
isVideoMuted: vi.fn().mockReturnValue(false),
|
||||
stream: new MockMediaStream("stream"),
|
||||
} as unknown as CallFeed;
|
||||
public remoteUsermediaFeed?: CallFeed;
|
||||
public remoteScreensharingFeed?: CallFeed;
|
||||
|
||||
public reject = jest.fn<void, []>();
|
||||
public answerWithCallFeeds = jest.fn<void, [CallFeed[]]>();
|
||||
public hangup = jest.fn<void, []>();
|
||||
public initStats = jest.fn<void, []>();
|
||||
public reject = vi.fn<() => void>();
|
||||
public answerWithCallFeeds = vi.fn<MatrixCall["answerWithCallFeeds"]>();
|
||||
public hangup = vi.fn<() => void>();
|
||||
public initStats = vi.fn<() => void>();
|
||||
|
||||
public sendMetadataUpdate = jest.fn<void, []>();
|
||||
public sendMetadataUpdate = vi.fn<() => void>();
|
||||
|
||||
public getOpponentMember(): Partial<RoomMember> {
|
||||
return this.opponentMember;
|
||||
@@ -586,11 +545,11 @@ export class MockCallFeed {
|
||||
}
|
||||
|
||||
export function installWebRTCMocks() {
|
||||
globalThis.navigator = {
|
||||
vi.stubGlobal("navigator", {
|
||||
mediaDevices: new MockMediaDevices().typed(),
|
||||
} as unknown as Navigator;
|
||||
});
|
||||
|
||||
globalThis.window = {
|
||||
vi.stubGlobal("window", {
|
||||
// @ts-ignore Mock
|
||||
RTCPeerConnection: MockRTCPeerConnection,
|
||||
// @ts-ignore Mock
|
||||
@@ -598,16 +557,16 @@ export function installWebRTCMocks() {
|
||||
// @ts-ignore Mock
|
||||
RTCIceCandidate: {},
|
||||
getUserMedia: () => new MockMediaStream("local_stream"),
|
||||
};
|
||||
// @ts-ignore Mock
|
||||
globalThis.document = {};
|
||||
});
|
||||
|
||||
vi.stubGlobal("document", {});
|
||||
|
||||
// @ts-ignore Mock
|
||||
globalThis.AudioContext = MockAudioContext;
|
||||
|
||||
// @ts-ignore Mock
|
||||
globalThis.RTCRtpReceiver = {
|
||||
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
|
||||
getCapabilities: vi.fn().mockReturnValue({
|
||||
codecs: [],
|
||||
headerExtensions: [],
|
||||
}),
|
||||
@@ -615,7 +574,7 @@ export function installWebRTCMocks() {
|
||||
|
||||
// @ts-ignore Mock
|
||||
globalThis.RTCRtpSender = {
|
||||
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
|
||||
getCapabilities: vi.fn().mockReturnValue({
|
||||
codecs: [],
|
||||
headerExtensions: [],
|
||||
}),
|
||||
@@ -632,22 +591,22 @@ export function makeMockGroupCallStateEvent(
|
||||
redacted?: boolean,
|
||||
): MatrixEvent {
|
||||
return {
|
||||
getType: jest.fn().mockReturnValue(EventType.GroupCallPrefix),
|
||||
getRoomId: jest.fn().mockReturnValue(roomId),
|
||||
getTs: jest.fn().mockReturnValue(0),
|
||||
getContent: jest.fn().mockReturnValue(content),
|
||||
getStateKey: jest.fn().mockReturnValue(groupCallId),
|
||||
isRedacted: jest.fn().mockReturnValue(redacted ?? false),
|
||||
getType: vi.fn().mockReturnValue(EventType.GroupCallPrefix),
|
||||
getRoomId: vi.fn().mockReturnValue(roomId),
|
||||
getTs: vi.fn().mockReturnValue(0),
|
||||
getContent: vi.fn().mockReturnValue(content),
|
||||
getStateKey: vi.fn().mockReturnValue(groupCallId),
|
||||
isRedacted: vi.fn().mockReturnValue(redacted ?? false),
|
||||
} as unknown as MatrixEvent;
|
||||
}
|
||||
|
||||
export function makeMockGroupCallMemberStateEvent(roomId: string, groupCallId: string): MatrixEvent {
|
||||
return {
|
||||
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
||||
getRoomId: jest.fn().mockReturnValue(roomId),
|
||||
getTs: jest.fn().mockReturnValue(0),
|
||||
getContent: jest.fn().mockReturnValue({}),
|
||||
getStateKey: jest.fn().mockReturnValue(groupCallId),
|
||||
getType: vi.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
||||
getRoomId: vi.fn().mockReturnValue(roomId),
|
||||
getTs: vi.fn().mockReturnValue(0),
|
||||
getContent: vi.fn().mockReturnValue({}),
|
||||
getStateKey: vi.fn().mockReturnValue(groupCallId),
|
||||
} as unknown as MatrixEvent;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ describe("NamespacedValue", () => {
|
||||
});
|
||||
|
||||
it("should have a falsey unstable if needed", () => {
|
||||
const ns = new NamespacedValue("stable");
|
||||
const ns = new NamespacedValue("stable", null);
|
||||
expect(ns.name).toBe(ns.stable);
|
||||
expect(ns.altName).toBeFalsy();
|
||||
expect(ns.names).toEqual([ns.stable]);
|
||||
@@ -61,7 +61,7 @@ describe("UnstableValue", () => {
|
||||
it("should return unstable if there is no stable", () => {
|
||||
const ns = new UnstableValue(null!, "unstable");
|
||||
expect(ns.name).toBe(ns.unstable);
|
||||
expect(ns.altName).toBeFalsy();
|
||||
expect(<any>ns.altName).toBeFalsy();
|
||||
expect(ns.names).toEqual([ns.unstable]);
|
||||
});
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ describe("ReEmitter", function () {
|
||||
const src = new EventSource();
|
||||
const tgt = new EventTarget();
|
||||
|
||||
const handler = jest.fn();
|
||||
const handler = vi.fn();
|
||||
tgt.on(EVENTNAME, handler);
|
||||
|
||||
const reEmitter = new ReEmitter(tgt);
|
||||
@@ -61,7 +61,7 @@ describe("ReEmitter", function () {
|
||||
// without the workaround in ReEmitter, this would throw
|
||||
src.doAnError();
|
||||
|
||||
const handler = jest.fn();
|
||||
const handler = vi.fn();
|
||||
tgt.on("error", handler);
|
||||
|
||||
src.doAnError();
|
||||
|
||||
@@ -35,16 +35,16 @@ describe("onResumedSync", () => {
|
||||
};
|
||||
|
||||
store = new StubStore();
|
||||
store.getOldestToDeviceBatch = jest.fn().mockImplementation(() => {
|
||||
store.getOldestToDeviceBatch = vi.fn().mockImplementation(() => {
|
||||
return batch;
|
||||
});
|
||||
store.removeToDeviceBatch = jest.fn().mockImplementation(() => {
|
||||
store.removeToDeviceBatch = vi.fn().mockImplementation(() => {
|
||||
batch = null;
|
||||
});
|
||||
|
||||
mockClient = getMockClientWithEventEmitter({});
|
||||
mockClient.store = store;
|
||||
mockClient.sendToDevice = jest.fn().mockImplementation(async () => {
|
||||
mockClient.sendToDevice = vi.fn().mockImplementation(async () => {
|
||||
if (shouldFailSendToDevice) {
|
||||
await Promise.reject(new ConnectionError("")).finally(() => {
|
||||
setTimeout(onSendToDeviceFailure, 0);
|
||||
|
||||
@@ -35,6 +35,7 @@ describe("AutoDiscovery", function () {
|
||||
AutoDiscovery.setFetchFn(realAutoDiscoveryFetch);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should throw an error when no domain is specified", function () {
|
||||
getHttpBackend();
|
||||
return Promise.all([
|
||||
@@ -190,7 +191,7 @@ describe("AutoDiscovery", function () {
|
||||
};
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then(expect(expected).toEqual),
|
||||
AutoDiscovery.findClientConfig("example.org").then((config) => expect(config).toEqual(expected)),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -29,10 +29,10 @@ describe("Beacon content helpers", () => {
|
||||
describe("makeBeaconInfoContent()", () => {
|
||||
const mockDateNow = 123456789;
|
||||
beforeEach(() => {
|
||||
jest.spyOn(globalThis.Date, "now").mockReturnValue(mockDateNow);
|
||||
vi.spyOn(globalThis.Date, "now").mockReturnValue(mockDateNow);
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.spyOn(globalThis.Date, "now").mockRestore();
|
||||
vi.spyOn(globalThis.Date, "now").mockRestore();
|
||||
});
|
||||
it("create fully defined event content", () => {
|
||||
expect(makeBeaconInfoContent(1234, true, "nice beacon_info", LocationAssetType.Pin)).toEqual({
|
||||
|
||||
@@ -14,8 +14,11 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
|
||||
import "fake-indexeddb/auto";
|
||||
import "jest-localstorage-mock";
|
||||
import { IndexedDBCryptoStore, LocalStorageCryptoStore, MemoryCryptoStore } from "../../../../src";
|
||||
import { type CryptoStore, MigrationState, SESSION_BATCH_SIZE } from "../../../../src/crypto/store/base";
|
||||
|
||||
|
||||
@@ -29,12 +29,7 @@ describe("sha256", () => {
|
||||
});
|
||||
|
||||
it("throws if webcrypto is not available", async () => {
|
||||
const oldCrypto = globalThis.crypto;
|
||||
try {
|
||||
globalThis.crypto = {} as any;
|
||||
await expect(sha256("test")).rejects.toThrow();
|
||||
} finally {
|
||||
globalThis.crypto = oldCrypto;
|
||||
}
|
||||
vi.stubGlobal("crypto", {});
|
||||
await expect(sha256("test")).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
+220
-44
@@ -1,7 +1,3 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
@@ -22,7 +18,7 @@ limitations under the License.
|
||||
// project, which doesn't know about our TypeEventEmitter implementation at all
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EventEmitter } from "events";
|
||||
import { type MockedObject } from "jest-mock";
|
||||
import { type MockedObject } from "vitest";
|
||||
import {
|
||||
type WidgetApi,
|
||||
WidgetApiToWidgetAction,
|
||||
@@ -36,7 +32,14 @@ import {
|
||||
type IRoomEvent,
|
||||
} from "matrix-widget-api";
|
||||
|
||||
import { createRoomWidgetClient, MatrixError, MsgType, UpdateDelayedEventAction } from "../../src/matrix";
|
||||
import {
|
||||
createRoomWidgetClient,
|
||||
EventType,
|
||||
type IEvent,
|
||||
MatrixError,
|
||||
MsgType,
|
||||
UpdateDelayedEventAction,
|
||||
} from "../../src/matrix";
|
||||
import { MatrixClient, ClientEvent, type ITurnServer as IClientTurnServer } from "../../src/client";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { type ICapabilities, type RoomWidgetClient } from "../../src/embedded";
|
||||
@@ -46,6 +49,7 @@ import { sleep } from "../../src/utils";
|
||||
import { SlidingSync } from "../../src/sliding-sync";
|
||||
import { logger } from "../../src/logger";
|
||||
import { flushPromises } from "../test-utils/flushPromises";
|
||||
import { RoomStickyEventsEvent, type RoomStickyEventsMap } from "../../src/models/room-sticky-events";
|
||||
|
||||
const testOIDCToken = {
|
||||
access_token: "12345678",
|
||||
@@ -53,27 +57,28 @@ const testOIDCToken = {
|
||||
matrix_server_name: "homeserver.oabc",
|
||||
token_type: "Bearer",
|
||||
};
|
||||
|
||||
class MockWidgetApi extends EventEmitter {
|
||||
public start = jest.fn().mockResolvedValue(undefined);
|
||||
public getClientVersions = jest.fn();
|
||||
public requestCapability = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilities = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityForRoomTimeline = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToSendEvent = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToReceiveEvent = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToSendMessage = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToReceiveMessage = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToSendState = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToReceiveState = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToSendToDevice = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToReceiveToDevice = jest.fn().mockResolvedValue(undefined);
|
||||
public sendRoomEvent = jest.fn(
|
||||
public start = vi.fn().mockResolvedValue(undefined);
|
||||
public getClientVersions = vi.fn();
|
||||
public requestCapability = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilities = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityForRoomTimeline = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToSendEvent = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToReceiveEvent = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToSendMessage = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToReceiveMessage = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToSendState = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToReceiveState = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToSendToDevice = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToReceiveToDevice = vi.fn().mockResolvedValue(undefined);
|
||||
public sendRoomEvent = vi.fn(
|
||||
async (eventType: string, content: unknown, roomId?: string, delay?: number, parentDelayId?: string) =>
|
||||
delay === undefined && parentDelayId === undefined
|
||||
? { event_id: `$${Math.random()}` }
|
||||
: { delay_id: `id-${Math.random()}` },
|
||||
);
|
||||
public sendStateEvent = jest.fn(
|
||||
public sendStateEvent = vi.fn(
|
||||
async (
|
||||
eventType: string,
|
||||
stateKey: string,
|
||||
@@ -86,24 +91,24 @@ class MockWidgetApi extends EventEmitter {
|
||||
? { event_id: `$${Math.random()}` }
|
||||
: { delay_id: `id-${Math.random()}` },
|
||||
);
|
||||
public cancelScheduledDelayedEvent = jest.fn().mockResolvedValue(undefined);
|
||||
public restartScheduledDelayedEvent = jest.fn().mockResolvedValue(undefined);
|
||||
public sendScheduledDelayedEvent = jest.fn().mockResolvedValue(undefined);
|
||||
public sendToDevice = jest.fn().mockResolvedValue(undefined);
|
||||
public requestOpenIDConnectToken = jest.fn(async () => {
|
||||
public cancelScheduledDelayedEvent = vi.fn().mockResolvedValue(undefined);
|
||||
public restartScheduledDelayedEvent = vi.fn().mockResolvedValue(undefined);
|
||||
public sendScheduledDelayedEvent = vi.fn().mockResolvedValue(undefined);
|
||||
public sendToDevice = vi.fn().mockResolvedValue(undefined);
|
||||
public requestOpenIDConnectToken = vi.fn(async () => {
|
||||
return testOIDCToken;
|
||||
return new Promise<IOpenIDCredentials>(() => {
|
||||
return testOIDCToken;
|
||||
});
|
||||
});
|
||||
public readStateEvents = jest.fn(async () => []);
|
||||
public getTurnServers = jest.fn(async () => []);
|
||||
public sendContentLoaded = jest.fn().mockResolvedValue(undefined);
|
||||
public readStateEvents = vi.fn(async () => []);
|
||||
public getTurnServers = vi.fn(async () => []);
|
||||
public sendContentLoaded = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
public transport = {
|
||||
reply: jest.fn(),
|
||||
send: jest.fn(),
|
||||
sendComplete: jest.fn(),
|
||||
reply: vi.fn(),
|
||||
send: vi.fn(),
|
||||
sendComplete: vi.fn(),
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -171,6 +176,9 @@ describe("RoomWidgetClient", () => {
|
||||
"org.matrix.rageshake_request",
|
||||
{ request_id: 123 },
|
||||
"!1:example.org",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -230,7 +238,7 @@ describe("RoomWidgetClient", () => {
|
||||
);
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToReceiveEvent).toHaveBeenCalledWith("org.matrix.rageshake_request");
|
||||
const injectSpy = jest.spyOn((client as any).syncApi, "injectRoomEvents");
|
||||
const injectSpy = vi.spyOn((client as any).syncApi, "injectRoomEvents");
|
||||
const widgetSendEmitter = new EventEmitter();
|
||||
const widgetSendPromise = new Promise<void>((resolve) =>
|
||||
widgetSendEmitter.once("send", () => resolve()),
|
||||
@@ -357,10 +365,10 @@ describe("RoomWidgetClient", () => {
|
||||
|
||||
it("handles widget errors with generic error data", async () => {
|
||||
const error = new Error("failed to send");
|
||||
widgetApi.transport.send.mockRejectedValue(error);
|
||||
vi.mocked(widgetApi.transport.send).mockRejectedValue(error);
|
||||
|
||||
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
|
||||
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send);
|
||||
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send as any);
|
||||
|
||||
await expect(
|
||||
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
|
||||
@@ -383,22 +391,22 @@ describe("RoomWidgetClient", () => {
|
||||
response: errorData,
|
||||
},
|
||||
});
|
||||
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl);
|
||||
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl, undefined, expect.any(Headers));
|
||||
|
||||
widgetApi.transport.send.mockRejectedValue(widgetError);
|
||||
vi.mocked(widgetApi.transport.send).mockRejectedValue(widgetError);
|
||||
|
||||
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
|
||||
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send);
|
||||
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send as any);
|
||||
|
||||
await expect(
|
||||
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
|
||||
).rejects.toThrow(matrixError);
|
||||
).rejects.toStrictEqual(matrixError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("delayed events", () => {
|
||||
describe("when supported", () => {
|
||||
const doesServerSupportUnstableFeatureMock = jest.fn((feature) =>
|
||||
const doesServerSupportUnstableFeatureMock = vi.fn((feature) =>
|
||||
Promise.resolve(feature === "org.matrix.msc4140"),
|
||||
);
|
||||
|
||||
@@ -426,6 +434,7 @@ describe("RoomWidgetClient", () => {
|
||||
"!1:example.org",
|
||||
2000,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -446,6 +455,7 @@ describe("RoomWidgetClient", () => {
|
||||
"!1:example.org",
|
||||
undefined,
|
||||
parentDelayId,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -580,6 +590,16 @@ describe("RoomWidgetClient", () => {
|
||||
});
|
||||
|
||||
describe("when unsupported", () => {
|
||||
const doesServerSupportUnstableFeatureMock = vi.fn().mockResolvedValue(false);
|
||||
|
||||
beforeAll(() => {
|
||||
MatrixClient.prototype.doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
doesServerSupportUnstableFeatureMock.mockReset();
|
||||
});
|
||||
|
||||
it("fails to send delayed message events", async () => {
|
||||
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
|
||||
await expect(
|
||||
@@ -770,7 +790,7 @@ describe("RoomWidgetClient", () => {
|
||||
|
||||
const emittedEvent = new Promise<MatrixEvent>((resolve) => client.once(ClientEvent.Event, resolve));
|
||||
const emittedSync = new Promise<SyncState>((resolve) => client.once(ClientEvent.Sync, resolve));
|
||||
const logSpy = jest.spyOn(logger, "error");
|
||||
const logSpy = vi.spyOn(logger, "error");
|
||||
widgetApi.emit(
|
||||
`action:${WidgetApiToWidgetAction.SendEvent}`,
|
||||
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
|
||||
@@ -849,6 +869,162 @@ describe("RoomWidgetClient", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("sticky events", () => {
|
||||
describe("when supported", () => {
|
||||
const doesServerSupportUnstableFeatureMock = vi.fn((feature) =>
|
||||
Promise.resolve(feature === "org.matrix.msc4354"),
|
||||
);
|
||||
|
||||
beforeAll(() => {
|
||||
MatrixClient.prototype.doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
doesServerSupportUnstableFeatureMock.mockReset();
|
||||
});
|
||||
|
||||
it("requests capabilities when set", async () => {
|
||||
await makeClient({ sendSticky: true, receiveSticky: true });
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4407SendStickyEvent);
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4407ReceiveStickyEvent);
|
||||
});
|
||||
|
||||
it("does not request capabilities when unset", async () => {
|
||||
await makeClient({});
|
||||
expect(widgetApi.requestCapability).not.toHaveBeenCalledWith(MatrixCapabilities.MSC4407SendStickyEvent);
|
||||
expect(widgetApi.requestCapability).not.toHaveBeenCalledWith(
|
||||
MatrixCapabilities.MSC4407ReceiveStickyEvent,
|
||||
);
|
||||
});
|
||||
|
||||
it("sends", async () => {
|
||||
await makeClient({ sendEvent: [EventType.RTCMembership], sendSticky: true });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToSendEvent).toHaveBeenCalledWith(EventType.RTCMembership);
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4407SendStickyEvent);
|
||||
await client._unstable_sendStickyEvent("!1:example.org", 2000, null, EventType.RTCMembership, {
|
||||
msc4354_sticky_key: "test",
|
||||
});
|
||||
expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith(
|
||||
EventType.RTCMembership,
|
||||
{ msc4354_sticky_key: "test" },
|
||||
"!1:example.org",
|
||||
undefined,
|
||||
undefined,
|
||||
2000,
|
||||
);
|
||||
});
|
||||
|
||||
it("receives (adds, updates, then removes when redacted)", async () => {
|
||||
await makeClient({ receiveEvent: [EventType.RTCMembership, EventType.RoomRedaction] });
|
||||
const room = client.getRoom("!1:example.org")!;
|
||||
|
||||
function expectStickyEvents(events: IEvent[]) {
|
||||
expect([...room._unstable_getStickyEvents()].map((e) => e.getEffectiveEvent())).toEqual(events);
|
||||
}
|
||||
|
||||
async function sendAndExpectStickyUpdate(
|
||||
eventToSend: IEvent,
|
||||
added: IEvent[],
|
||||
updated: { current: IEvent; previous: IEvent }[],
|
||||
removed: IEvent[],
|
||||
) {
|
||||
const emittedStickyUpdate = new Promise<
|
||||
Parameters<RoomStickyEventsMap[RoomStickyEventsEvent.Update]>
|
||||
>((resolve) => room.once(RoomStickyEventsEvent.Update, (...args) => resolve(args)));
|
||||
|
||||
widgetApi.emit(
|
||||
`action:${WidgetApiToWidgetAction.SendEvent}`,
|
||||
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, {
|
||||
detail: { data: eventToSend },
|
||||
}),
|
||||
);
|
||||
|
||||
const [addedReceived, updatedReceived, removedReceived] = await emittedStickyUpdate;
|
||||
expect(addedReceived.map((e) => e.getEffectiveEvent())).toEqual(added);
|
||||
expect(
|
||||
updatedReceived.map(({ current, previous }) => ({
|
||||
current: current.getEffectiveEvent(),
|
||||
previous: previous.getEffectiveEvent(),
|
||||
})),
|
||||
).toEqual(updated);
|
||||
expect(removedReceived.map((e) => e.getEffectiveEvent())).toEqual(removed);
|
||||
}
|
||||
|
||||
// First, add a new sticky event to the map. The client should emit.
|
||||
const event1 = new MatrixEvent({
|
||||
type: EventType.RTCMembership,
|
||||
event_id: "$pduhfiidph",
|
||||
room_id: "!1:example.org",
|
||||
sender: "@alice:example.org",
|
||||
msc4354_sticky: { duration_ms: 1200000 },
|
||||
content: { msc4354_sticky_key: "test" },
|
||||
}).getEffectiveEvent();
|
||||
await sendAndExpectStickyUpdate(event1, [event1], [], []);
|
||||
// It should remain cached in the sticky map
|
||||
expectStickyEvents([event1]);
|
||||
|
||||
// Next, update the same key in the sticky map
|
||||
const event2 = new MatrixEvent({
|
||||
type: EventType.RTCMembership,
|
||||
event_id: "$zshgyutptfh",
|
||||
room_id: "!1:example.org",
|
||||
sender: "@alice:example.org",
|
||||
msc4354_sticky: { duration_ms: 1200000 },
|
||||
content: { msc4354_sticky_key: "test" },
|
||||
}).getEffectiveEvent();
|
||||
await sendAndExpectStickyUpdate(event2, [], [{ current: event2, previous: event1 }], []);
|
||||
expectStickyEvents([event2]);
|
||||
|
||||
// Next, redact the second event. Because it has the first as a predecessor, the map should revert to
|
||||
// the first event.
|
||||
const redaction1 = new MatrixEvent({
|
||||
type: EventType.RoomRedaction,
|
||||
event_id: "$cimoexnvz",
|
||||
room_id: "!1:example.org",
|
||||
sender: "@alice:example.org",
|
||||
redacts: event2.event_id,
|
||||
content: { redacts: event2.event_id },
|
||||
}).getEffectiveEvent();
|
||||
await sendAndExpectStickyUpdate(redaction1, [], [{ current: event1, previous: event2 }], []);
|
||||
expectStickyEvents([event1]);
|
||||
|
||||
// Finally, redact the first event. Now everything should be gone from the map.
|
||||
const redaction2 = new MatrixEvent({
|
||||
type: EventType.RoomRedaction,
|
||||
event_id: "$drgzmenlh",
|
||||
room_id: "!1:example.org",
|
||||
sender: "@alice:example.org",
|
||||
redacts: event1.event_id,
|
||||
content: { redacts: event1.event_id },
|
||||
}).getEffectiveEvent();
|
||||
await sendAndExpectStickyUpdate(redaction2, [], [], [event1]);
|
||||
expectStickyEvents([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when unsupported", () => {
|
||||
const doesServerSupportUnstableFeatureMock = vi.fn().mockResolvedValue(false);
|
||||
|
||||
beforeAll(() => {
|
||||
MatrixClient.prototype.doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
doesServerSupportUnstableFeatureMock.mockReset();
|
||||
});
|
||||
|
||||
it("fails to send", async () => {
|
||||
await makeClient({ sendEvent: [EventType.RTCMembership], sendSticky: true });
|
||||
await expect(
|
||||
client._unstable_sendStickyEvent("!1:example.org", 2000, null, EventType.RTCMembership, {
|
||||
msc4354_sticky_key: "test",
|
||||
}),
|
||||
).rejects.toThrow("Server does not support");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("to-device messages", () => {
|
||||
const unencryptedContentMap = new Map([
|
||||
["@alice:example.org", new Map([["*", { hello: "alice!" }]])],
|
||||
@@ -970,7 +1146,7 @@ describe("RoomWidgetClient", () => {
|
||||
|
||||
it("handles widget errors with generic error data", async () => {
|
||||
const error = new Error("failed to get token");
|
||||
widgetApi.transport.sendComplete.mockRejectedValue(error);
|
||||
vi.mocked(widgetApi.transport.sendComplete).mockRejectedValue(error);
|
||||
|
||||
await makeClient({});
|
||||
widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any);
|
||||
@@ -994,9 +1170,9 @@ describe("RoomWidgetClient", () => {
|
||||
response: errorData,
|
||||
},
|
||||
});
|
||||
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl);
|
||||
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl, undefined, expect.any(Headers));
|
||||
|
||||
widgetApi.transport.sendComplete.mockRejectedValue(widgetError);
|
||||
vi.mocked(widgetApi.transport.sendComplete).mockRejectedValue(widgetError);
|
||||
|
||||
await makeClient({});
|
||||
widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any);
|
||||
|
||||
@@ -37,7 +37,7 @@ describe("eventMapperFor", function () {
|
||||
setUserCreator(_) {},
|
||||
} as IStore,
|
||||
scheduler: {
|
||||
setProcessFunction: jest.fn(),
|
||||
setProcessFunction: vi.fn(),
|
||||
} as unknown as MatrixScheduler,
|
||||
userId: userId,
|
||||
});
|
||||
@@ -133,7 +133,7 @@ describe("eventMapperFor", function () {
|
||||
event_id: eventId,
|
||||
};
|
||||
|
||||
const decryptEventIfNeededSpy = jest.spyOn(client, "decryptEventIfNeeded");
|
||||
const decryptEventIfNeededSpy = vi.spyOn(client, "decryptEventIfNeeded");
|
||||
decryptEventIfNeededSpy.mockResolvedValue(); // stub it out
|
||||
|
||||
const mapper = eventMapperFor(client, {
|
||||
@@ -161,7 +161,7 @@ describe("eventMapperFor", function () {
|
||||
event_id: eventId,
|
||||
};
|
||||
|
||||
const evListener = jest.fn();
|
||||
const evListener = vi.fn();
|
||||
client.on(MatrixEventEvent.Replaced, evListener);
|
||||
|
||||
const noReEmitMapper = eventMapperFor(client, {
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { type MockInstance } from "vitest";
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import {
|
||||
@@ -196,7 +196,7 @@ describe("EventTimelineSet", () => {
|
||||
|
||||
it("should aggregate relations which belong to unknown timeline without adding them to any timeline", () => {
|
||||
// If threads are disabled all events go into the main timeline
|
||||
mocked(client.supportsThreads).mockReturnValue(true);
|
||||
vi.mocked(client.supportsThreads).mockReturnValue(true);
|
||||
const reactionEvent = utils.mkReaction(messageEvent, client, client.getSafeUserId(), roomId);
|
||||
|
||||
const liveTimeline = eventTimelineSet.getLiveTimeline();
|
||||
@@ -228,7 +228,7 @@ describe("EventTimelineSet", () => {
|
||||
let thread: Thread;
|
||||
|
||||
beforeEach(() => {
|
||||
(client.supportsThreads as jest.Mock).mockReturnValue(true);
|
||||
vi.mocked(client.supportsThreads).mockReturnValue(true);
|
||||
thread = new Thread("!thread_id:server", messageEvent, { room, client });
|
||||
});
|
||||
|
||||
@@ -306,20 +306,20 @@ describe("EventTimelineSet", () => {
|
||||
});
|
||||
|
||||
describe("with events to be decrypted", () => {
|
||||
let messageEventShouldAttemptDecryptionSpy: jest.SpyInstance;
|
||||
let messageEventIsDecryptionFailureSpy: jest.SpyInstance;
|
||||
let messageEventShouldAttemptDecryptionSpy: MockInstance;
|
||||
let messageEventIsDecryptionFailureSpy: MockInstance;
|
||||
|
||||
let replyEventShouldAttemptDecryptionSpy: jest.SpyInstance;
|
||||
let replyEventIsDecryptionFailureSpy: jest.SpyInstance;
|
||||
let replyEventShouldAttemptDecryptionSpy: MockInstance;
|
||||
let replyEventIsDecryptionFailureSpy: MockInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
messageEventShouldAttemptDecryptionSpy = jest.spyOn(messageEvent, "shouldAttemptDecryption");
|
||||
messageEventShouldAttemptDecryptionSpy = vi.spyOn(messageEvent, "shouldAttemptDecryption");
|
||||
messageEventShouldAttemptDecryptionSpy.mockReturnValue(true);
|
||||
messageEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, "isDecryptionFailure");
|
||||
messageEventIsDecryptionFailureSpy = vi.spyOn(messageEvent, "isDecryptionFailure");
|
||||
|
||||
replyEventShouldAttemptDecryptionSpy = jest.spyOn(replyEvent, "shouldAttemptDecryption");
|
||||
replyEventShouldAttemptDecryptionSpy = vi.spyOn(replyEvent, "shouldAttemptDecryption");
|
||||
replyEventShouldAttemptDecryptionSpy.mockReturnValue(true);
|
||||
replyEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, "isDecryptionFailure");
|
||||
replyEventIsDecryptionFailureSpy = vi.spyOn(messageEvent, "isDecryptionFailure");
|
||||
|
||||
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, false, eventTimeline, "foo");
|
||||
});
|
||||
@@ -384,7 +384,7 @@ describe("EventTimelineSet", () => {
|
||||
let thread: Thread;
|
||||
|
||||
beforeEach(() => {
|
||||
(client.supportsThreads as jest.Mock).mockReturnValue(true);
|
||||
vi.mocked(client.supportsThreads).mockReturnValue(true);
|
||||
thread = new Thread("!thread_id:server", messageEvent, { room, client });
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { Direction, EventTimeline } from "../../src/models/event-timeline";
|
||||
import { RoomState } from "../../src/models/room-state";
|
||||
@@ -20,21 +18,21 @@ describe("EventTimeline", function () {
|
||||
const getTimeline = (): EventTimeline => {
|
||||
const room = new Room(roomId, mockClient, userA);
|
||||
const timelineSet = new EventTimelineSet(room);
|
||||
jest.spyOn(room, "getUnfilteredTimelineSet").mockReturnValue(timelineSet);
|
||||
vi.spyOn(room, "getUnfilteredTimelineSet").mockReturnValue(timelineSet);
|
||||
|
||||
const timeline = new EventTimeline(timelineSet);
|
||||
// We manually stub the methods we'll be mocking out later instead of mocking the whole module
|
||||
// otherwise the default member property values (e.g. paginationToken) will be incorrect
|
||||
timeline.getState(Direction.Backward)!.setStateEvents = jest.fn();
|
||||
timeline.getState(Direction.Backward)!.getSentinelMember = jest.fn();
|
||||
timeline.getState(Direction.Forward)!.setStateEvents = jest.fn();
|
||||
timeline.getState(Direction.Forward)!.getSentinelMember = jest.fn();
|
||||
timeline.getState(Direction.Backward)!.setStateEvents = vi.fn();
|
||||
timeline.getState(Direction.Backward)!.getSentinelMember = vi.fn();
|
||||
timeline.getState(Direction.Forward)!.setStateEvents = vi.fn();
|
||||
timeline.getState(Direction.Forward)!.getSentinelMember = vi.fn();
|
||||
return timeline;
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
// reset any RoomState mocks
|
||||
jest.resetAllMocks();
|
||||
vi.resetAllMocks();
|
||||
|
||||
timeline = getTimeline();
|
||||
});
|
||||
@@ -67,12 +65,12 @@ describe("EventTimeline", function () {
|
||||
timeline.initialiseState(events);
|
||||
// @ts-ignore private prop
|
||||
const timelineStartState = timeline.startState!;
|
||||
expect(mocked(timelineStartState).setStateEvents).toHaveBeenCalledWith(events, {
|
||||
expect(vi.mocked(timelineStartState).setStateEvents).toHaveBeenCalledWith(events, {
|
||||
timelineWasEmpty: undefined,
|
||||
});
|
||||
// @ts-ignore private prop
|
||||
const timelineEndState = timeline.endState!;
|
||||
expect(mocked(timelineEndState).setStateEvents).toHaveBeenCalledWith(events, {
|
||||
expect(vi.mocked(timelineEndState).setStateEvents).toHaveBeenCalledWith(events, {
|
||||
timelineWasEmpty: undefined,
|
||||
});
|
||||
});
|
||||
@@ -210,13 +208,13 @@ describe("EventTimeline", function () {
|
||||
sentinel.name = "Old Alice";
|
||||
sentinel.membership = KnownMembership.Join;
|
||||
|
||||
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
vi.mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
vi.mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
}
|
||||
@@ -253,13 +251,13 @@ describe("EventTimeline", function () {
|
||||
sentinel.name = "Old Alice";
|
||||
sentinel.membership = KnownMembership.Join;
|
||||
|
||||
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
vi.mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
vi.mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`MatrixHttpApi should return expected object from \`getContentUri\` 1`] = `
|
||||
exports[`MatrixHttpApi > should return expected object from \`getContentUri\` 1`] = `
|
||||
{
|
||||
"base": "http://baseUrl",
|
||||
"params": {
|
||||
|
||||
@@ -57,7 +57,7 @@ describe("MatrixError", () => {
|
||||
|
||||
it("should retrieve Date Retry-After header from rate-limit error", () => {
|
||||
headers.set("Retry-After", `${new Date(160000).toUTCString()}`);
|
||||
jest.spyOn(globalThis.Date, "now").mockImplementationOnce(() => 100000);
|
||||
vi.spyOn(globalThis.Date, "now").mockImplementationOnce(() => 100000);
|
||||
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED", retry_after_ms: 150000 });
|
||||
expect(err.isRateLimitError()).toBe(true);
|
||||
// prefer Retry-After header over retry_after_ms
|
||||
|
||||
@@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Mocked, MockedFunction } from "jest-mock";
|
||||
import { type Mocked, type MockedFunction } from "vitest";
|
||||
|
||||
import { FetchHttpApi } from "../../../src/http-api/fetch";
|
||||
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
|
||||
import {
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
type IHttpOpts,
|
||||
MatrixError,
|
||||
Method,
|
||||
TokenRefreshError,
|
||||
} from "../../../src";
|
||||
import { emitPromise } from "../../test-utils/test-utils";
|
||||
import { type QueryDict, sleep } from "../../../src/utils";
|
||||
@@ -38,7 +40,7 @@ describe("FetchHttpApi", () => {
|
||||
const tokenInactiveError = new MatrixError({ errcode: "M_UNKNOWN_TOKEN", error: "Token is not active" }, 401);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useRealTimers();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("should support aborting multiple times", () => {
|
||||
@@ -47,29 +49,29 @@ describe("FetchHttpApi", () => {
|
||||
|
||||
api.request(Method.Get, "/foo");
|
||||
api.request(Method.Get, "/baz");
|
||||
expect(fetchFn.mock.calls[0][0].href.endsWith("/foo")).toBeTruthy();
|
||||
expect(fetchFn.mock.calls[0][1].signal.aborted).toBeFalsy();
|
||||
expect(fetchFn.mock.calls[1][0].href.endsWith("/baz")).toBeTruthy();
|
||||
expect(fetchFn.mock.calls[1][1].signal.aborted).toBeFalsy();
|
||||
expect((fetchFn.mock.calls[0][0] as URL).href.endsWith("/foo")).toBeTruthy();
|
||||
expect(fetchFn.mock.calls[0][1]?.signal?.aborted).toBeFalsy();
|
||||
expect((fetchFn.mock.calls[1][0] as URL).href.endsWith("/baz")).toBeTruthy();
|
||||
expect(fetchFn.mock.calls[1][1]?.signal?.aborted).toBeFalsy();
|
||||
|
||||
api.abort();
|
||||
expect(fetchFn.mock.calls[0][1].signal.aborted).toBeTruthy();
|
||||
expect(fetchFn.mock.calls[1][1].signal.aborted).toBeTruthy();
|
||||
expect(fetchFn.mock.calls[0][1]?.signal?.aborted).toBeTruthy();
|
||||
expect(fetchFn.mock.calls[1][1]?.signal?.aborted).toBeTruthy();
|
||||
|
||||
api.request(Method.Get, "/bar");
|
||||
expect(fetchFn.mock.calls[2][0].href.endsWith("/bar")).toBeTruthy();
|
||||
expect(fetchFn.mock.calls[2][1].signal.aborted).toBeFalsy();
|
||||
expect((fetchFn.mock.calls[2][0] as URL).href.endsWith("/bar")).toBeTruthy();
|
||||
expect(fetchFn.mock.calls[2][1]?.signal?.aborted).toBeFalsy();
|
||||
|
||||
api.abort();
|
||||
expect(fetchFn.mock.calls[2][1].signal.aborted).toBeTruthy();
|
||||
expect(fetchFn.mock.calls[2][1]?.signal?.aborted).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should fall back to global fetch if fetchFn not provided", () => {
|
||||
globalThis.fetch = jest.fn();
|
||||
expect(globalThis.fetch).not.toHaveBeenCalled();
|
||||
const spy = (globalThis.fetch = vi.fn());
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
api.fetch("test");
|
||||
expect(globalThis.fetch).toHaveBeenCalled();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should update identity server base url", () => {
|
||||
@@ -97,8 +99,8 @@ describe("FetchHttpApi", () => {
|
||||
onlyData: true,
|
||||
});
|
||||
api.idServerRequest(Method.Get, "/test", { foo: "bar", via: ["a", "b"] }, IdentityPrefix.V2);
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("foo")).toBe("bar");
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.getAll("via")).toEqual(["a", "b"]);
|
||||
expect((fetchFn.mock.calls[0][0] as URL).searchParams.get("foo")).toBe("bar");
|
||||
expect((fetchFn.mock.calls[0][0] as URL).searchParams.getAll("via")).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
it("should send params as body for non-GET requests", () => {
|
||||
@@ -112,8 +114,8 @@ describe("FetchHttpApi", () => {
|
||||
});
|
||||
const params = { foo: "bar", via: ["a", "b"] };
|
||||
api.idServerRequest(Method.Post, "/test", params, IdentityPrefix.V2);
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("foo")).not.toBe("bar");
|
||||
expect(JSON.parse(fetchFn.mock.calls[0][1].body)).toStrictEqual(params);
|
||||
expect((fetchFn.mock.calls[0][0] as URL).searchParams.get("foo")).not.toBe("bar");
|
||||
expect(JSON.parse(fetchFn.mock.calls[0][1]!.body as string)).toStrictEqual(params);
|
||||
});
|
||||
|
||||
it("should add Authorization header if token provided", () => {
|
||||
@@ -126,7 +128,7 @@ describe("FetchHttpApi", () => {
|
||||
onlyData: true,
|
||||
});
|
||||
api.idServerRequest(Method.Post, "/test", {}, IdentityPrefix.V2, "token");
|
||||
expect(fetchFn.mock.calls[0][1].headers.Authorization).toBe("Bearer token");
|
||||
expect((fetchFn.mock.calls[0][1]!.headers as Record<string, any>).Authorization).toBe("Bearer token");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -142,7 +144,7 @@ describe("FetchHttpApi", () => {
|
||||
|
||||
it("should set an Accept header, and parse the response as JSON, by default", async () => {
|
||||
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 });
|
||||
await expect(api.requestOtherUrl(Method.Get, "http://url")).resolves.toBe(result);
|
||||
expect(fetchFn.mock.calls[0][1].headers.Accept).toBe("application/json");
|
||||
@@ -150,7 +152,7 @@ describe("FetchHttpApi", () => {
|
||||
|
||||
it("should not set an Accept header, and should return text if json=false", async () => {
|
||||
const text = "418 I'm a teapot";
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true, text: jest.fn().mockResolvedValue(text) });
|
||||
const fetchFn = vi.fn().mockResolvedValue({ ok: true, text: vi.fn().mockResolvedValue(text) });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
|
||||
await expect(
|
||||
api.requestOtherUrl(Method.Get, "http://url", undefined, {
|
||||
@@ -162,7 +164,7 @@ describe("FetchHttpApi", () => {
|
||||
|
||||
it("should not set an Accept header, and should return a blob, if rawResponseBody is true", async () => {
|
||||
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 });
|
||||
await expect(
|
||||
api.requestOtherUrl(Method.Get, "http://url", undefined, {
|
||||
@@ -176,7 +178,7 @@ describe("FetchHttpApi", () => {
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn: jest.fn(),
|
||||
fetchFn: vi.fn(),
|
||||
onlyData: true,
|
||||
});
|
||||
await expect(
|
||||
@@ -195,7 +197,7 @@ describe("FetchHttpApi", () => {
|
||||
onlyData: true,
|
||||
});
|
||||
await api.authedRequest(Method.Get, "/path");
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBe("token");
|
||||
expect((fetchFn.mock.calls[0][0] as URL).searchParams.get("access_token")).toBe("token");
|
||||
});
|
||||
|
||||
it("should send token via headers by default", async () => {
|
||||
@@ -208,7 +210,7 @@ describe("FetchHttpApi", () => {
|
||||
onlyData: true,
|
||||
});
|
||||
await api.authedRequest(Method.Get, "/path");
|
||||
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer token");
|
||||
expect((fetchFn.mock.calls[0][1]!.headers as Record<string, any>)["Authorization"]).toBe("Bearer token");
|
||||
});
|
||||
|
||||
it("should not send a token if not calling `authedRequest`", () => {
|
||||
@@ -221,8 +223,8 @@ describe("FetchHttpApi", () => {
|
||||
onlyData: true,
|
||||
});
|
||||
api.request(Method.Get, "/path");
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBeFalsy();
|
||||
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBeFalsy();
|
||||
expect((fetchFn.mock.calls[0][0] as URL).searchParams.get("access_token")).toBeFalsy();
|
||||
expect((fetchFn.mock.calls[0][1]!.headers as Record<string, any>)["Authorization"]).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should ensure no token is leaked out via query params if sending via headers", async () => {
|
||||
@@ -236,8 +238,8 @@ describe("FetchHttpApi", () => {
|
||||
onlyData: true,
|
||||
});
|
||||
await api.authedRequest(Method.Get, "/path", { access_token: "123" });
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBeFalsy();
|
||||
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer token");
|
||||
expect((fetchFn.mock.calls[0][0] as URL).searchParams.get("access_token")).toBeFalsy();
|
||||
expect((fetchFn.mock.calls[0][1]!.headers as Record<string, any>)["Authorization"]).toBe("Bearer token");
|
||||
});
|
||||
|
||||
it("should not override manually specified access token via query params", async () => {
|
||||
@@ -251,7 +253,7 @@ describe("FetchHttpApi", () => {
|
||||
onlyData: true,
|
||||
});
|
||||
await api.authedRequest(Method.Get, "/path", { access_token: "RealToken" });
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBe("RealToken");
|
||||
expect((fetchFn.mock.calls[0][0] as URL).searchParams.get("access_token")).toBe("RealToken");
|
||||
});
|
||||
|
||||
it("should not override manually specified access token via header", async () => {
|
||||
@@ -267,7 +269,7 @@ describe("FetchHttpApi", () => {
|
||||
await api.authedRequest(Method.Get, "/path", undefined, undefined, {
|
||||
headers: { Authorization: "Bearer RealToken" },
|
||||
});
|
||||
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer RealToken");
|
||||
expect((fetchFn.mock.calls[0][1]!.headers as Record<string, any>)["Authorization"]).toBe("Bearer RealToken");
|
||||
});
|
||||
|
||||
it("should not override Accept header", async () => {
|
||||
@@ -276,18 +278,18 @@ describe("FetchHttpApi", () => {
|
||||
await api.authedRequest(Method.Get, "/path", undefined, undefined, {
|
||||
headers: { Accept: "text/html" },
|
||||
});
|
||||
expect(fetchFn.mock.calls[0][1].headers["Accept"]).toBe("text/html");
|
||||
expect((fetchFn.mock.calls[0][1]!.headers as Record<string, any>)["Accept"]).toBe("text/html");
|
||||
});
|
||||
|
||||
it("should emit NoConsent when given errcode=M_CONTENT_NOT_GIVEN", async () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({
|
||||
const fetchFn = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
headers: {
|
||||
get(name: string): string | null {
|
||||
return name === "Content-Type" ? "application/json" : null;
|
||||
},
|
||||
},
|
||||
text: jest.fn().mockResolvedValue(
|
||||
text: vi.fn().mockResolvedValue(
|
||||
JSON.stringify({
|
||||
errcode: "M_CONSENT_NOT_GIVEN",
|
||||
error: "Ye shall ask for consent",
|
||||
@@ -309,7 +311,7 @@ describe("FetchHttpApi", () => {
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn, onlyData: true });
|
||||
await api.authedRequest(Method.Post, "/account/password");
|
||||
expect(fetchFn.mock.calls[0][1].headers.Authorization).toBeUndefined();
|
||||
expect((fetchFn.mock.calls[0][1]!.headers as Record<string, any>).Authorization).toBeUndefined();
|
||||
});
|
||||
|
||||
describe("with refresh token", () => {
|
||||
@@ -322,7 +324,13 @@ describe("FetchHttpApi", () => {
|
||||
error: "Token is not active",
|
||||
soft_logout: false,
|
||||
};
|
||||
const unknownTokenErr = new MatrixError(unknownTokenErrBody, 401);
|
||||
const unknownTokenErr = new MatrixError(
|
||||
unknownTokenErrBody,
|
||||
401,
|
||||
undefined,
|
||||
undefined,
|
||||
expect.anything(),
|
||||
);
|
||||
const unknownTokenResponse = {
|
||||
ok: false,
|
||||
status: 401,
|
||||
@@ -331,19 +339,19 @@ describe("FetchHttpApi", () => {
|
||||
return name === "Content-Type" ? "application/json" : null;
|
||||
},
|
||||
},
|
||||
text: jest.fn().mockResolvedValue(JSON.stringify(unknownTokenErrBody)),
|
||||
text: vi.fn().mockResolvedValue(JSON.stringify(unknownTokenErrBody)),
|
||||
};
|
||||
const okayResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: jest.fn().mockResolvedValue({ x: 1 }),
|
||||
json: vi.fn().mockResolvedValue({ x: 1 }),
|
||||
};
|
||||
|
||||
describe("without a tokenRefreshFunction", () => {
|
||||
it("should emit logout and throw", async () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue(unknownTokenResponse);
|
||||
const fetchFn = vi.fn().mockResolvedValue(unknownTokenResponse);
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
jest.spyOn(emitter, "emit");
|
||||
vi.spyOn(emitter, "emit");
|
||||
const api = new FetchHttpApi(emitter, {
|
||||
baseUrl,
|
||||
prefix,
|
||||
@@ -362,10 +370,10 @@ describe("FetchHttpApi", () => {
|
||||
describe("with a tokenRefreshFunction", () => {
|
||||
it("should emit logout and throw when token refresh fails", async () => {
|
||||
const error = new MatrixError();
|
||||
const tokenRefreshFunction = jest.fn().mockRejectedValue(error);
|
||||
const fetchFn = jest.fn().mockResolvedValue(unknownTokenResponse);
|
||||
const tokenRefreshFunction = vi.fn().mockRejectedValue(error);
|
||||
const fetchFn = vi.fn().mockResolvedValue(unknownTokenResponse);
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
jest.spyOn(emitter, "emit");
|
||||
vi.spyOn(emitter, "emit");
|
||||
const api = new FetchHttpApi(emitter, {
|
||||
baseUrl,
|
||||
prefix,
|
||||
@@ -375,7 +383,7 @@ describe("FetchHttpApi", () => {
|
||||
refreshToken,
|
||||
onlyData: true,
|
||||
});
|
||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toEqual(
|
||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
|
||||
unknownTokenErr,
|
||||
);
|
||||
expect(tokenRefreshFunction).toHaveBeenCalledWith(refreshToken);
|
||||
@@ -384,10 +392,10 @@ describe("FetchHttpApi", () => {
|
||||
|
||||
it("should not emit logout but still throw when token refresh fails due to transitive fault", async () => {
|
||||
const error = new ConnectionError("transitive fault");
|
||||
const tokenRefreshFunction = jest.fn().mockRejectedValue(error);
|
||||
const fetchFn = jest.fn().mockResolvedValue(unknownTokenResponse);
|
||||
const tokenRefreshFunction = vi.fn().mockRejectedValue(error);
|
||||
const fetchFn = vi.fn().mockResolvedValue(unknownTokenResponse);
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
jest.spyOn(emitter, "emit");
|
||||
vi.spyOn(emitter, "emit");
|
||||
const api = new FetchHttpApi(emitter, {
|
||||
baseUrl,
|
||||
prefix,
|
||||
@@ -397,8 +405,8 @@ describe("FetchHttpApi", () => {
|
||||
refreshToken,
|
||||
onlyData: true,
|
||||
});
|
||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toEqual(
|
||||
unknownTokenErr,
|
||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
|
||||
new TokenRefreshError(unknownTokenErr),
|
||||
);
|
||||
expect(tokenRefreshFunction).toHaveBeenCalledWith(refreshToken);
|
||||
expect(emitter.emit).not.toHaveBeenCalledWith(HttpApiEvent.SessionLoggedOut, unknownTokenErr);
|
||||
@@ -407,16 +415,16 @@ describe("FetchHttpApi", () => {
|
||||
it("should refresh token and retry request", async () => {
|
||||
const newAccessToken = "new-access-token";
|
||||
const newRefreshToken = "new-refresh-token";
|
||||
const tokenRefreshFunction = jest.fn().mockResolvedValue({
|
||||
const tokenRefreshFunction = vi.fn().mockResolvedValue({
|
||||
accessToken: newAccessToken,
|
||||
refreshToken: newRefreshToken,
|
||||
});
|
||||
const fetchFn = jest
|
||||
const fetchFn = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(unknownTokenResponse)
|
||||
.mockResolvedValueOnce(okayResponse);
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
jest.spyOn(emitter, "emit");
|
||||
vi.spyOn(emitter, "emit");
|
||||
const api = new FetchHttpApi(emitter, {
|
||||
baseUrl,
|
||||
prefix,
|
||||
@@ -448,7 +456,7 @@ describe("FetchHttpApi", () => {
|
||||
// count because it's only to get a token with an expiry)
|
||||
const newAccessToken = "new-access-token";
|
||||
const newRefreshToken = "new-refresh-token";
|
||||
const tokenRefreshFunction = jest.fn().mockReturnValue({
|
||||
const tokenRefreshFunction = vi.fn().mockReturnValue({
|
||||
accessToken: newAccessToken,
|
||||
refreshToken: newRefreshToken,
|
||||
// This needs to be sufficiently high that it's over the threshold for
|
||||
@@ -457,10 +465,10 @@ describe("FetchHttpApi", () => {
|
||||
});
|
||||
|
||||
// fetch doesn't like our new or old tokens
|
||||
const fetchFn = jest.fn().mockResolvedValue(unknownTokenResponse);
|
||||
const fetchFn = vi.fn().mockResolvedValue(unknownTokenResponse);
|
||||
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
jest.spyOn(emitter, "emit");
|
||||
vi.spyOn(emitter, "emit");
|
||||
const api = new FetchHttpApi(emitter, {
|
||||
baseUrl,
|
||||
prefix,
|
||||
@@ -470,7 +478,7 @@ describe("FetchHttpApi", () => {
|
||||
refreshToken,
|
||||
onlyData: true,
|
||||
});
|
||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
|
||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrowError(
|
||||
unknownTokenErr,
|
||||
);
|
||||
|
||||
@@ -492,7 +500,7 @@ describe("FetchHttpApi", () => {
|
||||
|
||||
// first refresh is to get a token with an expiry at all, because we
|
||||
// can't specify an expiry on the token we inject
|
||||
const tokenRefreshFunction = jest.fn().mockResolvedValueOnce({
|
||||
const tokenRefreshFunction = vi.fn().mockResolvedValueOnce({
|
||||
accessToken: newAccessToken,
|
||||
refreshToken: newRefreshToken,
|
||||
expiry: new Date(Date.now() + 1000),
|
||||
@@ -513,10 +521,10 @@ describe("FetchHttpApi", () => {
|
||||
expiry: new Date(Date.now() + 5 * 60 * 1000),
|
||||
});
|
||||
|
||||
const fetchFn = jest.fn().mockResolvedValue(unknownTokenResponse);
|
||||
const fetchFn = vi.fn().mockResolvedValue(unknownTokenResponse);
|
||||
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
jest.spyOn(emitter, "emit");
|
||||
vi.spyOn(emitter, "emit");
|
||||
const api = new FetchHttpApi(emitter, {
|
||||
baseUrl,
|
||||
prefix,
|
||||
@@ -526,7 +534,7 @@ describe("FetchHttpApi", () => {
|
||||
refreshToken,
|
||||
onlyData: true,
|
||||
});
|
||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
|
||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrowError(
|
||||
unknownTokenErr,
|
||||
);
|
||||
|
||||
@@ -543,7 +551,7 @@ describe("FetchHttpApi", () => {
|
||||
const localBaseUrl = "http://baseurl";
|
||||
const baseUrlWithTrailingSlash = "http://baseurl/";
|
||||
const makeApi = (thisBaseUrl = baseUrl): FetchHttpApi<any> => {
|
||||
const fetchFn = jest.fn();
|
||||
const fetchFn = vi.fn();
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
return new FetchHttpApi(emitter, { baseUrl: thisBaseUrl, prefix, fetchFn, onlyData: true });
|
||||
};
|
||||
@@ -596,7 +604,7 @@ describe("FetchHttpApi", () => {
|
||||
|
||||
describe("extraParams handling", () => {
|
||||
const makeApiWithExtraParams = (extraParams: QueryDict): FetchHttpApi<any> => {
|
||||
const fetchFn = jest.fn();
|
||||
const fetchFn = vi.fn();
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
return new FetchHttpApi(emitter, {
|
||||
baseUrl: localBaseUrl,
|
||||
@@ -655,7 +663,7 @@ describe("FetchHttpApi", () => {
|
||||
});
|
||||
|
||||
it("should work when extraParams is undefined", () => {
|
||||
const fetchFn = jest.fn();
|
||||
const fetchFn = vi.fn();
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
const api = new FetchHttpApi(emitter, { baseUrl: localBaseUrl, prefix, fetchFn, onlyData: true });
|
||||
|
||||
@@ -679,11 +687,11 @@ describe("FetchHttpApi", () => {
|
||||
});
|
||||
|
||||
it("should not log query parameters", async () => {
|
||||
jest.useFakeTimers();
|
||||
vi.useFakeTimers();
|
||||
const responseResolvers = Promise.withResolvers<Response>();
|
||||
const fetchFn = jest.fn().mockReturnValue(responseResolvers.promise);
|
||||
const fetchFn = vi.fn().mockReturnValue(responseResolvers.promise);
|
||||
const mockLogger = {
|
||||
debug: jest.fn(),
|
||||
debug: vi.fn(),
|
||||
} as unknown as Mocked<Logger>;
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
@@ -693,7 +701,7 @@ describe("FetchHttpApi", () => {
|
||||
onlyData: true,
|
||||
});
|
||||
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, json: () => Promise.resolve("RESPONSE") } as Response);
|
||||
await prom;
|
||||
expect(mockLogger.debug).not.toHaveBeenCalledWith("fragment");
|
||||
@@ -714,7 +722,7 @@ describe("FetchHttpApi", () => {
|
||||
|
||||
it("should not make multiple concurrent refresh token requests", async () => {
|
||||
const deferredTokenRefresh = Promise.withResolvers<{ accessToken: string; refreshToken: string }>();
|
||||
const fetchFn = jest.fn().mockResolvedValue({
|
||||
const fetchFn = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: tokenInactiveError.httpStatus,
|
||||
async text() {
|
||||
@@ -724,10 +732,10 @@ describe("FetchHttpApi", () => {
|
||||
return tokenInactiveError.data;
|
||||
},
|
||||
headers: {
|
||||
get: jest.fn().mockReturnValue("application/json"),
|
||||
get: vi.fn().mockReturnValue("application/json"),
|
||||
},
|
||||
});
|
||||
const tokenRefreshFunction = jest.fn().mockReturnValue(deferredTokenRefresh.promise);
|
||||
const tokenRefreshFunction = vi.fn().mockReturnValue(deferredTokenRefresh.promise);
|
||||
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
@@ -755,7 +763,7 @@ describe("FetchHttpApi", () => {
|
||||
return {};
|
||||
},
|
||||
headers: {
|
||||
get: jest.fn().mockReturnValue("application/json"),
|
||||
get: vi.fn().mockReturnValue("application/json"),
|
||||
},
|
||||
});
|
||||
deferredTokenRefresh.resolve({ accessToken: "NEW_ACCESS_TOKEN", refreshToken: "NEW_REFRESH_TOKEN" });
|
||||
@@ -770,7 +778,7 @@ describe("FetchHttpApi", () => {
|
||||
|
||||
it("should use newly refreshed token if request starts mid-refresh", async () => {
|
||||
const deferredTokenRefresh = Promise.withResolvers<{ accessToken: string; refreshToken: string }>();
|
||||
const fetchFn = jest.fn().mockResolvedValue({
|
||||
const fetchFn = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: tokenInactiveError.httpStatus,
|
||||
async text() {
|
||||
@@ -780,10 +788,10 @@ describe("FetchHttpApi", () => {
|
||||
return tokenInactiveError.data;
|
||||
},
|
||||
headers: {
|
||||
get: jest.fn().mockReturnValue("application/json"),
|
||||
get: vi.fn().mockReturnValue("application/json"),
|
||||
},
|
||||
});
|
||||
const tokenRefreshFunction = jest.fn().mockReturnValue(deferredTokenRefresh.promise);
|
||||
const tokenRefreshFunction = vi.fn().mockReturnValue(deferredTokenRefresh.promise);
|
||||
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
@@ -813,7 +821,7 @@ describe("FetchHttpApi", () => {
|
||||
return {};
|
||||
},
|
||||
headers: {
|
||||
get: jest.fn().mockReturnValue("application/json"),
|
||||
get: vi.fn().mockReturnValue("application/json"),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -832,6 +840,6 @@ describe("FetchHttpApi", () => {
|
||||
});
|
||||
});
|
||||
|
||||
function makeMockFetchFn(): MockedFunction<any> {
|
||||
return jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({}) });
|
||||
function makeMockFetchFn(): MockedFunction<Window["fetch"]> {
|
||||
return vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue({}) });
|
||||
}
|
||||
|
||||
@@ -14,54 +14,59 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { ClientPrefix, MatrixHttpApi, Method, type UploadResponse } from "../../../src";
|
||||
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
|
||||
|
||||
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
|
||||
|
||||
jest.useFakeTimers();
|
||||
vi.useFakeTimers();
|
||||
|
||||
describe("MatrixHttpApi", () => {
|
||||
const baseUrl = "http://baseUrl";
|
||||
const prefix = ClientPrefix.V3;
|
||||
|
||||
let xhr: Writeable<XMLHttpRequest>;
|
||||
let upload: Promise<UploadResponse>;
|
||||
|
||||
const DONE = 0;
|
||||
|
||||
function getRequest(): Writeable<XMLHttpRequest> | undefined {
|
||||
return vi.mocked(globalThis.XMLHttpRequest)?.mock.instances.at(-1);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
xhr = {
|
||||
upload: {} as XMLHttpRequestUpload,
|
||||
open: jest.fn(),
|
||||
send: jest.fn(),
|
||||
abort: jest.fn(),
|
||||
setRequestHeader: jest.fn(),
|
||||
onreadystatechange: undefined,
|
||||
getResponseHeader: jest.fn(),
|
||||
getAllResponseHeaders: jest.fn(),
|
||||
} as unknown as XMLHttpRequest;
|
||||
// We stub out XHR here as it is not available in JSDOM
|
||||
// We stub out XHR here as it is not available in the test environment
|
||||
// @ts-ignore
|
||||
globalThis.XMLHttpRequest = jest.fn().mockReturnValue(xhr);
|
||||
globalThis.XMLHttpRequest = vi.fn().mockImplementation(function (this: XMLHttpRequest) {
|
||||
// @ts-ignore
|
||||
this.upload = {} as XMLHttpRequestUpload;
|
||||
this.open = vi.fn();
|
||||
this.send = vi.fn();
|
||||
this.abort = vi.fn();
|
||||
this.setRequestHeader = vi.fn();
|
||||
// @ts-ignore
|
||||
this.onreadystatechange = undefined;
|
||||
this.getResponseHeader = vi.fn();
|
||||
this.getAllResponseHeaders = vi.fn();
|
||||
});
|
||||
// @ts-ignore
|
||||
globalThis.XMLHttpRequest.DONE = DONE;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
upload?.catch(() => {});
|
||||
// Abort any remaining requests
|
||||
xhr.readyState = DONE;
|
||||
xhr.status = 0;
|
||||
// @ts-ignore
|
||||
xhr.onreadystatechange?.(new Event("test"));
|
||||
const xhr = getRequest();
|
||||
if (xhr) {
|
||||
// Abort any remaining requests
|
||||
xhr.readyState = DONE;
|
||||
xhr.status = 0;
|
||||
// @ts-ignore
|
||||
xhr.onreadystatechange?.(new Event("test"));
|
||||
}
|
||||
});
|
||||
|
||||
it("should fall back to `fetch` where xhr is unavailable", async () => {
|
||||
globalThis.XMLHttpRequest = undefined!;
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({}) });
|
||||
const fetchFn = vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue({}) });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
await upload;
|
||||
@@ -69,11 +74,11 @@ describe("MatrixHttpApi", () => {
|
||||
});
|
||||
|
||||
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, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(fetchFn).not.toHaveBeenCalled();
|
||||
expect(xhr.open).toHaveBeenCalled();
|
||||
expect(getRequest()!.open).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should send access token in query params if header disabled", () => {
|
||||
@@ -85,11 +90,11 @@ describe("MatrixHttpApi", () => {
|
||||
onlyData: true,
|
||||
});
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(xhr.open).toHaveBeenCalledWith(
|
||||
expect(getRequest()!.open).toHaveBeenCalledWith(
|
||||
Method.Post,
|
||||
baseUrl.toLowerCase() + "/_matrix/media/v3/upload?access_token=token",
|
||||
);
|
||||
expect(xhr.setRequestHeader).not.toHaveBeenCalledWith("Authorization");
|
||||
expect(getRequest()!.setRequestHeader).not.toHaveBeenCalledWith("Authorization");
|
||||
});
|
||||
|
||||
it("should send access token in header by default", () => {
|
||||
@@ -100,14 +105,17 @@ describe("MatrixHttpApi", () => {
|
||||
onlyData: true,
|
||||
});
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/v3/upload");
|
||||
expect(xhr.setRequestHeader).toHaveBeenCalledWith("Authorization", "Bearer token");
|
||||
expect(getRequest()!.open).toHaveBeenCalledWith(
|
||||
Method.Post,
|
||||
baseUrl.toLowerCase() + "/_matrix/media/v3/upload",
|
||||
);
|
||||
expect(getRequest()!.setRequestHeader).toHaveBeenCalledWith("Authorization", "Bearer token");
|
||||
});
|
||||
|
||||
it("should include filename by default", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File, { name: "name" });
|
||||
expect(xhr.open).toHaveBeenCalledWith(
|
||||
expect(getRequest()!.open).toHaveBeenCalledWith(
|
||||
Method.Post,
|
||||
baseUrl.toLowerCase() + "/_matrix/media/v3/upload?filename=name",
|
||||
);
|
||||
@@ -116,42 +124,45 @@ describe("MatrixHttpApi", () => {
|
||||
it("should allow not sending the filename", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File, { name: "name", includeFilename: false });
|
||||
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/v3/upload");
|
||||
expect(getRequest()!.open).toHaveBeenCalledWith(
|
||||
Method.Post,
|
||||
baseUrl.toLowerCase() + "/_matrix/media/v3/upload",
|
||||
);
|
||||
});
|
||||
|
||||
it("should abort xhr when the upload is aborted", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
api.cancelUpload(upload);
|
||||
expect(xhr.abort).toHaveBeenCalled();
|
||||
expect(getRequest()!.abort).toHaveBeenCalled();
|
||||
return expect(upload).rejects.toThrow("Aborted");
|
||||
});
|
||||
|
||||
it("should timeout if no progress in 30s", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
jest.advanceTimersByTime(25000);
|
||||
vi.advanceTimersByTime(25000);
|
||||
// @ts-ignore
|
||||
xhr.upload.onprogress(new Event("progress", { loaded: 1, total: 100 }));
|
||||
jest.advanceTimersByTime(25000);
|
||||
expect(xhr.abort).not.toHaveBeenCalled();
|
||||
jest.advanceTimersByTime(5000);
|
||||
expect(xhr.abort).toHaveBeenCalled();
|
||||
getRequest()!.upload.onprogress(new Event("progress", { loaded: 1, total: 100 }));
|
||||
vi.advanceTimersByTime(25000);
|
||||
expect(getRequest()!.abort).not.toHaveBeenCalled();
|
||||
vi.advanceTimersByTime(5000);
|
||||
expect(getRequest()!.abort).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call progressHandler", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
const progressHandler = jest.fn();
|
||||
const progressHandler = vi.fn();
|
||||
upload = api.uploadContent({} as File, { progressHandler });
|
||||
const progressEvent = new Event("progress") as ProgressEvent;
|
||||
Object.assign(progressEvent, { loaded: 1, total: 100 });
|
||||
// @ts-ignore
|
||||
xhr.upload.onprogress(progressEvent);
|
||||
getRequest()!.upload.onprogress(progressEvent);
|
||||
expect(progressHandler).toHaveBeenCalledWith({ loaded: 1, total: 100 });
|
||||
|
||||
Object.assign(progressEvent, { loaded: 95, total: 100 });
|
||||
// @ts-ignore
|
||||
xhr.upload.onprogress(progressEvent);
|
||||
getRequest()!.upload.onprogress(progressEvent);
|
||||
expect(progressHandler).toHaveBeenCalledWith({ loaded: 95, total: 100 });
|
||||
});
|
||||
|
||||
@@ -159,11 +170,11 @@ describe("MatrixHttpApi", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
|
||||
xhr.readyState = DONE;
|
||||
xhr.responseText = "";
|
||||
xhr.status = 200;
|
||||
getRequest()!.readyState = DONE;
|
||||
getRequest()!.responseText = "";
|
||||
getRequest()!.status = 200;
|
||||
// @ts-ignore
|
||||
xhr.onreadystatechange?.(new Event("test"));
|
||||
getRequest()!.onreadystatechange?.(new Event("test"));
|
||||
|
||||
return expect(upload).rejects.toThrow("No response body.");
|
||||
});
|
||||
@@ -172,15 +183,15 @@ describe("MatrixHttpApi", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
|
||||
xhr.readyState = DONE;
|
||||
xhr.responseText = '{"errcode": "M_NOT_FOUND", "error": "Not found"}';
|
||||
xhr.status = 404;
|
||||
mocked(xhr.getResponseHeader).mockImplementation((name) =>
|
||||
getRequest()!.readyState = DONE;
|
||||
getRequest()!.responseText = '{"errcode": "M_NOT_FOUND", "error": "Not found"}';
|
||||
getRequest()!.status = 404;
|
||||
vi.mocked(getRequest()!.getResponseHeader).mockImplementation((name) =>
|
||||
name.toLowerCase() === "content-type" ? "application/json" : null,
|
||||
);
|
||||
mocked(xhr.getAllResponseHeaders).mockReturnValue("content-type: application/json\r\n");
|
||||
vi.mocked(getRequest()!.getAllResponseHeaders).mockReturnValue("content-type: application/json\r\n");
|
||||
// @ts-ignore
|
||||
xhr.onreadystatechange?.(new Event("test"));
|
||||
getRequest()!.onreadystatechange?.(new Event("test"));
|
||||
|
||||
return expect(upload).rejects.toThrow("Not found");
|
||||
});
|
||||
@@ -189,12 +200,12 @@ describe("MatrixHttpApi", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
|
||||
xhr.readyState = DONE;
|
||||
xhr.responseText = '{"content_uri": "mxc://server/foobar"}';
|
||||
xhr.status = 200;
|
||||
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
|
||||
getRequest()!.readyState = DONE;
|
||||
getRequest()!.responseText = '{"content_uri": "mxc://server/foobar"}';
|
||||
getRequest()!.status = 200;
|
||||
vi.mocked(getRequest()!.getResponseHeader).mockReturnValue("application/json");
|
||||
// @ts-ignore
|
||||
xhr.onreadystatechange?.(new Event("test"));
|
||||
getRequest()!.onreadystatechange?.(new Event("test"));
|
||||
|
||||
return expect(upload).resolves.toStrictEqual({ content_uri: "mxc://server/foobar" });
|
||||
});
|
||||
@@ -203,22 +214,22 @@ describe("MatrixHttpApi", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(api.cancelUpload(upload)).toBeTruthy();
|
||||
expect(xhr.abort).toHaveBeenCalled();
|
||||
expect(getRequest()!.abort).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return false when `cancelUpload` is called but unsuccessful", async () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
|
||||
xhr.readyState = DONE;
|
||||
xhr.status = 500;
|
||||
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
|
||||
getRequest()!.readyState = DONE;
|
||||
getRequest()!.status = 500;
|
||||
vi.mocked(getRequest()!.getResponseHeader).mockReturnValue("application/json");
|
||||
// @ts-ignore
|
||||
xhr.onreadystatechange?.(new Event("test"));
|
||||
getRequest()!.onreadystatechange?.(new Event("test"));
|
||||
await upload.catch(() => {});
|
||||
|
||||
expect(api.cancelUpload(upload)).toBeFalsy();
|
||||
expect(xhr.abort).not.toHaveBeenCalled();
|
||||
expect(getRequest()!.abort).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return active uploads in `getCurrentUploads`", () => {
|
||||
|
||||
@@ -14,51 +14,51 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import {
|
||||
anySignal,
|
||||
ConnectionError,
|
||||
HTTPError,
|
||||
MatrixError,
|
||||
MatrixSafetyError,
|
||||
MatrixSafetyErrorCode,
|
||||
parseErrorResponse,
|
||||
retryNetworkOperation,
|
||||
timeoutSignal,
|
||||
} from "../../../src";
|
||||
import { sleep } from "../../../src/utils";
|
||||
|
||||
jest.mock("../../../src/utils");
|
||||
vi.mock("../../../src/utils");
|
||||
// setupTests mocks `timeoutSignal` due to hanging timers
|
||||
jest.unmock("../../../src/http-api/utils");
|
||||
vi.unmock("../../../src/http-api/utils");
|
||||
|
||||
describe("timeoutSignal", () => {
|
||||
jest.useFakeTimers();
|
||||
vi.useFakeTimers();
|
||||
|
||||
it("should fire abort signal after specified timeout", () => {
|
||||
const signal = timeoutSignal(3000);
|
||||
const onabort = jest.fn();
|
||||
const onabort = vi.fn();
|
||||
signal.onabort = onabort;
|
||||
expect(signal.aborted).toBeFalsy();
|
||||
expect(onabort).not.toHaveBeenCalled();
|
||||
|
||||
jest.advanceTimersByTime(3000);
|
||||
vi.advanceTimersByTime(3000);
|
||||
expect(signal.aborted).toBeTruthy();
|
||||
expect(onabort).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("anySignal", () => {
|
||||
jest.useFakeTimers();
|
||||
vi.useFakeTimers();
|
||||
|
||||
it("should fire when any signal fires", () => {
|
||||
const { signal } = anySignal([timeoutSignal(3000), timeoutSignal(2000)]);
|
||||
|
||||
const onabort = jest.fn();
|
||||
const onabort = vi.fn();
|
||||
signal.onabort = onabort;
|
||||
expect(signal.aborted).toBeFalsy();
|
||||
expect(onabort).not.toHaveBeenCalled();
|
||||
|
||||
jest.advanceTimersByTime(2000);
|
||||
vi.advanceTimersByTime(2000);
|
||||
expect(signal.aborted).toBeTruthy();
|
||||
expect(onabort).toHaveBeenCalled();
|
||||
});
|
||||
@@ -66,13 +66,13 @@ describe("anySignal", () => {
|
||||
it("should cleanup when instructed", () => {
|
||||
const { signal, cleanup } = anySignal([timeoutSignal(3000), timeoutSignal(2000)]);
|
||||
|
||||
const onabort = jest.fn();
|
||||
const onabort = vi.fn();
|
||||
signal.onabort = onabort;
|
||||
expect(signal.aborted).toBeFalsy();
|
||||
expect(onabort).not.toHaveBeenCalled();
|
||||
|
||||
cleanup();
|
||||
jest.advanceTimersByTime(2000);
|
||||
vi.advanceTimersByTime(2000);
|
||||
expect(signal.aborted).toBeFalsy();
|
||||
expect(onabort).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -86,9 +86,14 @@ describe("anySignal", () => {
|
||||
});
|
||||
|
||||
describe("parseErrorResponse", () => {
|
||||
const url = "https://example.org";
|
||||
|
||||
let headers: Headers;
|
||||
const xhrHeaderMethods = {
|
||||
getResponseHeader: (name: string) => headers.get(name),
|
||||
responseURL: url,
|
||||
getResponseHeader: (name: string) => {
|
||||
headers.get(name);
|
||||
},
|
||||
getAllResponseHeaders: () => {
|
||||
let allHeaders = "";
|
||||
headers.forEach((value, key) => {
|
||||
@@ -118,6 +123,9 @@ describe("parseErrorResponse", () => {
|
||||
errcode: "TEST",
|
||||
},
|
||||
500,
|
||||
url,
|
||||
undefined,
|
||||
expect.any(Headers),
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -127,6 +135,7 @@ describe("parseErrorResponse", () => {
|
||||
expect(
|
||||
parseErrorResponse(
|
||||
{
|
||||
url,
|
||||
headers,
|
||||
status: 500,
|
||||
} as Response,
|
||||
@@ -138,6 +147,9 @@ describe("parseErrorResponse", () => {
|
||||
errcode: "TEST",
|
||||
},
|
||||
500,
|
||||
url,
|
||||
undefined,
|
||||
expect.any(Headers),
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -147,8 +159,8 @@ describe("parseErrorResponse", () => {
|
||||
expect(
|
||||
parseErrorResponse(
|
||||
{
|
||||
responseURL: "https://example.com",
|
||||
...xhrHeaderMethods,
|
||||
responseURL: "https://example.com",
|
||||
status: 500,
|
||||
} as XMLHttpRequest,
|
||||
'{"errcode": "TEST"}',
|
||||
@@ -160,6 +172,8 @@ describe("parseErrorResponse", () => {
|
||||
},
|
||||
500,
|
||||
"https://example.com",
|
||||
undefined,
|
||||
expect.any(Headers),
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -182,9 +196,40 @@ describe("parseErrorResponse", () => {
|
||||
},
|
||||
500,
|
||||
"https://example.com",
|
||||
undefined,
|
||||
expect.any(Headers),
|
||||
),
|
||||
);
|
||||
});
|
||||
it.each([
|
||||
{
|
||||
errcode: MatrixSafetyErrorCode.name,
|
||||
error: "Spammy",
|
||||
},
|
||||
{
|
||||
errcode: MatrixSafetyErrorCode.name,
|
||||
error: "Spammy",
|
||||
expiry: 5000,
|
||||
},
|
||||
{
|
||||
errcode: MatrixSafetyErrorCode.name,
|
||||
error: "Spammy",
|
||||
harms: ["m.spam", "org.example.additional-harm"],
|
||||
expiry: 5000,
|
||||
},
|
||||
])("should resolve MatrixSafetyErrors from fetch", (errContent) => {
|
||||
headers.set("Content-Type", "application/json");
|
||||
const value = parseErrorResponse(
|
||||
{
|
||||
headers,
|
||||
status: 400,
|
||||
} as Response,
|
||||
JSON.stringify(errContent),
|
||||
) as MatrixSafetyError;
|
||||
expect(value).toBeInstanceOf(MatrixSafetyError);
|
||||
expect(value.harms.size).toEqual(errContent.harms?.length ?? 0);
|
||||
expect(value.expiry?.getTime()).toEqual(errContent.expiry);
|
||||
});
|
||||
|
||||
describe("with HTTP headers", () => {
|
||||
function addHeaders(headers: Headers) {
|
||||
@@ -196,7 +241,7 @@ describe("parseErrorResponse", () => {
|
||||
}
|
||||
|
||||
function compareHeaders(expectedHeaders: Headers, otherHeaders: Headers | undefined) {
|
||||
expect(new Map(otherHeaders)).toEqual(new Map(expectedHeaders));
|
||||
expect(new Map(otherHeaders as any)).toEqual(new Map(expectedHeaders as any));
|
||||
}
|
||||
|
||||
it("should resolve HTTP Errors from XHR with headers", () => {
|
||||
@@ -265,7 +310,7 @@ describe("parseErrorResponse", () => {
|
||||
} as Response,
|
||||
'{"errcode": "TEST"}',
|
||||
),
|
||||
).toStrictEqual(new HTTPError("Server returned 500 error", 500));
|
||||
).toStrictEqual(new HTTPError("Server returned 500 error", 500, expect.any(Headers)));
|
||||
});
|
||||
|
||||
it("should handle empty type gracefully", () => {
|
||||
@@ -304,27 +349,27 @@ describe("parseErrorResponse", () => {
|
||||
} as Response,
|
||||
"I'm a teapot",
|
||||
),
|
||||
).toStrictEqual(new HTTPError("Server returned 418 error: I'm a teapot", 418));
|
||||
).toStrictEqual(new HTTPError("Server returned 418 error: I'm a teapot", 418, expect.any(Headers)));
|
||||
});
|
||||
});
|
||||
|
||||
describe("retryNetworkOperation", () => {
|
||||
it("should retry given number of times with exponential sleeps", async () => {
|
||||
const err = new ConnectionError("test");
|
||||
const fn = jest.fn().mockRejectedValue(err);
|
||||
mocked(sleep).mockResolvedValue(undefined);
|
||||
const fn = vi.fn().mockRejectedValue(err);
|
||||
vi.mocked(sleep).mockResolvedValue(undefined);
|
||||
await expect(retryNetworkOperation(4, fn)).rejects.toThrow(err);
|
||||
expect(fn).toHaveBeenCalledTimes(4);
|
||||
expect(mocked(sleep)).toHaveBeenCalledTimes(3);
|
||||
expect(mocked(sleep).mock.calls[0][0]).toBe(2000);
|
||||
expect(mocked(sleep).mock.calls[1][0]).toBe(4000);
|
||||
expect(mocked(sleep).mock.calls[2][0]).toBe(8000);
|
||||
expect(vi.mocked(sleep)).toHaveBeenCalledTimes(3);
|
||||
expect(vi.mocked(sleep).mock.calls[0][0]).toBe(2000);
|
||||
expect(vi.mocked(sleep).mock.calls[1][0]).toBe(4000);
|
||||
expect(vi.mocked(sleep).mock.calls[2][0]).toBe(8000);
|
||||
});
|
||||
|
||||
it("should bail out on errors other than ConnectionError", async () => {
|
||||
const err = new TypeError("invalid JSON");
|
||||
const fn = jest.fn().mockRejectedValue(err);
|
||||
mocked(sleep).mockResolvedValue(undefined);
|
||||
const fn = vi.fn().mockRejectedValue(err);
|
||||
vi.mocked(sleep).mockResolvedValue(undefined);
|
||||
await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -334,10 +379,10 @@ describe("retryNetworkOperation", () => {
|
||||
const err2 = new ConnectionError("test2");
|
||||
const err3 = new ConnectionError("test3");
|
||||
const errors = [err1, err2, err3];
|
||||
const fn = jest.fn().mockImplementation(() => {
|
||||
const fn = vi.fn().mockImplementation(() => {
|
||||
throw errors.shift();
|
||||
});
|
||||
mocked(sleep).mockResolvedValue(undefined);
|
||||
vi.mocked(sleep).mockResolvedValue(undefined);
|
||||
await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
|
||||
import { type MatrixClient } from "../../src/client";
|
||||
import { logger } from "../../src/logger";
|
||||
import { InteractiveAuth, AuthType } from "../../src/interactive-auth";
|
||||
import { InteractiveAuth, AuthType, NoAuthFlowFoundError } from "../../src/interactive-auth";
|
||||
import { HTTPError, MatrixError } from "../../src/http-api";
|
||||
import { sleep } from "../../src/utils";
|
||||
import { secureRandomString } from "../../src/randomstring";
|
||||
@@ -34,14 +34,14 @@ const getFakeClient = (): MatrixClient => new FakeClient() as unknown as MatrixC
|
||||
|
||||
describe("InteractiveAuth", () => {
|
||||
it("should start an auth stage and complete it", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
doRequest: doRequest,
|
||||
stateUpdated: stateUpdated,
|
||||
requestEmailToken: jest.fn(),
|
||||
requestEmailToken: vi.fn(),
|
||||
authData: {
|
||||
session: "sessionId",
|
||||
flows: [{ stages: [AuthType.Password] }],
|
||||
@@ -83,14 +83,14 @@ describe("InteractiveAuth", () => {
|
||||
});
|
||||
|
||||
it("should handle auth errcode presence", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
doRequest: doRequest,
|
||||
stateUpdated: stateUpdated,
|
||||
requestEmailToken: jest.fn(),
|
||||
requestEmailToken: vi.fn(),
|
||||
authData: {
|
||||
session: "sessionId",
|
||||
flows: [{ stages: [AuthType.Password] }],
|
||||
@@ -132,9 +132,9 @@ describe("InteractiveAuth", () => {
|
||||
});
|
||||
|
||||
it("should handle set emailSid for email flow", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
const requestEmailToken = vi.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
doRequest,
|
||||
@@ -186,9 +186,9 @@ describe("InteractiveAuth", () => {
|
||||
});
|
||||
|
||||
it("should make a request if no authdata is provided", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
const requestEmailToken = vi.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
@@ -248,9 +248,9 @@ describe("InteractiveAuth", () => {
|
||||
});
|
||||
|
||||
it("should make a request if authdata is null", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
const requestEmailToken = vi.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
@@ -310,9 +310,9 @@ describe("InteractiveAuth", () => {
|
||||
});
|
||||
|
||||
it("should start an auth stage and reject if no auth flow", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
const requestEmailToken = vi.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
@@ -337,13 +337,15 @@ describe("InteractiveAuth", () => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(new Error("No appropriate authentication flow found"));
|
||||
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(
|
||||
new NoAuthFlowFoundError("No appropriate authentication flow found", [], []),
|
||||
);
|
||||
});
|
||||
|
||||
it("should start an auth stage and reject if no auth flow but has session", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
const requestEmailToken = vi.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
@@ -372,13 +374,15 @@ describe("InteractiveAuth", () => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(new Error("No appropriate authentication flow found"));
|
||||
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(
|
||||
new NoAuthFlowFoundError("No appropriate authentication flow found", [], []),
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle unexpected error types without data property set", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
const requestEmailToken = vi.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
@@ -397,13 +401,13 @@ describe("InteractiveAuth", () => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(new Error("myerror"));
|
||||
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(new HTTPError("myerror", 401));
|
||||
});
|
||||
|
||||
it("should allow dummy auth", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
const requestEmailToken = vi.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
@@ -435,9 +439,9 @@ describe("InteractiveAuth", () => {
|
||||
|
||||
describe("requestEmailToken", () => {
|
||||
it("increases auth attempts", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
const requestEmailToken = vi.fn();
|
||||
requestEmailToken.mockImplementation(async () => ({ sid: "" }));
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
@@ -464,9 +468,9 @@ describe("InteractiveAuth", () => {
|
||||
});
|
||||
|
||||
it("passes errors through", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
const requestEmailToken = vi.fn();
|
||||
requestEmailToken.mockImplementation(async () => {
|
||||
throw new Error("unspecific network error");
|
||||
});
|
||||
@@ -482,9 +486,9 @@ describe("InteractiveAuth", () => {
|
||||
});
|
||||
|
||||
it("only starts one request at a time", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
const requestEmailToken = vi.fn();
|
||||
requestEmailToken.mockImplementation(() => sleep(500, { sid: "" }));
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
@@ -499,9 +503,9 @@ describe("InteractiveAuth", () => {
|
||||
});
|
||||
|
||||
it("stores result in email sid", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
const requestEmailToken = vi.fn();
|
||||
const sid = secureRandomString(24);
|
||||
requestEmailToken.mockImplementation(() => sleep(500, { sid }));
|
||||
|
||||
@@ -518,14 +522,14 @@ describe("InteractiveAuth", () => {
|
||||
});
|
||||
|
||||
it("should prioritise shorter flows", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
doRequest: doRequest,
|
||||
stateUpdated: stateUpdated,
|
||||
requestEmailToken: jest.fn(),
|
||||
requestEmailToken: vi.fn(),
|
||||
authData: {
|
||||
session: "sessionId",
|
||||
flows: [{ stages: [AuthType.Recaptcha, AuthType.Password] }, { stages: [AuthType.Password] }],
|
||||
@@ -539,14 +543,14 @@ describe("InteractiveAuth", () => {
|
||||
});
|
||||
|
||||
it("should prioritise flows with entirely supported stages", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
doRequest: doRequest,
|
||||
stateUpdated: stateUpdated,
|
||||
requestEmailToken: jest.fn(),
|
||||
requestEmailToken: vi.fn(),
|
||||
authData: {
|
||||
session: "sessionId",
|
||||
flows: [{ stages: ["com.devture.shared_secret_auth"] }, { stages: [AuthType.Password] }],
|
||||
@@ -561,14 +565,14 @@ describe("InteractiveAuth", () => {
|
||||
});
|
||||
|
||||
it("should fire stateUpdated callback with error when a request fails", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
doRequest: doRequest,
|
||||
stateUpdated: stateUpdated,
|
||||
requestEmailToken: jest.fn(),
|
||||
requestEmailToken: vi.fn(),
|
||||
authData: {
|
||||
session: "sessionId",
|
||||
flows: [{ stages: [AuthType.Password] }],
|
||||
|
||||
@@ -23,7 +23,7 @@ let client: MatrixClient;
|
||||
describe("Local notification settings", () => {
|
||||
beforeEach(() => {
|
||||
client = new TestClient("@alice:matrix.org", "123", undefined, undefined, undefined).client;
|
||||
client.setAccountData = jest.fn();
|
||||
client.setAccountData = vi.fn();
|
||||
});
|
||||
|
||||
describe("Lets you set local notification settings", () => {
|
||||
|
||||
@@ -21,12 +21,12 @@ import loglevel from "loglevel";
|
||||
import { DebugLogger, logger } from "../../src/logger.ts";
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("logger", () => {
|
||||
it("should log to console by default", () => {
|
||||
jest.spyOn(console, "debug").mockReturnValue(undefined);
|
||||
vi.spyOn(console, "debug").mockReturnValue(undefined);
|
||||
logger.debug("test1");
|
||||
logger.log("test2");
|
||||
|
||||
@@ -35,8 +35,8 @@ describe("logger", () => {
|
||||
});
|
||||
|
||||
it("should allow creation of child loggers which add a prefix", () => {
|
||||
jest.spyOn(loglevel, "getLogger");
|
||||
jest.spyOn(console, "debug").mockReturnValue(undefined);
|
||||
vi.spyOn(loglevel, "getLogger");
|
||||
vi.spyOn(console, "debug").mockReturnValue(undefined);
|
||||
|
||||
const childLogger = logger.getChild("[prefix1]");
|
||||
expect(loglevel.getLogger).toHaveBeenCalledWith("matrix-[prefix1]");
|
||||
@@ -52,7 +52,7 @@ describe("logger", () => {
|
||||
|
||||
describe("DebugLogger", () => {
|
||||
it("should handle empty log messages", () => {
|
||||
const mockTarget = jest.fn();
|
||||
const mockTarget = vi.fn();
|
||||
const logger = new DebugLogger(mockTarget as any);
|
||||
logger.info();
|
||||
expect(mockTarget).toHaveBeenCalledTimes(1);
|
||||
@@ -60,7 +60,7 @@ describe("DebugLogger", () => {
|
||||
});
|
||||
|
||||
it("should handle logging an Error", () => {
|
||||
const mockTarget = jest.fn();
|
||||
const mockTarget = vi.fn();
|
||||
const logger = new DebugLogger(mockTarget as any);
|
||||
|
||||
// If there is a stack and a message, we use the stack.
|
||||
@@ -79,7 +79,7 @@ describe("DebugLogger", () => {
|
||||
});
|
||||
|
||||
it("should handle logging an object", () => {
|
||||
const mockTarget = jest.fn();
|
||||
const mockTarget = vi.fn();
|
||||
const logger = new DebugLogger(mockTarget as any);
|
||||
|
||||
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 { SSOAction } from "../../src/@types/auth";
|
||||
@@ -51,27 +51,26 @@ describe("SSO login URL", function () {
|
||||
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, undefined);
|
||||
const url = new URL(urlString);
|
||||
expect(url.searchParams.has("org.matrix.msc3824.action")).toBe(false);
|
||||
expect(url.searchParams.has("action")).toBe(false);
|
||||
});
|
||||
|
||||
it("register", function () {
|
||||
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.REGISTER);
|
||||
const url = new URL(urlString);
|
||||
expect(url.searchParams.get("org.matrix.msc3824.action")).toEqual("register");
|
||||
expect(url.searchParams.get("action")).toEqual("register");
|
||||
});
|
||||
|
||||
it("login", function () {
|
||||
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.LOGIN);
|
||||
const url = new URL(urlString);
|
||||
expect(url.searchParams.get("org.matrix.msc3824.action")).toEqual("login");
|
||||
expect(url.searchParams.get("action")).toEqual("login");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("refreshToken", () => {
|
||||
afterEach(() => {
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
it("requests the correctly-prefixed /refresh endpoint when server correctly accepts /v3", async () => {
|
||||
const client = createExampleMatrixClient();
|
||||
|
||||
|
||||
+273
-149
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2023-2026 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -14,31 +14,34 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type MatrixEvent } from "../../../src";
|
||||
import {
|
||||
CallMembership,
|
||||
type SessionMembershipData,
|
||||
DEFAULT_EXPIRE_DURATION,
|
||||
type RtcMembershipData,
|
||||
} from "../../../src/matrixrtc/CallMembership";
|
||||
import { membershipTemplate } from "./mocks";
|
||||
import { type RtcMembershipData, type SessionMembershipData } from "../../../src/matrixrtc/membershipData/index.ts";
|
||||
import { type IContent, type MatrixEvent } from "../../../src/models/event.ts";
|
||||
import { EventType } from "../../../src/@types/event.ts";
|
||||
import { CallMembership, DEFAULT_EXPIRE_DURATION } from "../../../src/matrixrtc/CallMembership.ts";
|
||||
|
||||
function makeMockEvent(originTs = 0): MatrixEvent {
|
||||
return {
|
||||
getTs: jest.fn().mockReturnValue(originTs),
|
||||
getSender: jest.fn().mockReturnValue("@alice:example.org"),
|
||||
getId: jest.fn().mockReturnValue("$eventid"),
|
||||
} as unknown as MatrixEvent;
|
||||
function createCallMembership(ev: MatrixEvent, content: IContent): CallMembership {
|
||||
vi.mocked(ev.getContent).mockReturnValue(content);
|
||||
const data = CallMembership.membershipDataFromMatrixEvent(ev);
|
||||
return new CallMembership(ev, data, "xx");
|
||||
}
|
||||
|
||||
describe("CallMembership", () => {
|
||||
describe("SessionMembershipData", () => {
|
||||
function makeMockEvent(originTs = 0): MatrixEvent {
|
||||
return {
|
||||
getTs: vi.fn().mockReturnValue(originTs),
|
||||
getSender: vi.fn().mockReturnValue("@alice:example.org"),
|
||||
getId: vi.fn().mockReturnValue("$eventid"),
|
||||
getContent: vi.fn().mockReturnValue({}),
|
||||
getType: vi.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
||||
} as unknown as MatrixEvent;
|
||||
}
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
const membershipTemplate: SessionMembershipData = {
|
||||
@@ -53,29 +56,29 @@ describe("CallMembership", () => {
|
||||
|
||||
it("rejects membership with no device_id", () => {
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { device_id: undefined }));
|
||||
createCallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { device_id: undefined }));
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("rejects membership with no call_id", () => {
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { call_id: undefined }));
|
||||
createCallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { call_id: undefined }));
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("allow membership with no scope", () => {
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined }));
|
||||
createCallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined }));
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("uses event timestamp if no created_ts", () => {
|
||||
const membership = new CallMembership(makeMockEvent(12345), membershipTemplate);
|
||||
const membership = createCallMembership(makeMockEvent(12345), membershipTemplate);
|
||||
expect(membership.createdTs()).toEqual(12345);
|
||||
});
|
||||
|
||||
it("uses created_ts if present", () => {
|
||||
const membership = new CallMembership(
|
||||
const membership = createCallMembership(
|
||||
makeMockEvent(12345),
|
||||
Object.assign({}, membershipTemplate, { created_ts: 67890 }),
|
||||
);
|
||||
@@ -84,28 +87,28 @@ describe("CallMembership", () => {
|
||||
|
||||
it("considers memberships unexpired if local age low enough", () => {
|
||||
const fakeEvent = makeMockEvent(1000);
|
||||
fakeEvent.getTs = jest.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION - 1));
|
||||
expect(new CallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(false);
|
||||
fakeEvent.getTs = vi.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION - 1));
|
||||
expect(createCallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(false);
|
||||
});
|
||||
|
||||
it("considers memberships expired if local age large enough", () => {
|
||||
const fakeEvent = makeMockEvent(1000);
|
||||
fakeEvent.getTs = jest.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION + 1));
|
||||
expect(new CallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(true);
|
||||
fakeEvent.getTs = vi.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION + 1));
|
||||
expect(createCallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns preferred foci", () => {
|
||||
const fakeEvent = makeMockEvent();
|
||||
const mockFocus = { type: "this_is_a_mock_focus" };
|
||||
const membership = new CallMembership(fakeEvent, { ...membershipTemplate, foci_preferred: [mockFocus] });
|
||||
const membership = createCallMembership(fakeEvent, { ...membershipTemplate, foci_preferred: [mockFocus] });
|
||||
expect(membership.transports).toEqual([mockFocus]);
|
||||
});
|
||||
|
||||
describe("getTransport", () => {
|
||||
const mockFocus = { type: "this_is_a_mock_focus" };
|
||||
const oldestMembership = new CallMembership(makeMockEvent(), membershipTemplate);
|
||||
const oldestMembership = createCallMembership(makeMockEvent(), membershipTemplate);
|
||||
it("gets the correct active transport with oldest_membership", () => {
|
||||
const membership = new CallMembership(makeMockEvent(), {
|
||||
const membership = createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
foci_preferred: [mockFocus],
|
||||
focus_active: { type: "livekit", focus_selection: "oldest_membership" },
|
||||
@@ -119,7 +122,7 @@ describe("CallMembership", () => {
|
||||
});
|
||||
|
||||
it("gets the correct active transport with multi_sfu", () => {
|
||||
const membership = new CallMembership(makeMockEvent(), {
|
||||
const membership = createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
foci_preferred: [mockFocus],
|
||||
focus_active: { type: "livekit", focus_selection: "multi_sfu" },
|
||||
@@ -132,7 +135,7 @@ describe("CallMembership", () => {
|
||||
expect(membership.getTransport(oldestMembership)).toBe(mockFocus);
|
||||
});
|
||||
it("does not provide focus if the selection method is unknown", () => {
|
||||
const membership = new CallMembership(makeMockEvent(), {
|
||||
const membership = createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
foci_preferred: [mockFocus],
|
||||
focus_active: { type: "livekit", focus_selection: "unknown" },
|
||||
@@ -143,7 +146,7 @@ describe("CallMembership", () => {
|
||||
});
|
||||
});
|
||||
describe("correct values from computed fields", () => {
|
||||
const membership = new CallMembership(makeMockEvent(), membershipTemplate);
|
||||
const membership = createCallMembership(makeMockEvent(), membershipTemplate);
|
||||
it("returns correct sender", () => {
|
||||
expect(membership.sender).toBe("@alice:example.org");
|
||||
});
|
||||
@@ -151,8 +154,35 @@ describe("CallMembership", () => {
|
||||
expect(membership.eventId).toBe("$eventid");
|
||||
});
|
||||
it("returns correct slot_id", () => {
|
||||
expect(membership.slotId).toBe("m.call#");
|
||||
expect(membership.slotDescription).toStrictEqual({ id: "", application: "m.call" });
|
||||
// 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");
|
||||
@@ -170,7 +200,7 @@ describe("CallMembership", () => {
|
||||
expect(membership.scope).toBe("m.room");
|
||||
});
|
||||
it("returns correct membershipID", () => {
|
||||
expect(membership.membershipID).toBe("0");
|
||||
expect(membership.membershipID).toBe("@alice:example.org:AAAAAAA");
|
||||
});
|
||||
it("returns correct unused fields", () => {
|
||||
expect(membership.getAbsoluteExpiry()).toBe(DEFAULT_EXPIRE_DURATION);
|
||||
@@ -178,9 +208,40 @@ describe("CallMembership", () => {
|
||||
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("RtcMembershipData", () => {
|
||||
function makeMockEvent(originTs = 0, content: IContent = {}): MatrixEvent {
|
||||
return {
|
||||
getTs: vi.fn().mockReturnValue(originTs),
|
||||
getSender: vi.fn().mockReturnValue("@alice:example.org"),
|
||||
getId: vi.fn().mockReturnValue("$eventid"),
|
||||
getContent: vi.fn().mockReturnValue(content),
|
||||
getType: vi.fn().mockReturnValue(EventType.RTCMembership),
|
||||
} as unknown as MatrixEvent;
|
||||
}
|
||||
const membershipTemplate: RtcMembershipData = {
|
||||
slot_id: "m.call#",
|
||||
application: { "type": "m.call", "m.call.id": "", "m.call.intent": "voice" },
|
||||
@@ -192,29 +253,34 @@ describe("CallMembership", () => {
|
||||
|
||||
it("rejects membership with no slot_id", () => {
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: undefined });
|
||||
createCallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: undefined });
|
||||
}).toThrow();
|
||||
});
|
||||
it("rejects membership with invalid slot_id", () => {
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: "invalid_slot_id" });
|
||||
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(() => {
|
||||
new CallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: "m.call#" });
|
||||
createCallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: "m.call#" });
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("rejects membership with no application", () => {
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), { ...membershipTemplate, application: undefined });
|
||||
createCallMembership(makeMockEvent(), { ...membershipTemplate, application: undefined });
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("rejects membership with incorrect application", () => {
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), {
|
||||
createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
application: { wrong_type_key: "unknown" },
|
||||
});
|
||||
@@ -223,34 +289,34 @@ describe("CallMembership", () => {
|
||||
|
||||
it("rejects membership with no member", () => {
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), { ...membershipTemplate, member: undefined });
|
||||
createCallMembership(makeMockEvent(), { ...membershipTemplate, member: undefined });
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("rejects membership with incorrect member", () => {
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), { ...membershipTemplate, member: { i: "test" } });
|
||||
createCallMembership(makeMockEvent(), { ...membershipTemplate, member: { i: "test" } });
|
||||
}).toThrow();
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), {
|
||||
createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
member: { id: "test", device_id: "test", user_id_wrong: "test" },
|
||||
});
|
||||
}).toThrow();
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), {
|
||||
createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
member: { id: "test", device_id_wrong: "test", user_id_wrong: "test" },
|
||||
});
|
||||
}).toThrow();
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), {
|
||||
createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
member: { id: "test", device_id: "test", user_id: "@@test" },
|
||||
});
|
||||
}).toThrow();
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), {
|
||||
createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
member: { id: "test", device_id: "test", user_id: "@test-wrong-user:user.id" },
|
||||
});
|
||||
@@ -258,41 +324,41 @@ describe("CallMembership", () => {
|
||||
});
|
||||
it("rejects membership with incorrect sticky_key", () => {
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), membershipTemplate);
|
||||
createCallMembership(makeMockEvent(), membershipTemplate);
|
||||
}).not.toThrow();
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), {
|
||||
createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
sticky_key: 1,
|
||||
msc4354_sticky_key: undefined,
|
||||
});
|
||||
}).toThrow();
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), {
|
||||
createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
sticky_key: "1",
|
||||
msc4354_sticky_key: undefined,
|
||||
});
|
||||
}).not.toThrow();
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), { ...membershipTemplate, msc4354_sticky_key: undefined });
|
||||
createCallMembership(makeMockEvent(), { ...membershipTemplate, msc4354_sticky_key: undefined });
|
||||
}).toThrow();
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), {
|
||||
createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
msc4354_sticky_key: 1,
|
||||
sticky_key: "valid",
|
||||
});
|
||||
}).toThrow();
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), {
|
||||
createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
msc4354_sticky_key: "valid",
|
||||
sticky_key: "valid",
|
||||
});
|
||||
}).not.toThrow();
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), {
|
||||
createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
msc4354_sticky_key: "valid_but_different",
|
||||
sticky_key: "valid",
|
||||
@@ -300,21 +366,17 @@ describe("CallMembership", () => {
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("considers memberships unexpired if local age low enough", () => {
|
||||
// TODO link prev event
|
||||
});
|
||||
|
||||
it("considers memberships expired if local age large enough", () => {
|
||||
// TODO link prev event
|
||||
});
|
||||
// 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 = new CallMembership(makeMockEvent(), {
|
||||
const oldestMembership = createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
rtc_transports: [{ type: "oldest_transport" }],
|
||||
});
|
||||
const membership = new CallMembership(makeMockEvent(), membershipTemplate);
|
||||
const membership = createCallMembership(makeMockEvent(), membershipTemplate);
|
||||
|
||||
// if we are the oldest member we use our focus.
|
||||
expect(membership.getTransport(membership)).toStrictEqual({ type: "livekit" });
|
||||
@@ -324,7 +386,7 @@ describe("CallMembership", () => {
|
||||
});
|
||||
});
|
||||
describe("correct values from computed fields", () => {
|
||||
const membership = new CallMembership(makeMockEvent(), membershipTemplate);
|
||||
const membership = createCallMembership(makeMockEvent(), membershipTemplate);
|
||||
it("returns correct sender", () => {
|
||||
expect(membership.sender).toBe("@alice:example.org");
|
||||
});
|
||||
@@ -363,28 +425,9 @@ describe("CallMembership", () => {
|
||||
expect(membership.isExpired()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("expiry calculation", () => {
|
||||
let fakeEvent: MatrixEvent;
|
||||
let membership: CallMembership;
|
||||
|
||||
beforeEach(() => {
|
||||
// server origin timestamp for this event is 1000
|
||||
fakeEvent = makeMockEvent(1000);
|
||||
membership = new CallMembership(fakeEvent!, membershipTemplate);
|
||||
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("calculates time until expiry", () => {
|
||||
jest.setSystemTime(2000);
|
||||
// should be using absolute expiry time
|
||||
expect(membership.getMsUntilExpiry()).toEqual(DEFAULT_EXPIRE_DURATION - 1000);
|
||||
it("uses unpadded base64 for RTC backend identities", async () => {
|
||||
const membership = await CallMembership.parseFromEvent(makeMockEvent(0, { ...membershipTemplate }));
|
||||
expect(membership.rtcBackendIdentity).toBe("jUZ0Q1yF5nV3LlAI5xfD1I7BPnAytJaPEAR57EXjJ6s");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,27 +14,55 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ClientEvent, EventTimeline, MatrixClient, type Room } from "../../../src";
|
||||
import { RoomStateEvent } from "../../../src/models/room-state";
|
||||
import { MatrixRTCSessionManager, MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc/MatrixRTCSessionManager";
|
||||
import { makeMockRoom, type MembershipData, membershipTemplate, mockRoomState, mockRTCEvent } from "./mocks";
|
||||
import { ClientEvent, EventTimeline, MatrixClient, type Room, RoomStateEvent } from "../../../src";
|
||||
import { MatrixRTCSessionManager, MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc";
|
||||
import {
|
||||
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.each([{ eventKind: "sticky" }, { eventKind: "memberState" }])(
|
||||
"MatrixRTCSessionManager ($eventKind)",
|
||||
({ eventKind }) => {
|
||||
let client: MatrixClient;
|
||||
|
||||
function sendLeaveMembership(room: Room, membershipData: MembershipData[]): void {
|
||||
function generateMembership(opts: { type: string; callId?: string } = { type: "m.call" }): MembershipData {
|
||||
if (eventKind === "sticky") {
|
||||
return {
|
||||
...rtcMembershipTemplate,
|
||||
slot_id: opts.callId ? `${opts.type}#${opts.callId}` : rtcMembershipTemplate.slot_id,
|
||||
application: {
|
||||
...rtcMembershipTemplate.application,
|
||||
type: opts.type,
|
||||
},
|
||||
} satisfies RtcMembershipData & { user_id: string };
|
||||
}
|
||||
|
||||
return {
|
||||
...sessionMembershipTemplate,
|
||||
application: opts.type,
|
||||
call_id: opts.callId ?? sessionMembershipTemplate.call_id, // approximate version.
|
||||
} satisfies SessionMembershipData & { user_id: string };
|
||||
}
|
||||
|
||||
async function sendLeaveMembership(room: Room, membershipData: MembershipData[]): Promise<void> {
|
||||
if (eventKind === "memberState") {
|
||||
mockRoomState(room, [{ user_id: membershipTemplate.user_id }]);
|
||||
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: membershipTemplate.user_id });
|
||||
membershipData.splice(0, 1, { user_id: sessionMembershipTemplate.user_id });
|
||||
client.emit(ClientEvent.Event, mockRTCEvent(membershipData[0], room.roomId, 10000));
|
||||
}
|
||||
await flushPromises();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -45,30 +73,26 @@ describe.each([{ eventKind: "sticky" }, { eventKind: "memberState" }])(
|
||||
afterEach(() => {
|
||||
client.stopClient();
|
||||
client.matrixRTC.stop();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("Fires event when session starts", () => {
|
||||
const onStarted = jest.fn();
|
||||
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
||||
|
||||
try {
|
||||
const room1 = makeMockRoom([membershipTemplate], eventKind === "sticky");
|
||||
jest.spyOn(client, "getRooms").mockReturnValue([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 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 = jest.fn();
|
||||
const onStarted = vi.fn();
|
||||
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
||||
|
||||
try {
|
||||
const room1 = makeMockRoom([{ ...membershipTemplate, application: "m.other" }], eventKind === "sticky");
|
||||
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||
const room1 = makeMockRoom([generateMembership({ type: "m.other" })], eventKind === "sticky");
|
||||
vi.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||
|
||||
client.emit(ClientEvent.Room, room1);
|
||||
expect(onStarted).not.toHaveBeenCalled();
|
||||
@@ -77,23 +101,30 @@ describe.each([{ eventKind: "sticky" }, { eventKind: "memberState" }])(
|
||||
}
|
||||
});
|
||||
|
||||
it("Fires event when session ends", () => {
|
||||
const onEnded = jest.fn();
|
||||
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
|
||||
const membershipData: MembershipData[] = [membershipTemplate];
|
||||
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");
|
||||
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room1);
|
||||
vi.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||
vi.spyOn(client, "getRoom").mockReturnValue(room1);
|
||||
client.emit(ClientEvent.Room, room1);
|
||||
await sessionStartedPromise;
|
||||
await sendLeaveMembership(room1, membershipData);
|
||||
|
||||
sendLeaveMembership(room1, membershipData);
|
||||
|
||||
expect(onEnded).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
|
||||
await expect(sessionEndedPromise).resolves.toStrictEqual([
|
||||
room1.roomId,
|
||||
client.matrixRTC.getActiveRoomSession(room1),
|
||||
]);
|
||||
});
|
||||
|
||||
it("Fires correctly with custom sessionDescription", () => {
|
||||
const onStarted = jest.fn();
|
||||
const onEnded = jest.fn();
|
||||
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",
|
||||
@@ -104,53 +135,51 @@ describe.each([{ eventKind: "sticky" }, { eventKind: "memberState" }])(
|
||||
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)),
|
||||
);
|
||||
|
||||
try {
|
||||
// Create a session for applicaation m.other, we ignore this session ecause it lacks a call_id
|
||||
const room1MembershipData: MembershipData[] = [{ ...membershipTemplate, application: "m.other" }];
|
||||
const room1 = makeMockRoom(room1MembershipData, eventKind === "sticky");
|
||||
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||
client.emit(ClientEvent.Room, room1);
|
||||
expect(onStarted).not.toHaveBeenCalled();
|
||||
onStarted.mockClear();
|
||||
// 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 a call_id
|
||||
const room2MembershipData: MembershipData[] = [
|
||||
{ ...membershipTemplate, application: "m.notCall", call_id: "test" },
|
||||
];
|
||||
const room2 = makeMockRoom(room2MembershipData, eventKind === "sticky");
|
||||
jest.spyOn(client, "getRooms").mockReturnValue([room1, room2]);
|
||||
client.emit(ClientEvent.Room, room2);
|
||||
expect(onStarted).toHaveBeenCalled();
|
||||
onStarted.mockClear();
|
||||
// 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. Tracked.
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room2);
|
||||
sendLeaveMembership(room2, room2MembershipData);
|
||||
expect(onEnded).toHaveBeenCalled();
|
||||
onEnded.mockClear();
|
||||
// Stop room1's RTC session. Not tracked.
|
||||
vi.spyOn(client, "getRoom").mockReturnValue(room1);
|
||||
await sendLeaveMembership(room1, room1MembershipData);
|
||||
expect(onEnded).not.toHaveBeenCalled();
|
||||
|
||||
// Stop room1's RTC session. Not tracked.
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room1);
|
||||
sendLeaveMembership(room1, room1MembershipData);
|
||||
expect(onEnded).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
||||
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
|
||||
}
|
||||
// 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", () => {
|
||||
const onEnded = jest.fn();
|
||||
it("Doesn't fire event if unrelated sessions ends", async () => {
|
||||
const onEnded = vi.fn();
|
||||
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
|
||||
const membership: MembershipData[] = [{ ...membershipTemplate, application: "m.other_app" }];
|
||||
const membership: MembershipData[] = [generateMembership({ type: "m.other_app" })];
|
||||
const room1 = makeMockRoom(membership, eventKind === "sticky");
|
||||
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room1);
|
||||
vi.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||
vi.spyOn(client, "getRoom").mockReturnValue(room1);
|
||||
|
||||
client.emit(ClientEvent.Room, room1);
|
||||
|
||||
sendLeaveMembership(room1, membership);
|
||||
await sendLeaveMembership(room1, membership);
|
||||
|
||||
expect(onEnded).not.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");
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2025-2026 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type MockedFunction, type Mock } from "jest-mock";
|
||||
import { type Mock, type MockedFunction } from "vitest";
|
||||
|
||||
import {
|
||||
type EmptyObject,
|
||||
@@ -25,15 +25,16 @@ import {
|
||||
type Room,
|
||||
MAX_STICKY_DURATION_MS,
|
||||
} from "../../../src";
|
||||
import { MembershipManagerEvent, Status, type Transport, type LivekitFocusSelection } from "../../../src/matrixrtc";
|
||||
import {
|
||||
MembershipManagerEvent,
|
||||
Status,
|
||||
type Transport,
|
||||
type SessionMembershipData,
|
||||
type LivekitFocusSelection,
|
||||
} from "../../../src/matrixrtc";
|
||||
import { makeMockClient, makeMockRoom, membershipTemplate, mockCallMembership, type MockClient } from "./mocks";
|
||||
makeMockClient,
|
||||
makeMockRoom,
|
||||
sessionMembershipTemplate,
|
||||
mockCallMembership,
|
||||
type MockClient,
|
||||
} from "./mocks.ts";
|
||||
import { MembershipManager, StickyEventMembershipManager } from "../../../src/matrixrtc/MembershipManager.ts";
|
||||
import { type SessionMembershipData } from "../../../src/matrixrtc/membershipData/index.ts";
|
||||
|
||||
/**
|
||||
* Create a promise that will resolve once a mocked method is called.
|
||||
@@ -41,7 +42,7 @@ import { MembershipManager, StickyEventMembershipManager } from "../../../src/ma
|
||||
* @param returnVal Provide an optional value that the mocked method should return. (use Promise.resolve(val) or Promise.reject(err))
|
||||
* @returns The promise that resolves once the method is called.
|
||||
*/
|
||||
function waitForMockCall(method: MockedFunction<any>, returnVal?: Promise<any>): Promise<void> {
|
||||
function waitForMockCall(method: MockedFunction<(...args: any[]) => any>, returnVal?: Promise<any>): Promise<void> {
|
||||
const { promise, resolve } = Promise.withResolvers<void>();
|
||||
method.mockImplementation(() => {
|
||||
resolve();
|
||||
@@ -51,7 +52,7 @@ function waitForMockCall(method: MockedFunction<any>, returnVal?: Promise<any>):
|
||||
}
|
||||
|
||||
/** See waitForMockCall */
|
||||
function waitForMockCallOnce(method: MockedFunction<any>, returnVal?: Promise<any>) {
|
||||
function waitForMockCallOnce(method: MockedFunction<(...args: any[]) => any>, returnVal?: Promise<any>) {
|
||||
const { promise, resolve } = Promise.withResolvers<void>();
|
||||
method.mockImplementationOnce(() => {
|
||||
resolve();
|
||||
@@ -65,13 +66,13 @@ function waitForMockCallOnce(method: MockedFunction<any>, returnVal?: Promise<an
|
||||
* @param method The method to control the resolve timing.
|
||||
* @returns
|
||||
*/
|
||||
function createAsyncHandle<T>(method: MockedFunction<any>) {
|
||||
function createAsyncHandle<T>(method: MockedFunction<(...args: any[]) => any>) {
|
||||
const { reject, resolve, promise } = Promise.withResolvers<T>();
|
||||
method.mockImplementation(() => promise);
|
||||
return { reject, resolve };
|
||||
}
|
||||
|
||||
const callSession = { id: "", application: "m.call" };
|
||||
const callSession = { id: "ROOM", application: "m.call" };
|
||||
|
||||
describe("MembershipManager", () => {
|
||||
let client: MockClient;
|
||||
@@ -88,22 +89,22 @@ describe("MembershipManager", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
// Default to fake timers.
|
||||
jest.useFakeTimers();
|
||||
vi.useFakeTimers();
|
||||
client = makeMockClient("@alice:example.org", "AAAAAAA");
|
||||
room = makeMockRoom([membershipTemplate]);
|
||||
room = makeMockRoom([sessionMembershipTemplate]);
|
||||
// Provide a default mock that is like the default "non error" server behaviour.
|
||||
(client._unstable_sendDelayedStateEvent as Mock<any>).mockResolvedValue({ delay_id: "id" });
|
||||
(client._unstable_updateDelayedEvent as Mock<any>).mockResolvedValue(undefined);
|
||||
(client._unstable_cancelScheduledDelayedEvent as Mock<any>).mockResolvedValue(undefined);
|
||||
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockResolvedValue(undefined);
|
||||
(client._unstable_sendScheduledDelayedEvent as Mock<any>).mockResolvedValue(undefined);
|
||||
(client._unstable_sendStickyEvent as Mock<any>).mockResolvedValue({ event_id: "id" });
|
||||
(client._unstable_sendStickyDelayedEvent as Mock<any>).mockResolvedValue({ delay_id: "id" });
|
||||
(client.sendStateEvent as Mock<any>).mockResolvedValue({ event_id: "id" });
|
||||
vi.mocked(client._unstable_sendDelayedStateEvent).mockResolvedValue({ delay_id: "id" });
|
||||
vi.mocked(client._unstable_updateDelayedEvent).mockResolvedValue({});
|
||||
vi.mocked(client._unstable_cancelScheduledDelayedEvent).mockResolvedValue({});
|
||||
vi.mocked(client._unstable_restartScheduledDelayedEvent).mockResolvedValue({});
|
||||
vi.mocked(client._unstable_sendScheduledDelayedEvent).mockResolvedValue({});
|
||||
vi.mocked(client._unstable_sendStickyEvent).mockResolvedValue({ event_id: "id" });
|
||||
vi.mocked(client._unstable_sendStickyDelayedEvent).mockResolvedValue({ delay_id: "id" });
|
||||
vi.mocked(client.sendStateEvent).mockResolvedValue({ event_id: "id" });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
vi.useRealTimers();
|
||||
// There is no need to clean up mocks since we will recreate the client.
|
||||
});
|
||||
|
||||
@@ -126,7 +127,7 @@ describe("MembershipManager", () => {
|
||||
// Spys/Mocks
|
||||
|
||||
const restartScheduledDelayedEventHandle = createAsyncHandle<void>(
|
||||
client._unstable_restartScheduledDelayedEvent as Mock,
|
||||
client._unstable_restartScheduledDelayedEvent,
|
||||
);
|
||||
|
||||
// Test
|
||||
@@ -139,13 +140,16 @@ describe("MembershipManager", () => {
|
||||
"org.matrix.msc3401.call.member",
|
||||
{
|
||||
application: "m.call",
|
||||
// This tests INFO_SLOT_ID_LEGACY_CASE because it is using callSession = { id: "ROOM", application: "m.call" }
|
||||
call_id: "",
|
||||
device_id: "AAAAAAA",
|
||||
expires: 14400000,
|
||||
foci_preferred: [focus],
|
||||
membershipID: "@alice:example.org:AAAAAAA",
|
||||
focus_active: focusActive,
|
||||
scope: "m.room",
|
||||
},
|
||||
// This tests INFO_SLOT_ID_LEGACY_CASE because it is using callSession = { id: "ROOM", application: "m.call" }
|
||||
"_@alice:example.org_AAAAAAA_m.call",
|
||||
);
|
||||
restartScheduledDelayedEventHandle.resolve?.();
|
||||
@@ -159,6 +163,45 @@ describe("MembershipManager", () => {
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("sends correct call_id and state key when using non empty string. Not using empty string -> ROOM hack. See: INFO_SLOT_ID_LEGACY_CASE", async () => {
|
||||
// Spys/Mocks
|
||||
|
||||
const customCallSession = { id: "custom", application: "m.call" };
|
||||
const restartScheduledDelayedEventHandle = createAsyncHandle<void>(
|
||||
client._unstable_restartScheduledDelayedEvent,
|
||||
);
|
||||
|
||||
// Test
|
||||
const memberManager = new MembershipManager(undefined, room, client, customCallSession);
|
||||
memberManager.join([focus], undefined);
|
||||
// expects
|
||||
await waitForMockCall(client.sendStateEvent, Promise.resolve({ event_id: "id" }));
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||
room.roomId,
|
||||
"org.matrix.msc3401.call.member",
|
||||
{
|
||||
application: "m.call",
|
||||
call_id: "custom",
|
||||
device_id: "AAAAAAA",
|
||||
expires: 14400000,
|
||||
foci_preferred: [focus],
|
||||
membershipID: "@alice:example.org:AAAAAAA",
|
||||
focus_active: focusActive,
|
||||
scope: "m.room",
|
||||
},
|
||||
"_@alice:example.org_AAAAAAA_m.callcustom",
|
||||
);
|
||||
restartScheduledDelayedEventHandle.resolve?.();
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledWith(
|
||||
room.roomId,
|
||||
{ delay: 8000 },
|
||||
"org.matrix.msc3401.call.member",
|
||||
{},
|
||||
"_@alice:example.org_AAAAAAA_m.callcustom",
|
||||
);
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("reschedules delayed leave event if sending state cancels it", async () => {
|
||||
const memberManager = new MembershipManager(undefined, room, client, callSession);
|
||||
const waitForSendState = waitForMockCall(client.sendStateEvent);
|
||||
@@ -169,7 +212,7 @@ describe("MembershipManager", () => {
|
||||
memberManager.join([focus], focusActive);
|
||||
await waitForSendState;
|
||||
await waitForRestartScheduledDelayedEvent;
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
// Once for the initial event and once because of the errcode: "M_NOT_FOUND"
|
||||
// Different to "sends a membership event and schedules delayed leave when joining a call" where its only called once (1)
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(2);
|
||||
@@ -182,7 +225,7 @@ describe("MembershipManager", () => {
|
||||
// - run into rate limit for sending delayed event
|
||||
// - run into rate limit when setting membership state.
|
||||
if (useOwnedStateEvents) {
|
||||
room.getVersion = jest.fn().mockReturnValue("org.matrix.msc3757.default");
|
||||
room.getVersion = vi.fn().mockReturnValue("org.matrix.msc3757.default");
|
||||
}
|
||||
const restartScheduledDelayedEvent = waitForMockCall(client._unstable_restartScheduledDelayedEvent);
|
||||
const sentDelayedState = waitForMockCall(
|
||||
@@ -199,7 +242,7 @@ describe("MembershipManager", () => {
|
||||
"org.matrix.msc4140.errcode": "M_MAX_DELAY_EXCEEDED",
|
||||
"org.matrix.msc4140.max_delay": 7500,
|
||||
});
|
||||
(client._unstable_sendDelayedStateEvent as Mock).mockImplementationOnce(() => {
|
||||
vi.mocked(client._unstable_sendDelayedStateEvent).mockImplementationOnce(() => {
|
||||
resolve();
|
||||
return Promise.reject(error);
|
||||
});
|
||||
@@ -209,7 +252,7 @@ describe("MembershipManager", () => {
|
||||
// preparing the delayed disconnect should handle ratelimiting
|
||||
const sendDelayedStateAttempt = new Promise<void>((resolve) => {
|
||||
const error = new MatrixError({ errcode: "M_LIMIT_EXCEEDED" });
|
||||
(client._unstable_sendDelayedStateEvent as Mock).mockImplementationOnce(() => {
|
||||
vi.mocked(client._unstable_sendDelayedStateEvent).mockImplementationOnce(() => {
|
||||
resolve();
|
||||
return Promise.reject(error);
|
||||
});
|
||||
@@ -224,7 +267,7 @@ describe("MembershipManager", () => {
|
||||
undefined,
|
||||
new Headers({ "Retry-After": "1" }),
|
||||
);
|
||||
(client.sendStateEvent as Mock).mockImplementationOnce(() => {
|
||||
vi.mocked(client.sendStateEvent).mockImplementationOnce(() => {
|
||||
resolve();
|
||||
return Promise.reject(error);
|
||||
});
|
||||
@@ -247,11 +290,11 @@ describe("MembershipManager", () => {
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenNthCalledWith(1, ...callProps(9000));
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenNthCalledWith(2, ...callProps(7500));
|
||||
|
||||
await jest.advanceTimersByTimeAsync(5000);
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
|
||||
await sendStateEventAttempt.then(); // needed to resolve after resendIfRateLimited catches
|
||||
|
||||
await jest.advanceTimersByTimeAsync(1000);
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||
room!.roomId,
|
||||
@@ -263,6 +306,7 @@ describe("MembershipManager", () => {
|
||||
expires: 14400000,
|
||||
device_id: "AAAAAAA",
|
||||
foci_preferred: [focus],
|
||||
membershipID: "@alice:example.org:AAAAAAA",
|
||||
focus_active: focusActive,
|
||||
} satisfies SessionMembershipData,
|
||||
userStateKey,
|
||||
@@ -274,15 +318,17 @@ describe("MembershipManager", () => {
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(1);
|
||||
|
||||
// ensures that we reach the code that schedules the timeout for the next delay update before we advance the timers.
|
||||
await jest.advanceTimersByTimeAsync(5000);
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
// should update delayed disconnect
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(2);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("sends a membership event after rate limits during delayed event setup when joining a call", async () => {
|
||||
await testJoin(false);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("does not prefix the state key with _ for rooms that support user-owned state events", async () => {
|
||||
await testJoin(true);
|
||||
});
|
||||
@@ -291,7 +337,7 @@ describe("MembershipManager", () => {
|
||||
|
||||
describe("delayed leave event", () => {
|
||||
it("does not try again to schedule a delayed leave event if not supported", () => {
|
||||
const delayedHandle = createAsyncHandle(client._unstable_sendDelayedStateEvent as Mock);
|
||||
const delayedHandle = createAsyncHandle(client._unstable_sendDelayedStateEvent);
|
||||
const manager = new MembershipManager({}, room, client, callSession);
|
||||
manager.join([focus]);
|
||||
delayedHandle.reject?.(
|
||||
@@ -303,11 +349,11 @@ describe("MembershipManager", () => {
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it("does try to schedule a delayed leave event again if rate limited", async () => {
|
||||
const delayedHandle = createAsyncHandle(client._unstable_sendDelayedStateEvent as Mock);
|
||||
const delayedHandle = createAsyncHandle(client._unstable_sendDelayedStateEvent);
|
||||
const manager = new MembershipManager({}, room, client, callSession);
|
||||
manager.join([focus]);
|
||||
delayedHandle.reject?.(new HTTPError("rate limited", 429, undefined));
|
||||
await jest.advanceTimersByTimeAsync(5000);
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
it("uses delayedLeaveEventDelayMs from config", () => {
|
||||
@@ -336,7 +382,7 @@ describe("MembershipManager", () => {
|
||||
manager.join([focus]);
|
||||
expect(manager.status).toBe(Status.Connecting);
|
||||
// Let the scheduler run one iteration so that we can send the join state event
|
||||
await jest.runOnlyPendingTimersAsync();
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
|
||||
expect(manager.status).toBe(Status.Connected);
|
||||
// Now that we are connected, we set up the mocks.
|
||||
@@ -353,12 +399,12 @@ describe("MembershipManager", () => {
|
||||
);
|
||||
|
||||
const { resolve } = createAsyncHandle(client._unstable_sendDelayedStateEvent);
|
||||
await jest.advanceTimersByTimeAsync(RESTART_DELAY);
|
||||
await vi.advanceTimersByTimeAsync(RESTART_DELAY);
|
||||
// first simulate the sync, then resolve sending the delayed event.
|
||||
await manager.onRTCSessionMemberUpdate([mockCallMembership(membershipTemplate, room.roomId)]);
|
||||
await manager.onRTCSessionMemberUpdate([mockCallMembership(sessionMembershipTemplate, room.roomId)]);
|
||||
resolve({ delay_id: "id" });
|
||||
// Let the scheduler run one iteration so that the new join gets sent
|
||||
await jest.runOnlyPendingTimersAsync();
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
expect(client.sendStateEvent).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
@@ -383,6 +429,7 @@ describe("MembershipManager", () => {
|
||||
device_id: "AAAAAAA",
|
||||
expires: 1234567,
|
||||
foci_preferred: [focus],
|
||||
membershipID: "@alice:example.org:AAAAAAA",
|
||||
focus_active: {
|
||||
focus_selection: "oldest_membership",
|
||||
type: "livekit",
|
||||
@@ -407,15 +454,16 @@ describe("MembershipManager", () => {
|
||||
it("resolves delayed leave event when leave is called", async () => {
|
||||
const manager = new MembershipManager({}, room, client, callSession);
|
||||
manager.join([focus]);
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
await manager.leave();
|
||||
expect(client._unstable_sendScheduledDelayedEvent).toHaveBeenLastCalledWith("id");
|
||||
expect(client.sendStateEvent).toHaveBeenCalled();
|
||||
expect(manager.delayId).toBe(undefined);
|
||||
});
|
||||
it("send leave event when leave is called and resolving delayed leave fails", async () => {
|
||||
it("send leave event when leave is called and resolving delayed leave fails unknown error", async () => {
|
||||
const manager = new MembershipManager({}, room, client, callSession);
|
||||
manager.join([focus]);
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
(client._unstable_sendScheduledDelayedEvent as Mock<any>).mockRejectedValue("unknown");
|
||||
await manager.leave();
|
||||
|
||||
@@ -426,10 +474,31 @@ describe("MembershipManager", () => {
|
||||
{},
|
||||
"_@alice:example.org_AAAAAAA_m.call",
|
||||
);
|
||||
// If there is a unknown error, we do not reset the delayId
|
||||
// The delayed event might still be around and we track it.
|
||||
expect(manager.delayId).not.toBe(undefined);
|
||||
});
|
||||
it("does nothing if not joined", () => {
|
||||
it("send leave event when leave is called and resolving delayed leave fails not found error", async () => {
|
||||
const manager = new MembershipManager({}, room, client, callSession);
|
||||
expect(async () => await manager.leave()).not.toThrow();
|
||||
manager.join([focus]);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
(client._unstable_sendScheduledDelayedEvent as Mock<any>).mockRejectedValue(
|
||||
new MatrixError({ errcode: "M_NOT_FOUND" }, 404),
|
||||
);
|
||||
await manager.leave();
|
||||
|
||||
// We send a normal leave event since we failed using sendScheduledDelayedEvent.
|
||||
expect(client.sendStateEvent).toHaveBeenLastCalledWith(
|
||||
room.roomId,
|
||||
"org.matrix.msc3401.call.member",
|
||||
{},
|
||||
"_@alice:example.org_AAAAAAA_m.call",
|
||||
);
|
||||
expect(manager.delayId).toBe(undefined);
|
||||
});
|
||||
it("does nothing if not joined", async () => {
|
||||
const manager = new MembershipManager({}, room, client, callSession);
|
||||
await expect(manager.leave()).resolves.toBeTruthy();
|
||||
expect(client._unstable_sendDelayedStateEvent).not.toHaveBeenCalled();
|
||||
expect(client.sendStateEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -438,8 +507,8 @@ describe("MembershipManager", () => {
|
||||
describe("onRTCSessionMemberUpdate()", () => {
|
||||
it("does nothing if not joined", async () => {
|
||||
const manager = new MembershipManager({}, room, client, callSession);
|
||||
await manager.onRTCSessionMemberUpdate([mockCallMembership(membershipTemplate, room.roomId)]);
|
||||
await jest.advanceTimersToNextTimerAsync();
|
||||
await manager.onRTCSessionMemberUpdate([mockCallMembership(sessionMembershipTemplate, room.roomId)]);
|
||||
await vi.advanceTimersToNextTimerAsync();
|
||||
expect(client.sendStateEvent).not.toHaveBeenCalled();
|
||||
expect(client._unstable_sendDelayedStateEvent).not.toHaveBeenCalled();
|
||||
expect(client._unstable_updateDelayedEvent).not.toHaveBeenCalled();
|
||||
@@ -450,25 +519,25 @@ describe("MembershipManager", () => {
|
||||
it("does nothing if own membership still present", async () => {
|
||||
const manager = new MembershipManager({}, room, client, callSession);
|
||||
manager.join([focus], focusActive);
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
const myMembership = (client.sendStateEvent as Mock).mock.calls[0][2];
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
const myMembership = vi.mocked(client.sendStateEvent).mock.calls[0][2];
|
||||
// reset all mocks before checking what happens when calling: `onRTCSessionMemberUpdate`
|
||||
(client.sendStateEvent as Mock).mockClear();
|
||||
(client._unstable_updateDelayedEvent as Mock).mockClear();
|
||||
(client._unstable_cancelScheduledDelayedEvent as Mock).mockClear();
|
||||
(client._unstable_restartScheduledDelayedEvent as Mock).mockClear();
|
||||
(client._unstable_sendScheduledDelayedEvent as Mock).mockClear();
|
||||
(client._unstable_sendDelayedStateEvent as Mock).mockClear();
|
||||
vi.mocked(client.sendStateEvent).mockClear();
|
||||
vi.mocked(client._unstable_updateDelayedEvent).mockClear();
|
||||
vi.mocked(client._unstable_cancelScheduledDelayedEvent).mockClear();
|
||||
vi.mocked(client._unstable_restartScheduledDelayedEvent).mockClear();
|
||||
vi.mocked(client._unstable_sendScheduledDelayedEvent).mockClear();
|
||||
vi.mocked(client._unstable_sendDelayedStateEvent).mockClear();
|
||||
|
||||
await manager.onRTCSessionMemberUpdate([
|
||||
mockCallMembership(membershipTemplate, room.roomId),
|
||||
mockCallMembership(sessionMembershipTemplate, room.roomId),
|
||||
mockCallMembership(
|
||||
{ ...(myMembership as SessionMembershipData), user_id: client.getUserId()! },
|
||||
room.roomId,
|
||||
),
|
||||
]);
|
||||
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
|
||||
expect(client.sendStateEvent).not.toHaveBeenCalled();
|
||||
expect(client._unstable_sendDelayedStateEvent).not.toHaveBeenCalled();
|
||||
@@ -480,15 +549,15 @@ describe("MembershipManager", () => {
|
||||
it("recreates membership if it is missing", async () => {
|
||||
const manager = new MembershipManager({}, room, client, callSession);
|
||||
manager.join([focus], focusActive);
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
// clearing all mocks before checking what happens when calling: `onRTCSessionMemberUpdate`
|
||||
(client.sendStateEvent as Mock).mockClear();
|
||||
(client._unstable_restartScheduledDelayedEvent as Mock).mockClear();
|
||||
(client._unstable_sendDelayedStateEvent as Mock).mockClear();
|
||||
vi.mocked(client.sendStateEvent).mockClear();
|
||||
vi.mocked(client._unstable_restartScheduledDelayedEvent).mockClear();
|
||||
vi.mocked(client._unstable_sendDelayedStateEvent).mockClear();
|
||||
|
||||
// Our own membership is removed:
|
||||
await manager.onRTCSessionMemberUpdate([mockCallMembership(membershipTemplate, room.roomId)]);
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
await manager.onRTCSessionMemberUpdate([mockCallMembership(sessionMembershipTemplate, room.roomId)]);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(client.sendStateEvent).toHaveBeenCalled();
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalled();
|
||||
|
||||
@@ -498,21 +567,21 @@ describe("MembershipManager", () => {
|
||||
it("updates the UpdateExpiry entry in the action scheduler", async () => {
|
||||
const manager = new MembershipManager({}, room, client, callSession);
|
||||
manager.join([focus], focusActive);
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
// clearing all mocks before checking what happens when calling: `onRTCSessionMemberUpdate`
|
||||
(client.sendStateEvent as Mock).mockClear();
|
||||
(client._unstable_restartScheduledDelayedEvent as Mock).mockClear();
|
||||
(client._unstable_sendDelayedStateEvent as Mock).mockClear();
|
||||
vi.mocked(client.sendStateEvent).mockClear();
|
||||
vi.mocked(client._unstable_restartScheduledDelayedEvent).mockClear();
|
||||
vi.mocked(client._unstable_sendDelayedStateEvent).mockClear();
|
||||
|
||||
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockRejectedValueOnce(
|
||||
new MatrixError({ errcode: "M_NOT_FOUND" }),
|
||||
);
|
||||
|
||||
const { resolve } = createAsyncHandle(client._unstable_sendDelayedStateEvent);
|
||||
await jest.advanceTimersByTimeAsync(10_000);
|
||||
await manager.onRTCSessionMemberUpdate([mockCallMembership(membershipTemplate, room.roomId)]);
|
||||
await vi.advanceTimersByTimeAsync(10_000);
|
||||
await manager.onRTCSessionMemberUpdate([mockCallMembership(sessionMembershipTemplate, room.roomId)]);
|
||||
resolve({ delay_id: "id" });
|
||||
await jest.advanceTimersByTimeAsync(10_000);
|
||||
await vi.advanceTimersByTimeAsync(10_000);
|
||||
|
||||
expect(client.sendStateEvent).toHaveBeenCalled();
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalled();
|
||||
@@ -532,7 +601,7 @@ describe("MembershipManager", () => {
|
||||
{ id: "", application: "m.call" },
|
||||
);
|
||||
manager.join([focus], focusActive);
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
|
||||
|
||||
// The first call is from checking id the server deleted the delayed event
|
||||
@@ -543,7 +612,7 @@ describe("MembershipManager", () => {
|
||||
|
||||
for (let i = 2; i <= 12; i++) {
|
||||
// flush promises before advancing the timers to make sure schedulers are setup
|
||||
await jest.advanceTimersByTimeAsync(10_000);
|
||||
await vi.advanceTimersByTimeAsync(10_000);
|
||||
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(i);
|
||||
// TODO: Check that update delayed event is called with the correct HTTP request timeout
|
||||
@@ -566,18 +635,20 @@ describe("MembershipManager", () => {
|
||||
manager.join([focus], focusActive);
|
||||
await waitForMockCall(client.sendStateEvent);
|
||||
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
|
||||
const sentMembership = (client.sendStateEvent as Mock).mock.calls[0][2] as SessionMembershipData;
|
||||
const sentMembership = vi.mocked(client.sendStateEvent).mock.calls[0][2] as SessionMembershipData;
|
||||
expect(sentMembership.expires).toBe(expire);
|
||||
for (let i = 2; i <= 12; i++) {
|
||||
await jest.advanceTimersByTimeAsync(expire);
|
||||
await vi.advanceTimersByTimeAsync(expire);
|
||||
expect(client.sendStateEvent).toHaveBeenCalledTimes(i);
|
||||
const sentMembership = (client.sendStateEvent as Mock).mock.lastCall![2] as SessionMembershipData;
|
||||
const sentMembership = vi.mocked(client.sendStateEvent).mock.lastCall![2] as SessionMembershipData;
|
||||
expect(sentMembership.expires).toBe(expire * i);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("extends `expires` when call still active", async () => {
|
||||
await testExpires(10_000);
|
||||
});
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("extends `expires` using headroom configuration", async () => {
|
||||
await testExpires(10_000, 1_000);
|
||||
});
|
||||
@@ -594,23 +665,23 @@ describe("MembershipManager", () => {
|
||||
|
||||
const manager = new MembershipManager({}, room, client, callSession);
|
||||
expect(manager.status).toBe(Status.Disconnected);
|
||||
const connectEmit = jest.fn();
|
||||
const connectEmit = vi.fn();
|
||||
manager.on(MembershipManagerEvent.StatusChanged, connectEmit);
|
||||
manager.join([focus], focusActive);
|
||||
expect(manager.status).toBe(Status.Connecting);
|
||||
handleDelayedEvent.resolve();
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(connectEmit).toHaveBeenCalledWith(Status.Disconnected, Status.Connecting);
|
||||
handleStateEvent.resolve();
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(connectEmit).toHaveBeenCalledWith(Status.Connecting, Status.Connected);
|
||||
});
|
||||
it("emits 'Disconnecting' and 'Disconnected' after leave", async () => {
|
||||
const manager = new MembershipManager({}, room, client, callSession);
|
||||
const connectEmit = jest.fn();
|
||||
const connectEmit = vi.fn();
|
||||
manager.on(MembershipManagerEvent.StatusChanged, connectEmit);
|
||||
manager.join([focus], focusActive);
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
await manager.leave();
|
||||
expect(connectEmit).toHaveBeenCalledWith(Status.Connected, Status.Disconnecting);
|
||||
expect(connectEmit).toHaveBeenCalledWith(Status.Disconnecting, Status.Disconnected);
|
||||
@@ -635,7 +706,7 @@ describe("MembershipManager", () => {
|
||||
new Headers({ "Retry-After": "1" }),
|
||||
),
|
||||
);
|
||||
await jest.advanceTimersByTimeAsync(1000);
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
@@ -653,7 +724,7 @@ describe("MembershipManager", () => {
|
||||
// Should call _unstable_sendDelayedStateEvent but not sendStateEvent because of the
|
||||
// RateLimit error.
|
||||
manager.join([focus], focusActive);
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
|
||||
(client._unstable_sendDelayedStateEvent as Mock<any>).mockResolvedValue({ delay_id: "id" });
|
||||
@@ -661,7 +732,7 @@ describe("MembershipManager", () => {
|
||||
// the membership is no longer present on the homeserver
|
||||
await manager.onRTCSessionMemberUpdate([]);
|
||||
// Wait for all timers to be setup
|
||||
await jest.advanceTimersByTimeAsync(1000);
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
// We should send the first own membership and a new delayed event after the rate limit timeout.
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(2);
|
||||
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
|
||||
@@ -681,13 +752,13 @@ describe("MembershipManager", () => {
|
||||
),
|
||||
);
|
||||
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
|
||||
// the user terminated the call locally
|
||||
await manager.leave();
|
||||
|
||||
// Wait for all timers to be setup
|
||||
await jest.advanceTimersByTimeAsync(1000);
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
|
||||
// No new events should have been sent:
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
|
||||
@@ -708,16 +779,16 @@ describe("MembershipManager", () => {
|
||||
manager.join([focus], focusActive);
|
||||
|
||||
// Hit rate limit
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Hit second rate limit.
|
||||
await jest.advanceTimersByTimeAsync(1000);
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Setup resolve
|
||||
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockResolvedValue(undefined);
|
||||
await jest.advanceTimersByTimeAsync(1000);
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(3);
|
||||
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
|
||||
@@ -727,7 +798,7 @@ describe("MembershipManager", () => {
|
||||
describe("unrecoverable errors", () => {
|
||||
// because legacy does not have a retry limit and no mechanism to communicate unrecoverable errors.
|
||||
it("throws, when reaching maximum number of retries for initial delayed event creation", async () => {
|
||||
const delayEventSendError = jest.fn();
|
||||
const delayEventSendError = vi.fn();
|
||||
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(
|
||||
new MatrixError(
|
||||
{ errcode: "M_LIMIT_EXCEEDED" },
|
||||
@@ -741,13 +812,13 @@ describe("MembershipManager", () => {
|
||||
manager.join([focus], focusActive, delayEventSendError);
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await jest.advanceTimersByTimeAsync(2000);
|
||||
await vi.advanceTimersByTimeAsync(2000);
|
||||
}
|
||||
expect(delayEventSendError).toHaveBeenCalled();
|
||||
});
|
||||
// because legacy does not have a retry limit and no mechanism to communicate unrecoverable errors.
|
||||
it("throws, when reaching maximum number of retries", async () => {
|
||||
const delayEventRestartError = jest.fn();
|
||||
const delayEventRestartError = vi.fn();
|
||||
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockRejectedValue(
|
||||
new MatrixError(
|
||||
{ errcode: "M_LIMIT_EXCEEDED" },
|
||||
@@ -761,12 +832,12 @@ describe("MembershipManager", () => {
|
||||
manager.join([focus], focusActive, delayEventRestartError);
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await jest.advanceTimersByTimeAsync(1000);
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
}
|
||||
expect(delayEventRestartError).toHaveBeenCalled();
|
||||
});
|
||||
it("falls back to using pure state events when some error occurs while sending delayed events", async () => {
|
||||
const unrecoverableError = jest.fn();
|
||||
const unrecoverableError = vi.fn();
|
||||
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(new HTTPError("unknown", 601));
|
||||
const manager = new MembershipManager({}, room, client, callSession);
|
||||
manager.join([focus], focusActive, unrecoverableError);
|
||||
@@ -775,7 +846,7 @@ describe("MembershipManager", () => {
|
||||
expect(client.sendStateEvent).toHaveBeenCalled();
|
||||
});
|
||||
it("retries before failing in case its a network error", async () => {
|
||||
const unrecoverableError = jest.fn();
|
||||
const unrecoverableError = vi.fn();
|
||||
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(new HTTPError("unknown", 501));
|
||||
const manager = new MembershipManager(
|
||||
{ networkErrorRetryMs: 1000, maximumNetworkErrorRetryCount: 7 },
|
||||
@@ -786,7 +857,7 @@ describe("MembershipManager", () => {
|
||||
manager.join([focus], focusActive, unrecoverableError);
|
||||
for (let retries = 0; retries < 7; retries++) {
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(retries + 1);
|
||||
await jest.advanceTimersByTimeAsync(1000);
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
}
|
||||
expect(unrecoverableError).toHaveBeenCalled();
|
||||
expect(unrecoverableError.mock.lastCall![0].message).toMatch(
|
||||
@@ -795,13 +866,13 @@ describe("MembershipManager", () => {
|
||||
expect(client.sendStateEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
it("falls back to using pure state events when UnsupportedDelayedEventsEndpointError encountered for delayed events", async () => {
|
||||
const unrecoverableError = jest.fn();
|
||||
const unrecoverableError = vi.fn();
|
||||
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(
|
||||
new UnsupportedDelayedEventsEndpointError("not supported", "sendDelayedStateEvent"),
|
||||
);
|
||||
const manager = new MembershipManager({}, room, client, callSession);
|
||||
manager.join([focus], focusActive, unrecoverableError);
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
|
||||
expect(unrecoverableError).not.toHaveBeenCalled();
|
||||
expect(client.sendStateEvent).toHaveBeenCalled();
|
||||
@@ -817,7 +888,7 @@ describe("MembershipManager", () => {
|
||||
callSession,
|
||||
);
|
||||
const { promise: stuckPromise, reject: rejectStuckPromise } = Promise.withResolvers<EmptyObject>();
|
||||
const probablyLeftEmit = jest.fn();
|
||||
const probablyLeftEmit = vi.fn();
|
||||
manager.on(MembershipManagerEvent.ProbablyLeft, probablyLeftEmit);
|
||||
manager.join([focus], focusActive);
|
||||
try {
|
||||
@@ -826,19 +897,19 @@ describe("MembershipManager", () => {
|
||||
|
||||
// We never resolve the delayed event so that we can test the probablyLeft event.
|
||||
// This simulates the case where the server does not respond to the delayed event.
|
||||
client._unstable_restartScheduledDelayedEvent = jest.fn(() => stuckPromise);
|
||||
client._unstable_restartScheduledDelayedEvent = vi.fn((_) => stuckPromise);
|
||||
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
|
||||
expect(manager.status).toBe(Status.Connected);
|
||||
expect(probablyLeftEmit).not.toHaveBeenCalledWith(true);
|
||||
// We expect the probablyLeft event to be emitted after the `delayedLeaveEventDelayMs` = 10000.
|
||||
// We also track the calls to updated the delayed event that all will never resolve to simulate the server not responding.
|
||||
// The numbers are a bit arbitrary since we use the local timeout that does not perfectly match the 5s check interval in this test.
|
||||
await jest.advanceTimersByTimeAsync(5000);
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
// No emission after 5s
|
||||
expect(probablyLeftEmit).not.toHaveBeenCalledWith(true);
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(1);
|
||||
|
||||
await jest.advanceTimersByTimeAsync(4999);
|
||||
await vi.advanceTimersByTimeAsync(4999);
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(3);
|
||||
expect(probablyLeftEmit).not.toHaveBeenCalledWith(true);
|
||||
|
||||
@@ -846,14 +917,14 @@ describe("MembershipManager", () => {
|
||||
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockResolvedValue({});
|
||||
|
||||
// Emit after 10s
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(4);
|
||||
expect(probablyLeftEmit).toHaveBeenCalledWith(true);
|
||||
|
||||
// Mock a sync which does not include our own membership
|
||||
await manager.onRTCSessionMemberUpdate([]);
|
||||
// Wait for the current ongoing delayed event sending to finish
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
// We should send a new state event and an associated delayed leave event.
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(2);
|
||||
expect(client.sendStateEvent).toHaveBeenCalledTimes(2);
|
||||
@@ -866,6 +937,7 @@ describe("MembershipManager", () => {
|
||||
});
|
||||
|
||||
describe("updateCallIntent()", () => {
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should fail if the user has not joined the call", async () => {
|
||||
const manager = new MembershipManager({}, room, client, callSession);
|
||||
// After joining we want our own focus to be the one we select.
|
||||
@@ -879,11 +951,14 @@ describe("MembershipManager", () => {
|
||||
const manager = new MembershipManager({}, room, client, callSession);
|
||||
manager.join([]);
|
||||
expect(manager.isActivated()).toEqual(true);
|
||||
const membership = mockCallMembership({ ...membershipTemplate, user_id: client.getUserId()! }, room.roomId);
|
||||
const membership = mockCallMembership(
|
||||
{ ...sessionMembershipTemplate, user_id: client.getUserId()! },
|
||||
room.roomId,
|
||||
);
|
||||
await manager.onRTCSessionMemberUpdate([membership]);
|
||||
await manager.updateCallIntent("video");
|
||||
expect(client.sendStateEvent).toHaveBeenCalledTimes(2);
|
||||
const eventContent = (client.sendStateEvent as Mock).mock.calls[0][2] as SessionMembershipData;
|
||||
const eventContent = vi.mocked(client.sendStateEvent).mock.calls[0][2] as SessionMembershipData;
|
||||
expect(eventContent["created_ts"]).toEqual(membership.createdTs());
|
||||
expect(eventContent["m.call.intent"]).toEqual("video");
|
||||
});
|
||||
@@ -893,7 +968,7 @@ describe("MembershipManager", () => {
|
||||
manager.join([]);
|
||||
expect(manager.isActivated()).toEqual(true);
|
||||
const membership = mockCallMembership(
|
||||
{ ...membershipTemplate, "user_id": client.getUserId()!, "m.call.intent": "video" },
|
||||
{ ...sessionMembershipTemplate, "user_id": client.getUserId()!, "m.call.intent": "video" },
|
||||
room.roomId,
|
||||
);
|
||||
await manager.onRTCSessionMemberUpdate([membership]);
|
||||
@@ -913,9 +988,15 @@ describe("MembershipManager", () => {
|
||||
describe("sends an rtc membership event", () => {
|
||||
it("sends a membership event and schedules delayed leave when joining a call", async () => {
|
||||
const restartScheduledDelayedEventHandle = createAsyncHandle<void>(
|
||||
client._unstable_restartScheduledDelayedEvent as Mock,
|
||||
client._unstable_restartScheduledDelayedEvent,
|
||||
);
|
||||
const memberManager = new StickyEventMembershipManager(
|
||||
undefined,
|
||||
room,
|
||||
client,
|
||||
callSession,
|
||||
"@alice:example.org:AAAAAAA_m.call",
|
||||
);
|
||||
const memberManager = new StickyEventMembershipManager(undefined, room, client, callSession);
|
||||
|
||||
memberManager.join([], focus);
|
||||
|
||||
@@ -930,13 +1011,13 @@ describe("MembershipManager", () => {
|
||||
application: { type: "m.call" },
|
||||
member: {
|
||||
user_id: "@alice:example.org",
|
||||
id: "_@alice:example.org_AAAAAAA_m.call",
|
||||
id: "@alice:example.org:AAAAAAA_m.call",
|
||||
device_id: "AAAAAAA",
|
||||
},
|
||||
slot_id: "m.call#",
|
||||
rtc_transports: [focus],
|
||||
slot_id: "m.call#ROOM",
|
||||
rtc_transports: [{ type: focus.type, livekit_service_url: focus.livekit_service_url }],
|
||||
versions: [],
|
||||
msc4354_sticky_key: "_@alice:example.org_AAAAAAA_m.call",
|
||||
msc4354_sticky_key: "@alice:example.org:AAAAAAA_m.call",
|
||||
},
|
||||
);
|
||||
restartScheduledDelayedEventHandle.resolve?.();
|
||||
@@ -949,7 +1030,7 @@ describe("MembershipManager", () => {
|
||||
null,
|
||||
"org.matrix.msc4143.rtc.member",
|
||||
{
|
||||
msc4354_sticky_key: "_@alice:example.org_AAAAAAA_m.call",
|
||||
msc4354_sticky_key: "@alice:example.org:AAAAAAA_m.call",
|
||||
},
|
||||
);
|
||||
// ..once
|
||||
@@ -962,11 +1043,11 @@ describe("MembershipManager", () => {
|
||||
|
||||
it("Should prefix log with MembershipManager used", () => {
|
||||
const client = makeMockClient("@alice:example.org", "AAAAAAA");
|
||||
const room = makeMockRoom([membershipTemplate]);
|
||||
const room = makeMockRoom([sessionMembershipTemplate]);
|
||||
|
||||
const membershipManager = new MembershipManager(undefined, room, client, callSession);
|
||||
|
||||
const spy = jest.spyOn(console, "error");
|
||||
const spy = vi.spyOn(console, "error");
|
||||
// Double join
|
||||
membershipManager.join([]);
|
||||
membershipManager.join([]);
|
||||
|
||||
@@ -25,9 +25,9 @@ describe("OutdatedKeyFilter Test", () => {
|
||||
const olderKey = fakeInboundSessionWithTimestamp(300);
|
||||
// Simulate receiving out of order keys
|
||||
|
||||
expect(filter.isOutdated(aKey.participantId, aKey)).toBe(false);
|
||||
expect(filter.isOutdated(aKey.membership, aKey)).toBe(false);
|
||||
// Then we receive the most recent key out of order
|
||||
const isOutdated = filter.isOutdated(aKey.participantId, olderKey);
|
||||
const isOutdated = filter.isOutdated(aKey.membership, olderKey);
|
||||
// this key is older and should be ignored even if received after
|
||||
expect(isOutdated).toBe(true);
|
||||
});
|
||||
@@ -36,7 +36,7 @@ describe("OutdatedKeyFilter Test", () => {
|
||||
return {
|
||||
keyIndex: 0,
|
||||
creationTS: ts,
|
||||
participantId: "@alice:localhost|ABCDE",
|
||||
membership: { userId: "@alice:localhost", deviceId: "ABDE", memberId: "@alice:localhost:ABCDE" },
|
||||
key: new Uint8Array(16),
|
||||
};
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user