Compare commits
115 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d428e7119a | |||
| 5532066178 | |||
| bb7a689448 | |||
| c2b464a72c | |||
| 4a75d2c92f | |||
| 899cdb0e1d | |||
| da7c6717fe | |||
| b3fedf3a4e | |||
| 3d0ebdf6f1 | |||
| 25555ec431 | |||
| 33cd424f1f | |||
| 4d0d32307e | |||
| b1a578f62e | |||
| 841b654c00 | |||
| ca0d4622b3 | |||
| bfd87a0896 | |||
| 6c59b0c22f | |||
| eff75c6525 | |||
| ee4a0b001e | |||
| 5fcd6fd744 | |||
| bf58f18b5f | |||
| 0c020b3ca4 | |||
| 2727ebb67f | |||
| 8fae4f3111 | |||
| 455b614008 | |||
| 93f4f40202 | |||
| aeade9ce58 | |||
| 4b89fb23c5 | |||
| 174439c2f0 | |||
| 43f3e10f05 | |||
| 97fcdb2830 | |||
| 31e2d8eb20 | |||
| 633a5a8848 | |||
| a5086a09b9 | |||
| c251be9ae5 | |||
| ec137cb5fb | |||
| ab4e24f115 | |||
| 2218ec4e31 | |||
| 319a8309c5 | |||
| 5af046f54f | |||
| f97a9d9762 | |||
| dc1a57a9f2 | |||
| 2d6111a04b | |||
| 710fd7859d | |||
| e340a4ceaf | |||
| 3fa44e076e | |||
| 3f9fb9c936 | |||
| 8db347a75e | |||
| 8db3343280 | |||
| 25c5a5b4ff | |||
| a696e77652 | |||
| 582a76d87c | |||
| fdfddde55a | |||
| 0ecfef2352 | |||
| 4fdece6c1c | |||
| 6f0bce8708 | |||
| d3bdeb73f5 | |||
| 942fdf5bee | |||
| dd2635dbe6 | |||
| 3d1bcb73c1 | |||
| a960e686b3 | |||
| 946774c3fb | |||
| 15edbc8067 | |||
| 1398ac24a2 | |||
| c76df4cd8f | |||
| a5e4dbf2d3 | |||
| 3768187395 | |||
| 08d0ce25f1 | |||
| 23241f18e2 | |||
| 90da67aa95 | |||
| 0bf2702149 | |||
| c7a75c8824 | |||
| 98b2b9745d | |||
| 65d5b3172c | |||
| 2f72f9e889 | |||
| 18f500a1f8 | |||
| b1df58796a | |||
| 761b3771d6 | |||
| df88edfda0 | |||
| 1dee1ba581 | |||
| b274c74a30 | |||
| b489bb15cf | |||
| dff4922a42 | |||
| dc6ad0b54c | |||
| 9769c05dc5 | |||
| dd379d3d4c | |||
| 1b884a3e52 | |||
| ddb164490e | |||
| 796135c7ce | |||
| 4cc4c01dd8 | |||
| 533b40922c | |||
| 0ae483ce27 | |||
| dbc1fa87ed | |||
| 0a3675b971 | |||
| ab3f529d29 | |||
| 607b712a07 | |||
| b6d9e49277 | |||
| 731d5943e2 | |||
| 01e7a43593 | |||
| b69a19ce7c | |||
| 8703acb533 | |||
| b59603d748 | |||
| b0cbe22f64 | |||
| 977d0322da | |||
| dd7394c14c | |||
| 2731e20893 | |||
| 502a513b5b | |||
| 3ac47e71cd | |||
| 7c1e25e713 | |||
| 2d90ad95f1 | |||
| cd9794471f | |||
| b2d3ab8bc1 | |||
| d8b70ef83b | |||
| a67fb1fb8d | |||
| ddd6e77cde |
+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",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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@8f6863c6c894ba46f9e676ef5cccec4752723c1e # 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@8f6863c6c894ba46f9e676ef5cccec4752723c1e # 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@8f6863c6c894ba46f9e676ef5cccec4752723c1e # 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"
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
deployments: write
|
||||
steps:
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
|
||||
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3
|
||||
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
repository: ${{ matrix.repo }}
|
||||
|
||||
@@ -73,6 +73,8 @@ 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' &&
|
||||
|
||||
@@ -16,12 +16,12 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
ref: staging
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
cache: "yarn"
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
disable-autolabeler: true
|
||||
|
||||
- name: Get actions scripts
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
persist-credentials: false
|
||||
|
||||
@@ -18,14 +18,14 @@ jobs:
|
||||
merge:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 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
|
||||
|
||||
- name: Get actions scripts
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
persist-credentials: false
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
sparse-checkout: |
|
||||
scripts/release
|
||||
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
|
||||
@@ -4,8 +4,6 @@ on:
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
NPM_TOKEN:
|
||||
required: false
|
||||
GPG_PASSPHRASE:
|
||||
required: false
|
||||
GPG_PRIVATE_KEY:
|
||||
@@ -73,7 +71,7 @@ jobs:
|
||||
draft: true
|
||||
latest: true
|
||||
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 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
|
||||
@@ -81,7 +79,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get actions scripts
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
persist-credentials: false
|
||||
@@ -125,7 +123,7 @@ jobs:
|
||||
git config --global user.email "releases@riot.im"
|
||||
git config --global user.name "RiotRobot"
|
||||
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
@@ -282,8 +280,6 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
post-release:
|
||||
name: Post release steps
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
name: Publish to npm
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
NPM_TOKEN:
|
||||
required: true
|
||||
outputs:
|
||||
id:
|
||||
description: "The npm package@version string we published"
|
||||
@@ -20,32 +17,29 @@ jobs:
|
||||
id: ${{ steps.npm-publish.outputs.id }}
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
ref: staging
|
||||
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
node-version-file: 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"
|
||||
|
||||
- name: 🚀 Publish to npm
|
||||
id: npm-publish
|
||||
run: |
|
||||
npm publish --provenance --access public --tag next
|
||||
npm publish --provenance --access public --tag "$TAG"
|
||||
release=$(jq -r '"\(.name)@\(.version)"' package.json)
|
||||
echo "id=$release" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: 🎖️ Add `latest` dist-tag to final releases
|
||||
if: steps.npm-publish.outputs.id && !contains(steps.npm-publish.outputs.id, '-rc.')
|
||||
run: npm dist-tag add "$release" latest
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
release: ${{ steps.npm-publish.outputs.id }}
|
||||
TAG: ${{ contains(steps.npm-publish.outputs.id, '-rc.') && 'next' || 'latest' }}
|
||||
|
||||
@@ -44,13 +44,13 @@ jobs:
|
||||
repo:
|
||||
- element-hq/element-web
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
repository: ${{ matrix.repo }}
|
||||
ref: staging
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
@@ -73,10 +73,10 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
|
||||
@@ -12,7 +12,7 @@ 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"
|
||||
permissions: {}
|
||||
jobs:
|
||||
sonarqube:
|
||||
@@ -36,14 +36,14 @@ jobs:
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- name: "🧮 Checkout code"
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 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
|
||||
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
if: ${{ !inputs.sharded }}
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
name: coverage
|
||||
path: coverage
|
||||
- name: 📥 Download sharded artifacts
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
if: inputs.sharded
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -75,13 +75,14 @@ jobs:
|
||||
|
||||
- name: "🩻 SonarCloud Scan"
|
||||
id: sonarcloud
|
||||
uses: matrix-org/sonarcloud-workflow-action@820f7c2e9e94ba9e35add0f739691e5c7e23fa25 # v4.0
|
||||
uses: matrix-org/sonarcloud-workflow-action@9f6f057c1b30500877460c66d4ae937e53d168ee
|
||||
# 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' }}
|
||||
skip_coverage_label: Z-Skip-Coverage
|
||||
version_cmd: "cat package.json | jq -r .version"
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
revision: ${{ github.event.workflow_run.head_sha }}
|
||||
|
||||
@@ -14,9 +14,9 @@ jobs:
|
||||
name: "Typescript Syntax Check"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
@@ -31,9 +31,9 @@ jobs:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
@@ -48,9 +48,9 @@ jobs:
|
||||
name: "Node.js example"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
- name: Build Types
|
||||
run: "yarn build:types"
|
||||
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "npm"
|
||||
node-version-file: "examples/node/package.json"
|
||||
@@ -83,9 +83,9 @@ jobs:
|
||||
name: "Workflow Lint"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
@@ -100,9 +100,9 @@ jobs:
|
||||
name: "JSDoc Checker"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
@@ -114,7 +114,7 @@ jobs:
|
||||
run: "yarn run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: docs
|
||||
path: _docs
|
||||
@@ -125,9 +125,9 @@ jobs:
|
||||
name: "Analyse Dead Code"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
@@ -143,11 +143,11 @@ jobs:
|
||||
if: github.event_name == 'merge_group'
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
repository: element-hq/element-web
|
||||
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
+11
-15
@@ -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,11 +22,11 @@ jobs:
|
||||
node: ["lts/*", 22]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- name: Setup Node
|
||||
id: setupNode
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: ${{ matrix.node }}
|
||||
@@ -42,14 +42,10 @@ jobs:
|
||||
run: |
|
||||
yarn test \
|
||||
--coverage=${{ env.ENABLE_COVERAGE }} \
|
||||
--ci \
|
||||
--max-workers ${{ steps.cpu-cores.outputs.count }} \
|
||||
--maxWorkers ${{ steps.cpu-cores.outputs.count }} \
|
||||
./spec/${{ matrix.specs }}
|
||||
env:
|
||||
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
|
||||
|
||||
# tell jest to use coloured output
|
||||
FORCE_COLOR: true
|
||||
SHARD: ${{ matrix.specs }}
|
||||
|
||||
- name: Move coverage files into place
|
||||
if: env.ENABLE_COVERAGE == 'true'
|
||||
@@ -57,7 +53,7 @@ jobs:
|
||||
|
||||
- name: Upload Artifact
|
||||
if: env.ENABLE_COVERAGE == 'true'
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: coverage-${{ matrix.specs }}-${{ matrix.node == 'lts/*' && 'lts' || matrix.node }}
|
||||
path: |
|
||||
@@ -65,13 +61,13 @@ 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:
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10
|
||||
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10
|
||||
with:
|
||||
operations-per-run: 250
|
||||
days-before-issue-stale: -1
|
||||
|
||||
@@ -1,3 +1,100 @@
|
||||
Changes in [40.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v40.1.0) (2026-01-27)
|
||||
==================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* Deprecate unused `EventShieldReason` reason codes ([#5127](https://github.com/matrix-org/matrix-js-sdk/pull/5127)). Contributed by @richvdh.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Add stable m.oauth UIA stage enum ([#5138](https://github.com/matrix-org/matrix-js-sdk/pull/5138)). Contributed by @hughns.
|
||||
* Add `MatrixEvent.getKeyForwardingUser` ([#5128](https://github.com/matrix-org/matrix-js-sdk/pull/5128)). Contributed by @richvdh.
|
||||
* Add types for (unstable) policy servers ([#5116](https://github.com/matrix-org/matrix-js-sdk/pull/5116)). Contributed by @turt2live.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [Backport staging] Recalculate room name on loading members ([#5164](https://github.com/matrix-org/matrix-js-sdk/pull/5164)). Contributed by @RiotRobot.
|
||||
* Avoid rapidly retrying failed requests ([#5146](https://github.com/matrix-org/matrix-js-sdk/pull/5146)). Contributed by @andybalaam.
|
||||
* [matrixRTC] MatrixRTCSessions, add missing event reemission. ([#5144](https://github.com/matrix-org/matrix-js-sdk/pull/5144)). Contributed by @toger5.
|
||||
* Use normal base64 encoding for RTC backend identities ([#5129](https://github.com/matrix-org/matrix-js-sdk/pull/5129)). Contributed by @robintown.
|
||||
* export parseCallNotificationContent and isMyMembership from RTC types ([#5132](https://github.com/matrix-org/matrix-js-sdk/pull/5132)). Contributed by @Half-Shot.
|
||||
|
||||
|
||||
Changes in [40.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v40.0.0) (2026-01-13)
|
||||
==================================================================================================
|
||||
## 🚨 BREAKING CHANGES
|
||||
|
||||
* MatrixRTC Pseudonymous livekit identities ([#5110](https://github.com/matrix-org/matrix-js-sdk/pull/5110)). Contributed by @toger5.
|
||||
|
||||
## 🦖 Deprecations
|
||||
|
||||
* Mark `forwardingCurve25519KeyChain` as deprecated ([#5111](https://github.com/matrix-org/matrix-js-sdk/pull/5111)). Contributed by @richvdh.
|
||||
* Mark `IEventDecryptionResult` as deprecated ([#5112](https://github.com/matrix-org/matrix-js-sdk/pull/5112)). Contributed by @richvdh.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Implement MSC4387: M\_SAFETY error ([#5107](https://github.com/matrix-org/matrix-js-sdk/pull/5107)). Contributed by @Half-Shot.
|
||||
* Implement \_unstable\_getRTCTransports for MSC4143 ([#5104](https://github.com/matrix-org/matrix-js-sdk/pull/5104)). Contributed by @Half-Shot.
|
||||
* Use `membershipID` for session events ([#5105](https://github.com/matrix-org/matrix-js-sdk/pull/5105)). Contributed by @toger5.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Make MatrixRTC encryption key types narrower for TS 5.9 compatibility ([#5117](https://github.com/matrix-org/matrix-js-sdk/pull/5117)). Contributed by @robintown.
|
||||
* Re-check outgoing requests after processing them ([#5109](https://github.com/matrix-org/matrix-js-sdk/pull/5109)). Contributed by @andybalaam.
|
||||
* Make token refresher init itself lazily ([#5106](https://github.com/matrix-org/matrix-js-sdk/pull/5106)). Contributed by @dbkr.
|
||||
|
||||
|
||||
Changes in [39.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.4.0) (2025-12-16)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Import room key bundles received after invite. ([#5080](https://github.com/matrix-org/matrix-js-sdk/pull/5080)). Contributed by @kaylendog.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Allow msc4354\_sticky\_key to be optional on sticky events. ([#5073](https://github.com/matrix-org/matrix-js-sdk/pull/5073)). Contributed by @Half-Shot.
|
||||
* Handle all response fields from /context API being optional ([#5089](https://github.com/matrix-org/matrix-js-sdk/pull/5089)). Contributed by @t3chguy.
|
||||
|
||||
|
||||
Changes in [39.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.3.0) (2025-12-02)
|
||||
==================================================================================================
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Re-add truthy check on room name/avatar/alias events ([#5081](https://github.com/matrix-org/matrix-js-sdk/pull/5081)). Contributed by @t3chguy.
|
||||
* Fix invalid state events corrupting room objects ([#5078](https://github.com/matrix-org/matrix-js-sdk/pull/5078)). Contributed by @t3chguy.
|
||||
|
||||
|
||||
Changes in [39.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.2.0) (2025-11-18)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Delayed event management: split endpoints, no auth ([#5066](https://github.com/matrix-org/matrix-js-sdk/pull/5066)). Contributed by @AndrewFerr.
|
||||
* do not set cache in authenticated fetch ([#5020](https://github.com/matrix-org/matrix-js-sdk/pull/5020)). Contributed by @pkuzco.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix media switching during legacy calls ([#5069](https://github.com/matrix-org/matrix-js-sdk/pull/5069)). Contributed by @langleyd.
|
||||
|
||||
|
||||
Changes in [39.1.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.1.2) (2025-11-04)
|
||||
==================================================================================================
|
||||
Re-release of v39.1.0 to fix npm publishing workflow
|
||||
|
||||
|
||||
|
||||
Changes in [39.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.1.1) (2025-11-04)
|
||||
==================================================================================================
|
||||
Re-release of v39.1.0 to fix npm publishing workflow
|
||||
|
||||
Changes in [39.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.1.0) (2025-11-04)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* [MatrixRTC] Sticky Events support (MSC4354) ([#5017](https://github.com/matrix-org/matrix-js-sdk/pull/5017)). Contributed by @toger5.
|
||||
* Add `CryptoApi.getSecretStorageStatus` ([#5054](https://github.com/matrix-org/matrix-js-sdk/pull/5054)). Contributed by @richvdh.
|
||||
* Add parseCallNotificationContent ([#5015](https://github.com/matrix-org/matrix-js-sdk/pull/5015)). Contributed by @toger5.
|
||||
* MSC4140: support filters on delayed event lookup ([#5038](https://github.com/matrix-org/matrix-js-sdk/pull/5038)). Contributed by @AndrewFerr.
|
||||
|
||||
|
||||
Changes in [39.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.0.0) (2025-10-21)
|
||||
==================================================================================================
|
||||
## 🚨 BREAKING CHANGES
|
||||
|
||||
+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
|
||||
|
||||
@@ -464,7 +464,7 @@ To build a browser version from scratch when developing:
|
||||
$ yarn build
|
||||
```
|
||||
|
||||
To run tests (Jest):
|
||||
To run tests:
|
||||
|
||||
```
|
||||
$ yarn test
|
||||
|
||||
+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
|
||||
|
||||
@@ -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;
|
||||
@@ -28,10 +28,8 @@ 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 `vitest`
|
||||
"vitest-sonar-reporter",
|
||||
// Used by `@babel/plugin-transform-runtime`
|
||||
"@babel/runtime",
|
||||
],
|
||||
|
||||
+21
-27
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "39.0.0",
|
||||
"version": "40.1.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
@@ -19,8 +19,8 @@
|
||||
"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",
|
||||
"test": "vitest",
|
||||
"test:watch": "vitest --watch",
|
||||
"coverage": "yarn test --coverage"
|
||||
},
|
||||
"repository": {
|
||||
@@ -49,17 +49,17 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^15.3.0",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^17.0.0",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^6.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"loglevel": "^1.9.2",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-widget-api": "^1.10.0",
|
||||
"matrix-widget-api": "^1.14.0",
|
||||
"oidc-client-ts": "^3.0.1",
|
||||
"p-retry": "7",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"sdp-transform": "^3.0.0",
|
||||
"unhomoglyph": "^1.0.6",
|
||||
"uuid": "13"
|
||||
},
|
||||
@@ -78,18 +78,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": "^29.0.0",
|
||||
"@types/node": "18",
|
||||
"@types/sdp-transform": "^2.4.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"babel-jest": "^29.0.0",
|
||||
"@vitest/coverage-v8": "^4.0.17",
|
||||
"@vitest/eslint-plugin": "^1.6.6",
|
||||
"@vitest/ui": "^4.0.17",
|
||||
"babel-plugin-search-and-replace": "^1.1.1",
|
||||
"debug": "^4.3.4",
|
||||
"eslint": "8.57.1",
|
||||
@@ -97,36 +98,29 @@
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-import-resolver-typescript": "^4.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jest": "^28.0.0",
|
||||
"eslint-plugin-jsdoc": "^50.0.0",
|
||||
"eslint-plugin-matrix-org": "2.1.0",
|
||||
"eslint-plugin-jsdoc": "^61.0.0",
|
||||
"eslint-plugin-matrix-org": "^3.0.0",
|
||||
"eslint-plugin-n": "^14.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.4.0",
|
||||
"eslint-plugin-tsdoc": "^0.5.0",
|
||||
"eslint-plugin-unicorn": "^56.0.0",
|
||||
"fake-indexeddb": "^5.0.2",
|
||||
"fetch-mock": "11.1.5",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
"fetch-mock": "^12.6.0",
|
||||
"happy-dom": "^20.1.0",
|
||||
"husky": "^9.0.0",
|
||||
"jest": "^29.0.0",
|
||||
"jest-environment-jsdom": "^29.0.0",
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jest-mock": "^29.0.0",
|
||||
"knip": "^5.0.0",
|
||||
"lint-staged": "^16.0.0",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"node-fetch": "^2.7.0",
|
||||
"prettier": "3.6.2",
|
||||
"prettier": "3.7.4",
|
||||
"rimraf": "^6.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"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": "^5.4.2",
|
||||
"vitest": "^4.0.17",
|
||||
"vitest-sonar-reporter": "^3.0.0"
|
||||
},
|
||||
"@casualbot/jest-sonar-reporter": {
|
||||
"outputDirectory": "coverage",
|
||||
"outputName": "jest-sonar-report.xml",
|
||||
"relativePaths": true
|
||||
"resolutions": {
|
||||
"expect": "30.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
@@ -104,7 +104,7 @@ afterEach(() => {
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = new IDBFactory();
|
||||
|
||||
jest.useRealTimers();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe("crypto", () => {
|
||||
@@ -154,13 +154,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 +165,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 +218,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 +243,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 +264,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 +344,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 +720,7 @@ describe("crypto", () => {
|
||||
syncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
await awaitDecryptionError;
|
||||
await expect(awaitDecryptionError).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -870,6 +871,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 +899,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 +1000,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 +1032,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 +1153,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 +1177,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 +1265,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 +1289,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 +1399,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 +1431,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 +1445,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 +1536,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 +1584,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 +1596,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 +1609,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 +1633,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 +1651,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 +1663,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 +1671,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 +1692,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 +1770,7 @@ describe("crypto", () => {
|
||||
|
||||
it("Should create a 4S key", async () => {
|
||||
accountDataAccumulator.interceptGetAccountData();
|
||||
accountDataAccumulator.interceptSetAccountData();
|
||||
|
||||
const awaitAccountData = awaitAccountDataUpdate("m.secret_storage.default_key");
|
||||
|
||||
@@ -1943,8 +1922,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 +1939,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 +1983,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) => {
|
||||
@@ -2106,8 +2071,7 @@ 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 !
|
||||
@@ -2195,6 +2159,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 +2217,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 });
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
@@ -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 mkDebug from "debug";
|
||||
|
||||
import {
|
||||
@@ -30,11 +30,12 @@ import {
|
||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver.ts";
|
||||
import { SyncResponder } from "../../test-utils/SyncResponder.ts";
|
||||
import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints.ts";
|
||||
import { getSyncResponse, mkEventCustom, syncPromise } from "../../test-utils/test-utils.ts";
|
||||
import { getSyncResponse, mkEventCustom, syncPromise, waitFor } from "../../test-utils/test-utils.ts";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder.ts";
|
||||
import { flushPromises } from "../../test-utils/flushPromises.ts";
|
||||
import { E2EOTKClaimResponder } from "../../test-utils/E2EOTKClaimResponder.ts";
|
||||
import { escapeRegExp } from "../../../src/utils.ts";
|
||||
import { EventShieldColour, EventShieldReason } from "../../../src/crypto-api";
|
||||
|
||||
const debug = mkDebug("matrix-js-sdk:history-sharing");
|
||||
|
||||
@@ -82,7 +83,7 @@ describe("History Sharing", () => {
|
||||
beforeEach(async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
|
||||
mockSetupCrossSigningRequests();
|
||||
|
||||
const aliceId = "@alice:localhost";
|
||||
@@ -128,8 +129,8 @@ describe("History Sharing", () => {
|
||||
|
||||
// Now, Alice invites Bob
|
||||
const uploadProm = new Promise<Uint8Array>((resolve) => {
|
||||
fetchMock.postOnce(new URL("/_matrix/media/v3/upload", ALICE_HOMESERVER_URL).toString(), (url, request) => {
|
||||
const body = request.body as Uint8Array;
|
||||
fetchMock.postOnce(new URL("/_matrix/media/v3/upload", ALICE_HOMESERVER_URL).toString(), (callLog) => {
|
||||
const body = callLog.options.body as Uint8Array;
|
||||
debug(`Alice uploaded blob of length ${body.length}`);
|
||||
resolve(body);
|
||||
return { content_uri: "mxc://alice-server/here" };
|
||||
@@ -173,11 +174,9 @@ describe("History Sharing", () => {
|
||||
fetchMock.postOnce(`${BOB_HOMESERVER_URL}/_matrix/client/v3/join/${encodeURIComponent(ROOM_ID)}`, {
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
fetchMock.getOnce(
|
||||
`begin:${BOB_HOMESERVER_URL}/_matrix/client/v1/media/download/alice-server/here`,
|
||||
{ body: uploadedBlob },
|
||||
{ sendAsJson: false },
|
||||
);
|
||||
fetchMock.getOnce(`begin:${BOB_HOMESERVER_URL}/_matrix/client/v1/media/download/alice-server/here`, {
|
||||
body: uploadedBlob,
|
||||
});
|
||||
await bobClient.joinRoom(ROOM_ID, { acceptSharedHistory: true });
|
||||
|
||||
// Bob receives, should be able to decrypt, the megolm message
|
||||
@@ -199,6 +198,110 @@ describe("History Sharing", () => {
|
||||
await event.getDecryptionPromise();
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent().body).toEqual("Hi!");
|
||||
expect(event.getKeyForwardingUser()).toEqual(aliceClient.getUserId());
|
||||
const encryptionInfo = await bobClient.getCrypto()!.getEncryptionInfoForEvent(event);
|
||||
expect(encryptionInfo?.shieldColour).toEqual(EventShieldColour.GREY);
|
||||
expect(encryptionInfo?.shieldReason).toEqual(EventShieldReason.AUTHENTICITY_NOT_GUARANTEED);
|
||||
});
|
||||
|
||||
test("Room keys are imported correctly if invite is accepted before the bundle arrives", async () => {
|
||||
// Alice is in an encrypted room
|
||||
const syncResponse = getSyncResponse([aliceClient.getSafeUserId()], ROOM_ID);
|
||||
aliceSyncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// ... and she sends an event
|
||||
const msgProm = expectSendRoomEvent(ALICE_HOMESERVER_URL, "m.room.encrypted");
|
||||
await aliceClient.sendEvent(ROOM_ID, EventType.RoomMessage, { msgtype: MsgType.Text, body: "Hello!" });
|
||||
const sentMessage = await msgProm;
|
||||
debug(`Alice sent encrypted room event: ${JSON.stringify(sentMessage)}`);
|
||||
|
||||
// Now, Alice invites Bob
|
||||
const uploadProm = new Promise<Uint8Array>((resolve) => {
|
||||
fetchMock.postOnce(new URL("/_matrix/media/v3/upload", ALICE_HOMESERVER_URL).toString(), (callLog) => {
|
||||
const body = callLog.options.body as Uint8Array;
|
||||
debug(`Alice uploaded blob of length ${body.length}`);
|
||||
resolve(body);
|
||||
return { content_uri: "mxc://alice-server/here" };
|
||||
});
|
||||
});
|
||||
const toDeviceMessageProm = expectSendToDeviceMessage(ALICE_HOMESERVER_URL, "m.room.encrypted");
|
||||
// POST https://alice-server.com/_matrix/client/v3/rooms/!room%3Aexample.com/invite
|
||||
fetchMock.postOnce(`${ALICE_HOMESERVER_URL}/_matrix/client/v3/rooms/${encodeURIComponent(ROOM_ID)}/invite`, {});
|
||||
await aliceClient.invite(ROOM_ID, bobClient.getSafeUserId(), { shareEncryptedHistory: true });
|
||||
const uploadedBlob = await uploadProm;
|
||||
const sentToDeviceRequest = await toDeviceMessageProm;
|
||||
debug(`Alice sent encrypted to-device events: ${JSON.stringify(sentToDeviceRequest)}`);
|
||||
const bobToDeviceMessage = sentToDeviceRequest[bobClient.getSafeUserId()][bobClient.deviceId!];
|
||||
expect(bobToDeviceMessage).toBeDefined();
|
||||
|
||||
// Bob receives the room invite, but not the room key bundle
|
||||
const inviteEvent = mkEventCustom({
|
||||
type: "m.room.member",
|
||||
sender: aliceClient.getSafeUserId(),
|
||||
state_key: bobClient.getSafeUserId(),
|
||||
content: { membership: KnownMembership.Invite },
|
||||
});
|
||||
bobSyncResponder.sendOrQueueSyncResponse({
|
||||
rooms: { invite: { [ROOM_ID]: { invite_state: { events: [inviteEvent] } } } },
|
||||
});
|
||||
await syncPromise(bobClient);
|
||||
|
||||
const room = bobClient.getRoom(ROOM_ID);
|
||||
expect(room).toBeTruthy();
|
||||
expect(room?.getMyMembership()).toEqual(KnownMembership.Invite);
|
||||
|
||||
fetchMock.postOnce(`${BOB_HOMESERVER_URL}/_matrix/client/v3/join/${encodeURIComponent(ROOM_ID)}`, {
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
await bobClient.joinRoom(ROOM_ID, { acceptSharedHistory: true });
|
||||
|
||||
// Bob receives and attempts to decrypt the megolm message, but should not be able to (yet).
|
||||
const bobSyncResponse = getSyncResponse([aliceClient.getSafeUserId(), bobClient.getSafeUserId()], ROOM_ID);
|
||||
bobSyncResponse.rooms.join[ROOM_ID].timeline.events.push(
|
||||
mkEventCustom({
|
||||
type: "m.room.encrypted",
|
||||
sender: aliceClient.getSafeUserId(),
|
||||
content: sentMessage,
|
||||
event_id: "$event_id",
|
||||
}) as any,
|
||||
);
|
||||
bobSyncResponder.sendOrQueueSyncResponse(bobSyncResponse);
|
||||
await syncPromise(bobClient);
|
||||
const bobRoom = bobClient.getRoom(ROOM_ID);
|
||||
const event = bobRoom!.getLastLiveEvent()!;
|
||||
expect(event.getId()).toEqual("$event_id");
|
||||
await event.getDecryptionPromise();
|
||||
expect(event.isDecryptionFailure()).toBeTruthy();
|
||||
|
||||
// Now the room key bundle message arrives
|
||||
fetchMock.getOnce(`begin:${BOB_HOMESERVER_URL}/_matrix/client/v1/media/download/alice-server/here`, {
|
||||
body: uploadedBlob,
|
||||
});
|
||||
bobSyncResponder.sendOrQueueSyncResponse({
|
||||
to_device: {
|
||||
events: [
|
||||
{
|
||||
type: "m.room.encrypted",
|
||||
sender: aliceClient.getSafeUserId(),
|
||||
content: bobToDeviceMessage,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await syncPromise(bobClient);
|
||||
|
||||
// Once the room key bundle finishes downloading, we should be able to decrypt the message.
|
||||
await waitFor(async () => {
|
||||
await event.getDecryptionPromise();
|
||||
expect(event.isDecryptionFailure()).toBeFalsy();
|
||||
});
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent().body).toEqual("Hello!");
|
||||
expect(event.getKeyForwardingUser()).toEqual(aliceClient.getUserId());
|
||||
const encryptionInfo = await bobClient.getCrypto()!.getEncryptionInfoForEvent(event);
|
||||
expect(encryptionInfo?.shieldColour).toEqual(EventShieldColour.GREY);
|
||||
expect(encryptionInfo?.shieldReason).toEqual(EventShieldReason.AUTHENTICITY_NOT_GUARANTEED);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -212,8 +315,8 @@ function expectSendRoomEvent(homeserverUrl: string, msgtype: string): Promise<IC
|
||||
return new Promise<IContent>((resolve) => {
|
||||
fetchMock.putOnce(
|
||||
new RegExp(`^${escapeRegExp(homeserverUrl)}/_matrix/client/v3/rooms/[^/]*/send/${escapeRegExp(msgtype)}/`),
|
||||
(url, request) => {
|
||||
const content = JSON.parse(request.body as string);
|
||||
(callLog) => {
|
||||
const content = JSON.parse(callLog.options.body as string);
|
||||
resolve(content);
|
||||
return { event_id: "$event_id" };
|
||||
},
|
||||
@@ -229,8 +332,8 @@ function expectSendToDeviceMessage(
|
||||
return new Promise((resolve) => {
|
||||
fetchMock.putOnce(
|
||||
new RegExp(`^${escapeRegExp(homeserverUrl)}/_matrix/client/v3/sendToDevice/${escapeRegExp(msgtype)}/`),
|
||||
(url: string, opts: RequestInit) => {
|
||||
const body = JSON.parse(opts.body as string);
|
||||
(callLog) => {
|
||||
const body = JSON.parse(callLog.options.body as string);
|
||||
resolve(body.messages);
|
||||
return {};
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
@@ -69,10 +70,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 +85,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 +101,7 @@ function mockUploadEmitter(
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
{ name: "mock-upload-emitter" },
|
||||
);
|
||||
return emitter;
|
||||
}
|
||||
@@ -117,12 +117,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 +131,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 +202,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 +232,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 +248,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 +269,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 +283,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 +359,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 +375,7 @@ describe("megolm-keys backup", () => {
|
||||
check!.backupInfo!.version!,
|
||||
);
|
||||
|
||||
const progressCallback = jest.fn();
|
||||
const progressCallback = vi.fn();
|
||||
const result = await aliceCrypto.restoreKeyBackup({
|
||||
progressCallback,
|
||||
});
|
||||
@@ -417,7 +412,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 +422,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 +434,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 +458,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 +475,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,7 +500,7 @@ 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: {
|
||||
@@ -534,7 +529,7 @@ describe("megolm-keys backup", () => {
|
||||
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 +566,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 +576,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 +600,7 @@ describe("megolm-keys backup", () => {
|
||||
|
||||
await aliceCrypto.importRoomKeys([newKey]);
|
||||
|
||||
jest.runAllTimers();
|
||||
vi.runAllTimers();
|
||||
await newKeyUploadPromise;
|
||||
});
|
||||
|
||||
@@ -630,7 +625,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 +635,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 +646,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 +689,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 +709,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 +725,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 +752,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 +760,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 +779,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 +789,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 +811,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 +827,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 +963,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 +973,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 +988,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 +998,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 +1013,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 +1034,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 +1072,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 +1088,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 +1099,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 +1110,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 +1123,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 +1142,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 +1178,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";
|
||||
|
||||
@@ -72,7 +72,6 @@ describe("Encrypted State Events", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
|
||||
const homeserverUrl = "https://alice-server.com";
|
||||
aliceClient = createClient({
|
||||
@@ -96,15 +95,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 +185,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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -15,8 +15,7 @@ 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 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();
|
||||
@@ -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" },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ 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,
|
||||
},
|
||||
}),
|
||||
],
|
||||
@@ -136,7 +136,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 +592,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 +641,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+97
-50
@@ -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,
|
||||
@@ -54,26 +50,26 @@ const testOIDCToken = {
|
||||
token_type: "Bearer",
|
||||
};
|
||||
class MockWidgetApi extends EventEmitter {
|
||||
public start = jest.fn().mockResolvedValue(undefined);
|
||||
public getClientVersions = jest.fn();
|
||||
public requestCapability = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilities = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityForRoomTimeline = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToSendEvent = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToReceiveEvent = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToSendMessage = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToReceiveMessage = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToSendState = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToReceiveState = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToSendToDevice = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToReceiveToDevice = jest.fn().mockResolvedValue(undefined);
|
||||
public sendRoomEvent = jest.fn(
|
||||
public start = vi.fn().mockResolvedValue(undefined);
|
||||
public getClientVersions = vi.fn();
|
||||
public requestCapability = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilities = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityForRoomTimeline = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToSendEvent = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToReceiveEvent = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToSendMessage = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToReceiveMessage = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToSendState = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToReceiveState = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToSendToDevice = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToReceiveToDevice = vi.fn().mockResolvedValue(undefined);
|
||||
public sendRoomEvent = vi.fn(
|
||||
async (eventType: string, content: unknown, roomId?: string, delay?: number, parentDelayId?: string) =>
|
||||
delay === undefined && parentDelayId === undefined
|
||||
? { event_id: `$${Math.random()}` }
|
||||
: { delay_id: `id-${Math.random()}` },
|
||||
);
|
||||
public sendStateEvent = jest.fn(
|
||||
public sendStateEvent = vi.fn(
|
||||
async (
|
||||
eventType: string,
|
||||
stateKey: string,
|
||||
@@ -86,22 +82,24 @@ class MockWidgetApi extends EventEmitter {
|
||||
? { event_id: `$${Math.random()}` }
|
||||
: { delay_id: `id-${Math.random()}` },
|
||||
);
|
||||
public updateDelayedEvent = jest.fn().mockResolvedValue(undefined);
|
||||
public sendToDevice = jest.fn().mockResolvedValue(undefined);
|
||||
public requestOpenIDConnectToken = jest.fn(async () => {
|
||||
public cancelScheduledDelayedEvent = vi.fn().mockResolvedValue(undefined);
|
||||
public restartScheduledDelayedEvent = vi.fn().mockResolvedValue(undefined);
|
||||
public sendScheduledDelayedEvent = vi.fn().mockResolvedValue(undefined);
|
||||
public sendToDevice = vi.fn().mockResolvedValue(undefined);
|
||||
public requestOpenIDConnectToken = vi.fn(async () => {
|
||||
return testOIDCToken;
|
||||
return new Promise<IOpenIDCredentials>(() => {
|
||||
return testOIDCToken;
|
||||
});
|
||||
});
|
||||
public readStateEvents = jest.fn(async () => []);
|
||||
public getTurnServers = jest.fn(async () => []);
|
||||
public sendContentLoaded = jest.fn().mockResolvedValue(undefined);
|
||||
public readStateEvents = vi.fn(async () => []);
|
||||
public getTurnServers = vi.fn(async () => []);
|
||||
public sendContentLoaded = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
public transport = {
|
||||
reply: jest.fn(),
|
||||
send: jest.fn(),
|
||||
sendComplete: jest.fn(),
|
||||
reply: vi.fn(),
|
||||
send: vi.fn(),
|
||||
sendComplete: vi.fn(),
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -228,7 +226,7 @@ describe("RoomWidgetClient", () => {
|
||||
);
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToReceiveEvent).toHaveBeenCalledWith("org.matrix.rageshake_request");
|
||||
const injectSpy = jest.spyOn((client as any).syncApi, "injectRoomEvents");
|
||||
const injectSpy = vi.spyOn((client as any).syncApi, "injectRoomEvents");
|
||||
const widgetSendEmitter = new EventEmitter();
|
||||
const widgetSendPromise = new Promise<void>((resolve) =>
|
||||
widgetSendEmitter.once("send", () => resolve()),
|
||||
@@ -355,10 +353,10 @@ describe("RoomWidgetClient", () => {
|
||||
|
||||
it("handles widget errors with generic error data", async () => {
|
||||
const error = new Error("failed to send");
|
||||
widgetApi.transport.send.mockRejectedValue(error);
|
||||
vi.mocked(widgetApi.transport.send).mockRejectedValue(error);
|
||||
|
||||
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
|
||||
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send);
|
||||
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send as any);
|
||||
|
||||
await expect(
|
||||
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
|
||||
@@ -381,22 +379,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"),
|
||||
);
|
||||
|
||||
@@ -531,21 +529,63 @@ describe("RoomWidgetClient", () => {
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("updates delayed events", async () => {
|
||||
it.each([UpdateDelayedEventAction.Cancel, UpdateDelayedEventAction.Restart, UpdateDelayedEventAction.Send])(
|
||||
"can %s scheduled delayed events (action in parameter)",
|
||||
async (action: UpdateDelayedEventAction) => {
|
||||
await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(
|
||||
MatrixCapabilities.MSC4157UpdateDelayedEvent,
|
||||
);
|
||||
await client._unstable_updateDelayedEvent("id", action);
|
||||
let updateDelayedEvent: (delayId: string) => Promise<unknown>;
|
||||
switch (action) {
|
||||
case UpdateDelayedEventAction.Cancel:
|
||||
updateDelayedEvent = widgetApi.cancelScheduledDelayedEvent;
|
||||
break;
|
||||
case UpdateDelayedEventAction.Restart:
|
||||
updateDelayedEvent = widgetApi.cancelScheduledDelayedEvent;
|
||||
break;
|
||||
case UpdateDelayedEventAction.Send:
|
||||
updateDelayedEvent = widgetApi.sendScheduledDelayedEvent;
|
||||
break;
|
||||
}
|
||||
expect(updateDelayedEvent).toHaveBeenCalledWith("id");
|
||||
},
|
||||
);
|
||||
|
||||
it("can cancel scheduled delayed events (action in method)", async () => {
|
||||
await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157UpdateDelayedEvent);
|
||||
for (const action of [
|
||||
UpdateDelayedEventAction.Cancel,
|
||||
UpdateDelayedEventAction.Restart,
|
||||
UpdateDelayedEventAction.Send,
|
||||
]) {
|
||||
await client._unstable_updateDelayedEvent("id", action);
|
||||
expect(widgetApi.updateDelayedEvent).toHaveBeenCalledWith("id", action);
|
||||
}
|
||||
await client._unstable_cancelScheduledDelayedEvent("id");
|
||||
expect(widgetApi.cancelScheduledDelayedEvent).toHaveBeenCalledWith("id");
|
||||
});
|
||||
|
||||
it("can restart scheduled delayed events (action in method)", async () => {
|
||||
await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157UpdateDelayedEvent);
|
||||
await client._unstable_restartScheduledDelayedEvent("id");
|
||||
expect(widgetApi.restartScheduledDelayedEvent).toHaveBeenCalledWith("id");
|
||||
});
|
||||
|
||||
it("can send scheduled delayed events (action in method)", async () => {
|
||||
await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157UpdateDelayedEvent);
|
||||
await client._unstable_sendScheduledDelayedEvent("id");
|
||||
expect(widgetApi.sendScheduledDelayedEvent).toHaveBeenCalledWith("id");
|
||||
});
|
||||
});
|
||||
|
||||
describe("when unsupported", () => {
|
||||
const doesServerSupportUnstableFeatureMock = vi.fn().mockResolvedValue(false);
|
||||
|
||||
beforeAll(() => {
|
||||
MatrixClient.prototype.doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
doesServerSupportUnstableFeatureMock.mockReset();
|
||||
});
|
||||
|
||||
it("fails to send delayed message events", async () => {
|
||||
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
|
||||
await expect(
|
||||
@@ -583,6 +623,13 @@ describe("RoomWidgetClient", () => {
|
||||
"Server does not support",
|
||||
);
|
||||
}
|
||||
for (const updateDelayedEvent of [
|
||||
client._unstable_cancelScheduledDelayedEvent,
|
||||
client._unstable_restartScheduledDelayedEvent,
|
||||
client._unstable_sendScheduledDelayedEvent,
|
||||
]) {
|
||||
await expect(updateDelayedEvent.call(client, "id")).rejects.toThrow("Server does not support");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -729,7 +776,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 } }),
|
||||
@@ -929,7 +976,7 @@ describe("RoomWidgetClient", () => {
|
||||
|
||||
it("handles widget errors with generic error data", async () => {
|
||||
const error = new Error("failed to get token");
|
||||
widgetApi.transport.sendComplete.mockRejectedValue(error);
|
||||
vi.mocked(widgetApi.transport.sendComplete).mockRejectedValue(error);
|
||||
|
||||
await makeClient({});
|
||||
widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any);
|
||||
@@ -953,9 +1000,9 @@ describe("RoomWidgetClient", () => {
|
||||
response: errorData,
|
||||
},
|
||||
});
|
||||
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl);
|
||||
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl, undefined, expect.any(Headers));
|
||||
|
||||
widgetApi.transport.sendComplete.mockRejectedValue(widgetError);
|
||||
vi.mocked(widgetApi.transport.sendComplete).mockRejectedValue(widgetError);
|
||||
|
||||
await makeClient({});
|
||||
widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any);
|
||||
|
||||
@@ -37,7 +37,7 @@ describe("eventMapperFor", function () {
|
||||
setUserCreator(_) {},
|
||||
} as IStore,
|
||||
scheduler: {
|
||||
setProcessFunction: jest.fn(),
|
||||
setProcessFunction: vi.fn(),
|
||||
} as unknown as MatrixScheduler,
|
||||
userId: userId,
|
||||
});
|
||||
@@ -133,7 +133,7 @@ describe("eventMapperFor", function () {
|
||||
event_id: eventId,
|
||||
};
|
||||
|
||||
const decryptEventIfNeededSpy = jest.spyOn(client, "decryptEventIfNeeded");
|
||||
const decryptEventIfNeededSpy = vi.spyOn(client, "decryptEventIfNeeded");
|
||||
decryptEventIfNeededSpy.mockResolvedValue(); // stub it out
|
||||
|
||||
const mapper = eventMapperFor(client, {
|
||||
@@ -161,7 +161,7 @@ describe("eventMapperFor", function () {
|
||||
event_id: eventId,
|
||||
};
|
||||
|
||||
const evListener = jest.fn();
|
||||
const evListener = vi.fn();
|
||||
client.on(MatrixEventEvent.Replaced, evListener);
|
||||
|
||||
const noReEmitMapper = eventMapperFor(client, {
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { type MockInstance } from "vitest";
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import {
|
||||
@@ -196,7 +196,7 @@ describe("EventTimelineSet", () => {
|
||||
|
||||
it("should aggregate relations which belong to unknown timeline without adding them to any timeline", () => {
|
||||
// If threads are disabled all events go into the main timeline
|
||||
mocked(client.supportsThreads).mockReturnValue(true);
|
||||
vi.mocked(client.supportsThreads).mockReturnValue(true);
|
||||
const reactionEvent = utils.mkReaction(messageEvent, client, client.getSafeUserId(), roomId);
|
||||
|
||||
const liveTimeline = eventTimelineSet.getLiveTimeline();
|
||||
@@ -228,7 +228,7 @@ describe("EventTimelineSet", () => {
|
||||
let thread: Thread;
|
||||
|
||||
beforeEach(() => {
|
||||
(client.supportsThreads as jest.Mock).mockReturnValue(true);
|
||||
vi.mocked(client.supportsThreads).mockReturnValue(true);
|
||||
thread = new Thread("!thread_id:server", messageEvent, { room, client });
|
||||
});
|
||||
|
||||
@@ -306,20 +306,20 @@ describe("EventTimelineSet", () => {
|
||||
});
|
||||
|
||||
describe("with events to be decrypted", () => {
|
||||
let messageEventShouldAttemptDecryptionSpy: jest.SpyInstance;
|
||||
let messageEventIsDecryptionFailureSpy: jest.SpyInstance;
|
||||
let messageEventShouldAttemptDecryptionSpy: MockInstance;
|
||||
let messageEventIsDecryptionFailureSpy: MockInstance;
|
||||
|
||||
let replyEventShouldAttemptDecryptionSpy: jest.SpyInstance;
|
||||
let replyEventIsDecryptionFailureSpy: jest.SpyInstance;
|
||||
let replyEventShouldAttemptDecryptionSpy: MockInstance;
|
||||
let replyEventIsDecryptionFailureSpy: MockInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
messageEventShouldAttemptDecryptionSpy = jest.spyOn(messageEvent, "shouldAttemptDecryption");
|
||||
messageEventShouldAttemptDecryptionSpy = vi.spyOn(messageEvent, "shouldAttemptDecryption");
|
||||
messageEventShouldAttemptDecryptionSpy.mockReturnValue(true);
|
||||
messageEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, "isDecryptionFailure");
|
||||
messageEventIsDecryptionFailureSpy = vi.spyOn(messageEvent, "isDecryptionFailure");
|
||||
|
||||
replyEventShouldAttemptDecryptionSpy = jest.spyOn(replyEvent, "shouldAttemptDecryption");
|
||||
replyEventShouldAttemptDecryptionSpy = vi.spyOn(replyEvent, "shouldAttemptDecryption");
|
||||
replyEventShouldAttemptDecryptionSpy.mockReturnValue(true);
|
||||
replyEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, "isDecryptionFailure");
|
||||
replyEventIsDecryptionFailureSpy = vi.spyOn(messageEvent, "isDecryptionFailure");
|
||||
|
||||
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, false, eventTimeline, "foo");
|
||||
});
|
||||
@@ -384,7 +384,7 @@ describe("EventTimelineSet", () => {
|
||||
let thread: Thread;
|
||||
|
||||
beforeEach(() => {
|
||||
(client.supportsThreads as jest.Mock).mockReturnValue(true);
|
||||
vi.mocked(client.supportsThreads).mockReturnValue(true);
|
||||
thread = new Thread("!thread_id:server", messageEvent, { room, client });
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { Direction, EventTimeline } from "../../src/models/event-timeline";
|
||||
import { RoomState } from "../../src/models/room-state";
|
||||
@@ -20,21 +18,21 @@ describe("EventTimeline", function () {
|
||||
const getTimeline = (): EventTimeline => {
|
||||
const room = new Room(roomId, mockClient, userA);
|
||||
const timelineSet = new EventTimelineSet(room);
|
||||
jest.spyOn(room, "getUnfilteredTimelineSet").mockReturnValue(timelineSet);
|
||||
vi.spyOn(room, "getUnfilteredTimelineSet").mockReturnValue(timelineSet);
|
||||
|
||||
const timeline = new EventTimeline(timelineSet);
|
||||
// We manually stub the methods we'll be mocking out later instead of mocking the whole module
|
||||
// otherwise the default member property values (e.g. paginationToken) will be incorrect
|
||||
timeline.getState(Direction.Backward)!.setStateEvents = jest.fn();
|
||||
timeline.getState(Direction.Backward)!.getSentinelMember = jest.fn();
|
||||
timeline.getState(Direction.Forward)!.setStateEvents = jest.fn();
|
||||
timeline.getState(Direction.Forward)!.getSentinelMember = jest.fn();
|
||||
timeline.getState(Direction.Backward)!.setStateEvents = vi.fn();
|
||||
timeline.getState(Direction.Backward)!.getSentinelMember = vi.fn();
|
||||
timeline.getState(Direction.Forward)!.setStateEvents = vi.fn();
|
||||
timeline.getState(Direction.Forward)!.getSentinelMember = vi.fn();
|
||||
return timeline;
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
// reset any RoomState mocks
|
||||
jest.resetAllMocks();
|
||||
vi.resetAllMocks();
|
||||
|
||||
timeline = getTimeline();
|
||||
});
|
||||
@@ -67,12 +65,12 @@ describe("EventTimeline", function () {
|
||||
timeline.initialiseState(events);
|
||||
// @ts-ignore private prop
|
||||
const timelineStartState = timeline.startState!;
|
||||
expect(mocked(timelineStartState).setStateEvents).toHaveBeenCalledWith(events, {
|
||||
expect(vi.mocked(timelineStartState).setStateEvents).toHaveBeenCalledWith(events, {
|
||||
timelineWasEmpty: undefined,
|
||||
});
|
||||
// @ts-ignore private prop
|
||||
const timelineEndState = timeline.endState!;
|
||||
expect(mocked(timelineEndState).setStateEvents).toHaveBeenCalledWith(events, {
|
||||
expect(vi.mocked(timelineEndState).setStateEvents).toHaveBeenCalledWith(events, {
|
||||
timelineWasEmpty: undefined,
|
||||
});
|
||||
});
|
||||
@@ -210,13 +208,13 @@ describe("EventTimeline", function () {
|
||||
sentinel.name = "Old Alice";
|
||||
sentinel.membership = KnownMembership.Join;
|
||||
|
||||
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
vi.mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
vi.mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
}
|
||||
@@ -253,13 +251,13 @@ describe("EventTimeline", function () {
|
||||
sentinel.name = "Old Alice";
|
||||
sentinel.membership = KnownMembership.Join;
|
||||
|
||||
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
vi.mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
vi.mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`MatrixHttpApi should return expected object from \`getContentUri\` 1`] = `
|
||||
exports[`MatrixHttpApi > should return expected object from \`getContentUri\` 1`] = `
|
||||
{
|
||||
"base": "http://baseUrl",
|
||||
"params": {
|
||||
|
||||
@@ -57,7 +57,7 @@ describe("MatrixError", () => {
|
||||
|
||||
it("should retrieve Date Retry-After header from rate-limit error", () => {
|
||||
headers.set("Retry-After", `${new Date(160000).toUTCString()}`);
|
||||
jest.spyOn(globalThis.Date, "now").mockImplementationOnce(() => 100000);
|
||||
vi.spyOn(globalThis.Date, "now").mockImplementationOnce(() => 100000);
|
||||
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED", retry_after_ms: 150000 });
|
||||
expect(err.isRateLimitError()).toBe(true);
|
||||
// prefer Retry-After header over retry_after_ms
|
||||
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -398,7 +406,7 @@ describe("FetchHttpApi", () => {
|
||||
onlyData: true,
|
||||
});
|
||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
|
||||
unknownTokenErr,
|
||||
new TokenRefreshError(unknownTokenErr),
|
||||
);
|
||||
expect(tokenRefreshFunction).toHaveBeenCalledWith(refreshToken);
|
||||
expect(emitter.emit).not.toHaveBeenCalledWith(HttpApiEvent.SessionLoggedOut, unknownTokenErr);
|
||||
@@ -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";
|
||||
@@ -68,10 +68,6 @@ describe("SSO login URL", function () {
|
||||
});
|
||||
|
||||
describe("refreshToken", () => {
|
||||
afterEach(() => {
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
it("requests the correctly-prefixed /refresh endpoint when server correctly accepts /v3", async () => {
|
||||
const client = createExampleMatrixClient();
|
||||
|
||||
|
||||
+366
-158
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type MatrixEvent } from "../../../src";
|
||||
import { type IContent, type MatrixEvent } from "../../../src";
|
||||
import {
|
||||
CallMembership,
|
||||
type SessionMembershipData,
|
||||
@@ -25,20 +25,27 @@ import { membershipTemplate } from "./mocks";
|
||||
|
||||
function makeMockEvent(originTs = 0): MatrixEvent {
|
||||
return {
|
||||
getTs: jest.fn().mockReturnValue(originTs),
|
||||
getSender: jest.fn().mockReturnValue("@alice:example.org"),
|
||||
getId: jest.fn().mockReturnValue("$eventid"),
|
||||
getTs: vi.fn().mockReturnValue(originTs),
|
||||
getSender: vi.fn().mockReturnValue("@alice:example.org"),
|
||||
getId: vi.fn().mockReturnValue("$eventid"),
|
||||
getContent: vi.fn().mockReturnValue({}),
|
||||
} 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", () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
const membershipTemplate: SessionMembershipData = {
|
||||
@@ -53,29 +60,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 +91,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 +126,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 +139,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 +150,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");
|
||||
});
|
||||
@@ -170,7 +177,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);
|
||||
@@ -192,29 +199,29 @@ 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("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 +230,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 +265,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 +307,21 @@ describe("CallMembership", () => {
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("considers memberships unexpired if local age low enough", () => {
|
||||
it.skip("considers memberships unexpired if local age low enough", () => {
|
||||
// TODO link prev event
|
||||
});
|
||||
|
||||
it("considers memberships expired if local age large enough", () => {
|
||||
it.skip("considers memberships expired if local age large enough", () => {
|
||||
// TODO link prev event
|
||||
});
|
||||
|
||||
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 +331,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");
|
||||
});
|
||||
@@ -372,19 +379,35 @@ describe("CallMembership", () => {
|
||||
beforeEach(() => {
|
||||
// server origin timestamp for this event is 1000
|
||||
fakeEvent = makeMockEvent(1000);
|
||||
membership = new CallMembership(fakeEvent!, membershipTemplate);
|
||||
membership = createCallMembership(fakeEvent!, membershipTemplate);
|
||||
|
||||
jest.useFakeTimers();
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("calculates time until expiry", () => {
|
||||
jest.setSystemTime(2000);
|
||||
vi.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 () => {
|
||||
expect(
|
||||
await CallMembership.computeRtcBackendIdentity(makeMockEvent(), {
|
||||
kind: "rtc",
|
||||
data: {
|
||||
slot_id: "m.call#",
|
||||
application: { "type": "m.call", "m.call.id": "", "m.call.intent": "voice" },
|
||||
member: { user_id: "@alice:example.org", device_id: "AAAAAAA", id: "xyzRANDOMxyz" },
|
||||
rtc_transports: [{ type: "livekit" }],
|
||||
versions: [],
|
||||
msc4354_sticky_key: "abc123",
|
||||
},
|
||||
}),
|
||||
).toBe("2+h2ELE1XY/NsuveToZOekORCoyQMO6V0W7XZUWk5Q4");
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,136 +14,149 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ClientEvent, EventTimeline, MatrixClient } from "../../../src";
|
||||
import { RoomStateEvent } from "../../../src/models/room-state";
|
||||
import { MatrixRTCSessionManager, MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc/MatrixRTCSessionManager";
|
||||
import { makeMockRoom, membershipTemplate, mockRoomState } from "./mocks";
|
||||
import { ClientEvent, EventTimeline, MatrixClient, type Room, RoomStateEvent } from "../../../src";
|
||||
import { MatrixRTCSessionManager, MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc";
|
||||
import { makeMockRoom, type MembershipData, membershipTemplate, mockRoomState, mockRTCEvent } from "./mocks";
|
||||
import { logger } from "../../../src/logger";
|
||||
import { flushPromises } from "../../test-utils/flushPromises";
|
||||
|
||||
describe("MatrixRTCSessionManager", () => {
|
||||
let client: MatrixClient;
|
||||
describe.each([{ eventKind: "sticky" }, { eventKind: "memberState" }])(
|
||||
"MatrixRTCSessionManager ($eventKind)",
|
||||
({ eventKind }) => {
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
client = new MatrixClient({ baseUrl: "base_url" });
|
||||
client.matrixRTC.start();
|
||||
});
|
||||
async function sendLeaveMembership(room: Room, membershipData: MembershipData[]): Promise<void> {
|
||||
if (eventKind === "memberState") {
|
||||
mockRoomState(room, [{ user_id: membershipTemplate.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 });
|
||||
client.emit(ClientEvent.Event, mockRTCEvent(membershipData[0], room.roomId, 10000));
|
||||
}
|
||||
await flushPromises();
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
client.stopClient();
|
||||
client.matrixRTC.stop();
|
||||
});
|
||||
beforeEach(() => {
|
||||
client = new MatrixClient({ baseUrl: "base_url" });
|
||||
client.matrixRTC.start();
|
||||
});
|
||||
|
||||
it("Fires event when session starts", () => {
|
||||
const onStarted = jest.fn();
|
||||
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
||||
afterEach(() => {
|
||||
client.stopClient();
|
||||
client.matrixRTC.stop();
|
||||
});
|
||||
|
||||
try {
|
||||
const room1 = makeMockRoom([membershipTemplate]);
|
||||
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||
it("Fires event when session starts", async () => {
|
||||
const onStarted = vi.fn();
|
||||
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
||||
|
||||
try {
|
||||
const room1 = makeMockRoom([membershipTemplate], eventKind === "sticky");
|
||||
vi.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||
|
||||
client.emit(ClientEvent.Room, room1);
|
||||
await flushPromises();
|
||||
expect(onStarted).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
|
||||
} finally {
|
||||
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
||||
}
|
||||
});
|
||||
|
||||
it("Doesn't fire event if unrelated sessions starts", () => {
|
||||
const onStarted = vi.fn();
|
||||
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
||||
|
||||
try {
|
||||
const room1 = makeMockRoom([{ ...membershipTemplate, application: "m.other" }], eventKind === "sticky");
|
||||
vi.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||
|
||||
client.emit(ClientEvent.Room, room1);
|
||||
expect(onStarted).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
||||
}
|
||||
});
|
||||
|
||||
it("Fires event when session ends", async () => {
|
||||
const onEnded = vi.fn();
|
||||
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
|
||||
const membershipData: MembershipData[] = [membershipTemplate];
|
||||
const room1 = makeMockRoom(membershipData, eventKind === "sticky");
|
||||
vi.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||
vi.spyOn(client, "getRoom").mockReturnValue(room1);
|
||||
client.emit(ClientEvent.Room, room1);
|
||||
await flushPromises();
|
||||
await sendLeaveMembership(room1, membershipData);
|
||||
|
||||
expect(onEnded).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
|
||||
});
|
||||
|
||||
it("Fires correctly with custom sessionDescription", async () => {
|
||||
const onStarted = vi.fn();
|
||||
const onEnded = vi.fn();
|
||||
// create a session manager with a custom session description
|
||||
const sessionManager = new MatrixRTCSessionManager(logger, client, {
|
||||
id: "test",
|
||||
application: "m.notCall",
|
||||
});
|
||||
|
||||
// manually start the session manager (its not the default one started by the client)
|
||||
sessionManager.start();
|
||||
sessionManager.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
|
||||
sessionManager.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
||||
|
||||
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");
|
||||
vi.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||
client.emit(ClientEvent.Room, room1);
|
||||
await flushPromises();
|
||||
expect(onStarted).not.toHaveBeenCalled();
|
||||
onStarted.mockClear();
|
||||
|
||||
// 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");
|
||||
vi.spyOn(client, "getRooms").mockReturnValue([room1, room2]);
|
||||
client.emit(ClientEvent.Room, room2);
|
||||
await flushPromises();
|
||||
expect(onStarted).toHaveBeenCalled();
|
||||
onStarted.mockClear();
|
||||
|
||||
// Stop room1's RTC session. Tracked.
|
||||
vi.spyOn(client, "getRoom").mockReturnValue(room2);
|
||||
await 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();
|
||||
} finally {
|
||||
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
||||
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
|
||||
}
|
||||
});
|
||||
|
||||
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 room1 = makeMockRoom(membership, eventKind === "sticky");
|
||||
vi.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||
vi.spyOn(client, "getRoom").mockReturnValue(room1);
|
||||
|
||||
client.emit(ClientEvent.Room, room1);
|
||||
expect(onStarted).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
|
||||
} finally {
|
||||
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
||||
}
|
||||
});
|
||||
|
||||
it("Doesn't fire event if unrelated sessions starts", () => {
|
||||
const onStarted = jest.fn();
|
||||
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
||||
await sendLeaveMembership(room1, membership);
|
||||
|
||||
try {
|
||||
const room1 = makeMockRoom([{ ...membershipTemplate, application: "m.other" }]);
|
||||
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||
|
||||
client.emit(ClientEvent.Room, room1);
|
||||
expect(onStarted).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
||||
}
|
||||
});
|
||||
|
||||
it("Fires event when session ends", () => {
|
||||
const onEnded = jest.fn();
|
||||
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
|
||||
const room1 = makeMockRoom([membershipTemplate]);
|
||||
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room1);
|
||||
|
||||
client.emit(ClientEvent.Room, room1);
|
||||
|
||||
mockRoomState(room1, [{ user_id: membershipTemplate.user_id }]);
|
||||
|
||||
const roomState = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
||||
const membEvent = roomState.getStateEvents("org.matrix.msc3401.call.member")[0];
|
||||
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
|
||||
|
||||
expect(onEnded).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
|
||||
});
|
||||
|
||||
it("Fires correctly with for with custom sessionDescription", () => {
|
||||
const onStarted = jest.fn();
|
||||
const onEnded = jest.fn();
|
||||
// create a session manager with a custom session description
|
||||
const sessionManager = new MatrixRTCSessionManager(logger, client, { id: "test", application: "m.notCall" });
|
||||
|
||||
// manually start the session manager (its not the default one started by the client)
|
||||
sessionManager.start();
|
||||
sessionManager.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
|
||||
sessionManager.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
||||
|
||||
try {
|
||||
const room1 = makeMockRoom([{ ...membershipTemplate, application: "m.other" }]);
|
||||
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||
|
||||
client.emit(ClientEvent.Room, room1);
|
||||
expect(onStarted).not.toHaveBeenCalled();
|
||||
onStarted.mockClear();
|
||||
|
||||
const room2 = makeMockRoom([{ ...membershipTemplate, application: "m.notCall", call_id: "test" }]);
|
||||
jest.spyOn(client, "getRooms").mockReturnValue([room1, room2]);
|
||||
|
||||
client.emit(ClientEvent.Room, room2);
|
||||
expect(onStarted).toHaveBeenCalled();
|
||||
onStarted.mockClear();
|
||||
|
||||
mockRoomState(room2, [{ user_id: membershipTemplate.user_id }]);
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room2);
|
||||
|
||||
const roomState = room2.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
||||
const membEvent = roomState.getStateEvents("org.matrix.msc3401.call.member")[0];
|
||||
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
|
||||
expect(onEnded).toHaveBeenCalled();
|
||||
onEnded.mockClear();
|
||||
|
||||
mockRoomState(room1, [{ user_id: membershipTemplate.user_id }]);
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room1);
|
||||
|
||||
const roomStateOther = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
||||
const membEventOther = roomStateOther.getStateEvents("org.matrix.msc3401.call.member")[0];
|
||||
client.emit(RoomStateEvent.Events, membEventOther, roomStateOther, null);
|
||||
expect(onEnded).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
||||
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
|
||||
}
|
||||
});
|
||||
|
||||
it("Doesn't fire event if unrelated sessions ends", () => {
|
||||
const onEnded = jest.fn();
|
||||
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
|
||||
const room1 = makeMockRoom([{ ...membershipTemplate, application: "m.other_app" }]);
|
||||
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room1);
|
||||
|
||||
client.emit(ClientEvent.Room, room1);
|
||||
|
||||
mockRoomState(room1, [{ user_id: membershipTemplate.user_id }]);
|
||||
|
||||
const roomState = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
||||
const membEvent = roomState.getStateEvents("org.matrix.msc3401.call.member")[0];
|
||||
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
|
||||
|
||||
expect(onEnded).not.toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
|
||||
});
|
||||
});
|
||||
expect(onEnded).not.toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
MatrixError,
|
||||
UnsupportedDelayedEventsEndpointError,
|
||||
type Room,
|
||||
MAX_STICKY_DURATION_MS,
|
||||
} from "../../../src";
|
||||
import {
|
||||
MembershipManagerEvent,
|
||||
@@ -32,7 +33,7 @@ import {
|
||||
type LivekitFocusSelection,
|
||||
} from "../../../src/matrixrtc";
|
||||
import { makeMockClient, makeMockRoom, membershipTemplate, mockCallMembership, type MockClient } from "./mocks";
|
||||
import { MembershipManager } from "../../../src/matrixrtc/MembershipManager.ts";
|
||||
import { MembershipManager, StickyEventMembershipManager } from "../../../src/matrixrtc/MembershipManager.ts";
|
||||
|
||||
/**
|
||||
* Create a promise that will resolve once a mocked method is called.
|
||||
@@ -40,7 +41,7 @@ import { MembershipManager } from "../../../src/matrixrtc/MembershipManager.ts";
|
||||
* @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();
|
||||
@@ -50,7 +51,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();
|
||||
@@ -64,7 +65,7 @@ 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 };
|
||||
@@ -87,17 +88,22 @@ describe("MembershipManager", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
// Default to fake timers.
|
||||
jest.useFakeTimers();
|
||||
vi.useFakeTimers();
|
||||
client = makeMockClient("@alice:example.org", "AAAAAAA");
|
||||
room = makeMockRoom([membershipTemplate]);
|
||||
// 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.sendStateEvent as Mock<any>).mockResolvedValue(undefined);
|
||||
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.
|
||||
});
|
||||
|
||||
@@ -119,7 +125,9 @@ describe("MembershipManager", () => {
|
||||
it("sends a membership event and schedules delayed leave when joining a call", async () => {
|
||||
// Spys/Mocks
|
||||
|
||||
const updateDelayedEventHandle = createAsyncHandle<void>(client._unstable_updateDelayedEvent as Mock);
|
||||
const restartScheduledDelayedEventHandle = createAsyncHandle<void>(
|
||||
client._unstable_restartScheduledDelayedEvent,
|
||||
);
|
||||
|
||||
// Test
|
||||
const memberManager = new MembershipManager(undefined, room, client, callSession);
|
||||
@@ -135,12 +143,13 @@ describe("MembershipManager", () => {
|
||||
device_id: "AAAAAAA",
|
||||
expires: 14400000,
|
||||
foci_preferred: [focus],
|
||||
membershipID: "@alice:example.org:AAAAAAA",
|
||||
focus_active: focusActive,
|
||||
scope: "m.room",
|
||||
},
|
||||
"_@alice:example.org_AAAAAAA_m.call",
|
||||
);
|
||||
updateDelayedEventHandle.resolve?.();
|
||||
restartScheduledDelayedEventHandle.resolve?.();
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledWith(
|
||||
room.roomId,
|
||||
{ delay: 8000 },
|
||||
@@ -151,54 +160,17 @@ describe("MembershipManager", () => {
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("sends a rtc membership event when using `useRtcMemberFormat`", async () => {
|
||||
// Spys/Mocks
|
||||
|
||||
const updateDelayedEventHandle = createAsyncHandle<void>(client._unstable_updateDelayedEvent as Mock);
|
||||
|
||||
// Test
|
||||
const memberManager = new MembershipManager({ useRtcMemberFormat: true }, room, client, callSession);
|
||||
memberManager.join([], focus);
|
||||
// expects
|
||||
await waitForMockCall(client.sendStateEvent, Promise.resolve({ event_id: "id" }));
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||
room.roomId,
|
||||
"org.matrix.msc4143.rtc.member",
|
||||
{
|
||||
application: { type: "m.call" },
|
||||
member: {
|
||||
user_id: "@alice:example.org",
|
||||
id: "_@alice:example.org_AAAAAAA_m.call",
|
||||
device_id: "AAAAAAA",
|
||||
},
|
||||
slot_id: "m.call#",
|
||||
rtc_transports: [focus],
|
||||
versions: [],
|
||||
},
|
||||
"_@alice:example.org_AAAAAAA_m.call",
|
||||
);
|
||||
updateDelayedEventHandle.resolve?.();
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledWith(
|
||||
room.roomId,
|
||||
{ delay: 8000 },
|
||||
"org.matrix.msc4143.rtc.member",
|
||||
{},
|
||||
"_@alice:example.org_AAAAAAA_m.call",
|
||||
);
|
||||
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);
|
||||
const waitForUpdateDelaye = waitForMockCallOnce(
|
||||
client._unstable_updateDelayedEvent,
|
||||
const waitForRestartScheduledDelayedEvent = waitForMockCallOnce(
|
||||
client._unstable_restartScheduledDelayedEvent,
|
||||
Promise.reject(new MatrixError({ errcode: "M_NOT_FOUND" })),
|
||||
);
|
||||
memberManager.join([focus], focusActive);
|
||||
await waitForSendState;
|
||||
await waitForUpdateDelaye;
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
await waitForRestartScheduledDelayedEvent;
|
||||
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);
|
||||
@@ -211,9 +183,9 @@ 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 updatedDelayedEvent = waitForMockCall(client._unstable_updateDelayedEvent);
|
||||
const restartScheduledDelayedEvent = waitForMockCall(client._unstable_restartScheduledDelayedEvent);
|
||||
const sentDelayedState = waitForMockCall(
|
||||
client._unstable_sendDelayedStateEvent,
|
||||
Promise.resolve({
|
||||
@@ -228,7 +200,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);
|
||||
});
|
||||
@@ -238,7 +210,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);
|
||||
});
|
||||
@@ -253,7 +225,7 @@ describe("MembershipManager", () => {
|
||||
undefined,
|
||||
new Headers({ "Retry-After": "1" }),
|
||||
);
|
||||
(client.sendStateEvent as Mock).mockImplementationOnce(() => {
|
||||
vi.mocked(client.sendStateEvent).mockImplementationOnce(() => {
|
||||
resolve();
|
||||
return Promise.reject(error);
|
||||
});
|
||||
@@ -276,11 +248,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,
|
||||
@@ -292,6 +264,7 @@ describe("MembershipManager", () => {
|
||||
expires: 14400000,
|
||||
device_id: "AAAAAAA",
|
||||
foci_preferred: [focus],
|
||||
membershipID: "@alice:example.org:AAAAAAA",
|
||||
focus_active: focusActive,
|
||||
} satisfies SessionMembershipData,
|
||||
userStateKey,
|
||||
@@ -299,19 +272,21 @@ describe("MembershipManager", () => {
|
||||
await sentDelayedState;
|
||||
|
||||
// should have prepared the heartbeat to keep delaying the leave event while still connected
|
||||
await updatedDelayedEvent;
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1);
|
||||
await restartScheduledDelayedEvent;
|
||||
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_updateDelayedEvent).toHaveBeenCalledTimes(2);
|
||||
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);
|
||||
});
|
||||
@@ -320,7 +295,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?.(
|
||||
@@ -332,11 +307,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", () => {
|
||||
@@ -365,7 +340,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.
|
||||
@@ -377,17 +352,17 @@ describe("MembershipManager", () => {
|
||||
// (onRTCSessionMemberUpdate)
|
||||
// - Only then do we resolve the sending of the delayed event.
|
||||
// - We test that the manager acknowledges the leave and sends a new membership state event.
|
||||
(client._unstable_updateDelayedEvent as Mock<any>).mockRejectedValueOnce(
|
||||
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockRejectedValueOnce(
|
||||
new MatrixError({ errcode: "M_NOT_FOUND" }),
|
||||
);
|
||||
|
||||
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)]);
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -412,6 +387,7 @@ describe("MembershipManager", () => {
|
||||
device_id: "AAAAAAA",
|
||||
expires: 1234567,
|
||||
foci_preferred: [focus],
|
||||
membershipID: "@alice:example.org:AAAAAAA",
|
||||
focus_active: {
|
||||
focus_selection: "oldest_membership",
|
||||
type: "livekit",
|
||||
@@ -436,19 +412,19 @@ 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_updateDelayedEvent).toHaveBeenLastCalledWith("id", "send");
|
||||
expect(client._unstable_sendScheduledDelayedEvent).toHaveBeenLastCalledWith("id");
|
||||
expect(client.sendStateEvent).toHaveBeenCalled();
|
||||
});
|
||||
it("send leave event when leave is called and resolving delayed leave fails", async () => {
|
||||
const manager = new MembershipManager({}, room, client, callSession);
|
||||
manager.join([focus]);
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
(client._unstable_updateDelayedEvent as Mock<any>).mockRejectedValue("unknown");
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
(client._unstable_sendScheduledDelayedEvent as Mock<any>).mockRejectedValue("unknown");
|
||||
await manager.leave();
|
||||
|
||||
// We send a normal leave event since we failed using updateDelayedEvent with the "send" action.
|
||||
// We send a normal leave event since we failed using sendScheduledDelayedEvent.
|
||||
expect(client.sendStateEvent).toHaveBeenLastCalledWith(
|
||||
room.roomId,
|
||||
"org.matrix.msc3401.call.member",
|
||||
@@ -456,9 +432,9 @@ describe("MembershipManager", () => {
|
||||
"_@alice:example.org_AAAAAAA_m.call",
|
||||
);
|
||||
});
|
||||
it("does nothing if not joined", () => {
|
||||
it("does nothing if not joined", async () => {
|
||||
const manager = new MembershipManager({}, room, client, callSession);
|
||||
expect(async () => await manager.leave()).not.toThrow();
|
||||
await expect(manager.leave()).resolves.toBeTruthy();
|
||||
expect(client._unstable_sendDelayedStateEvent).not.toHaveBeenCalled();
|
||||
expect(client.sendStateEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -468,20 +444,26 @@ describe("MembershipManager", () => {
|
||||
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 vi.advanceTimersToNextTimerAsync();
|
||||
expect(client.sendStateEvent).not.toHaveBeenCalled();
|
||||
expect(client._unstable_sendDelayedStateEvent).not.toHaveBeenCalled();
|
||||
expect(client._unstable_updateDelayedEvent).not.toHaveBeenCalled();
|
||||
expect(client._unstable_cancelScheduledDelayedEvent).not.toHaveBeenCalled();
|
||||
expect(client._unstable_restartScheduledDelayedEvent).not.toHaveBeenCalled();
|
||||
expect(client._unstable_sendScheduledDelayedEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
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_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),
|
||||
@@ -491,53 +473,56 @@ describe("MembershipManager", () => {
|
||||
),
|
||||
]);
|
||||
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
|
||||
expect(client.sendStateEvent).not.toHaveBeenCalled();
|
||||
expect(client._unstable_sendDelayedStateEvent).not.toHaveBeenCalled();
|
||||
expect(client._unstable_updateDelayedEvent).not.toHaveBeenCalled();
|
||||
expect(client._unstable_cancelScheduledDelayedEvent).not.toHaveBeenCalled();
|
||||
expect(client._unstable_restartScheduledDelayedEvent).not.toHaveBeenCalled();
|
||||
expect(client._unstable_sendScheduledDelayedEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
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_updateDelayedEvent 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 vi.advanceTimersByTimeAsync(1);
|
||||
expect(client.sendStateEvent).toHaveBeenCalled();
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalled();
|
||||
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenCalled();
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
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_updateDelayedEvent 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_updateDelayedEvent as Mock<any>).mockRejectedValueOnce(
|
||||
(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 vi.advanceTimersByTimeAsync(10_000);
|
||||
await manager.onRTCSessionMemberUpdate([mockCallMembership(membershipTemplate, 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();
|
||||
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenCalled();
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalled();
|
||||
expect(manager.status).toBe(Status.Connected);
|
||||
});
|
||||
});
|
||||
@@ -552,22 +537,22 @@ 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
|
||||
// so it does not need a `advanceTimersByTime`
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1);
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(1);
|
||||
// TODO: Check that update delayed event is called with the correct HTTP request timeout
|
||||
// expect(client._unstable_updateDelayedEvent).toHaveBeenLastCalledWith("id", 10_000, { localTimeoutMs: 20_000 });
|
||||
// expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenLastCalledWith("id", 10_000, { localTimeoutMs: 20_000 });
|
||||
|
||||
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_updateDelayedEvent).toHaveBeenCalledTimes(i);
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(i);
|
||||
// TODO: Check that update delayed event is called with the correct HTTP request timeout
|
||||
// expect(client._unstable_updateDelayedEvent).toHaveBeenLastCalledWith("id", 10_000, { localTimeoutMs: 20_000 });
|
||||
// expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenLastCalledWith("id", 10_000, { localTimeoutMs: 20_000 });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -586,18 +571,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);
|
||||
});
|
||||
@@ -614,23 +601,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);
|
||||
@@ -655,7 +642,7 @@ describe("MembershipManager", () => {
|
||||
new Headers({ "Retry-After": "1" }),
|
||||
),
|
||||
);
|
||||
await jest.advanceTimersByTimeAsync(1000);
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
@@ -673,7 +660,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" });
|
||||
@@ -681,7 +668,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);
|
||||
@@ -701,13 +688,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);
|
||||
@@ -715,7 +702,7 @@ describe("MembershipManager", () => {
|
||||
});
|
||||
describe("retries sending update delayed leave event restart", () => {
|
||||
it("resends the initial check delayed update event", async () => {
|
||||
(client._unstable_updateDelayedEvent as Mock<any>).mockRejectedValue(
|
||||
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockRejectedValue(
|
||||
new MatrixError(
|
||||
{ errcode: "M_LIMIT_EXCEEDED" },
|
||||
429,
|
||||
@@ -728,18 +715,18 @@ describe("MembershipManager", () => {
|
||||
manager.join([focus], focusActive);
|
||||
|
||||
// Hit rate limit
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Hit second rate limit.
|
||||
await jest.advanceTimersByTimeAsync(1000);
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(2);
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Setup resolve
|
||||
(client._unstable_updateDelayedEvent as Mock<any>).mockResolvedValue(undefined);
|
||||
await jest.advanceTimersByTimeAsync(1000);
|
||||
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockResolvedValue(undefined);
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(3);
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(3);
|
||||
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -747,7 +734,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" },
|
||||
@@ -761,14 +748,14 @@ 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();
|
||||
(client._unstable_updateDelayedEvent as Mock<any>).mockRejectedValue(
|
||||
const delayEventRestartError = vi.fn();
|
||||
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockRejectedValue(
|
||||
new MatrixError(
|
||||
{ errcode: "M_LIMIT_EXCEEDED" },
|
||||
429,
|
||||
@@ -781,12 +768,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);
|
||||
@@ -795,7 +782,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 },
|
||||
@@ -806,7 +793,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(
|
||||
@@ -815,13 +802,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();
|
||||
@@ -837,43 +824,43 @@ 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 {
|
||||
// Let the scheduler run one iteration so that we can send the join state event
|
||||
await waitForMockCall(client._unstable_updateDelayedEvent);
|
||||
await waitForMockCall(client._unstable_restartScheduledDelayedEvent);
|
||||
|
||||
// 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_updateDelayedEvent = 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_updateDelayedEvent).toHaveBeenCalledTimes(1);
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(1);
|
||||
|
||||
await jest.advanceTimersByTimeAsync(4999);
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(3);
|
||||
await vi.advanceTimersByTimeAsync(4999);
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(3);
|
||||
expect(probablyLeftEmit).not.toHaveBeenCalledWith(true);
|
||||
|
||||
// Reset mocks before we setup the next delayed event restart by advancing the timers 1 more ms.
|
||||
(client._unstable_updateDelayedEvent as Mock<any>).mockResolvedValue({});
|
||||
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockResolvedValue({});
|
||||
|
||||
// Emit after 10s
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(4);
|
||||
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);
|
||||
@@ -886,6 +873,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.
|
||||
@@ -903,7 +891,7 @@ describe("MembershipManager", () => {
|
||||
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");
|
||||
});
|
||||
@@ -921,6 +909,69 @@ describe("MembershipManager", () => {
|
||||
expect(client.sendStateEvent).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("StickyEventMembershipManager", () => {
|
||||
beforeEach(() => {
|
||||
// Provide a default mock that is like the default "non error" server behaviour.
|
||||
(client._unstable_sendStickyDelayedEvent as Mock<any>).mockResolvedValue({ delay_id: "id" });
|
||||
(client._unstable_sendStickyEvent as Mock<any>).mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
describe("join()", () => {
|
||||
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,
|
||||
);
|
||||
const memberManager = new StickyEventMembershipManager(
|
||||
undefined,
|
||||
room,
|
||||
client,
|
||||
callSession,
|
||||
"@alice:example.org:AAAAAAA_m.call",
|
||||
);
|
||||
|
||||
memberManager.join([], focus);
|
||||
|
||||
await waitForMockCall(client._unstable_sendStickyEvent, Promise.resolve({ event_id: "id" }));
|
||||
// Test we sent the initial join
|
||||
expect(client._unstable_sendStickyEvent).toHaveBeenCalledWith(
|
||||
room.roomId,
|
||||
3600000,
|
||||
null,
|
||||
"org.matrix.msc4143.rtc.member",
|
||||
{
|
||||
application: { type: "m.call" },
|
||||
member: {
|
||||
user_id: "@alice:example.org",
|
||||
id: "@alice:example.org:AAAAAAA_m.call",
|
||||
device_id: "AAAAAAA",
|
||||
},
|
||||
slot_id: "m.call#",
|
||||
rtc_transports: [focus],
|
||||
versions: [],
|
||||
msc4354_sticky_key: "@alice:example.org:AAAAAAA_m.call",
|
||||
},
|
||||
);
|
||||
restartScheduledDelayedEventHandle.resolve?.();
|
||||
|
||||
// Ensure we have sent the delayed disconnect event.
|
||||
expect(client._unstable_sendStickyDelayedEvent).toHaveBeenCalledWith(
|
||||
room.roomId,
|
||||
MAX_STICKY_DURATION_MS,
|
||||
{ delay: 8000 },
|
||||
null,
|
||||
"org.matrix.msc4143.rtc.member",
|
||||
{
|
||||
msc4354_sticky_key: "@alice:example.org:AAAAAAA_m.call",
|
||||
},
|
||||
);
|
||||
// ..once
|
||||
expect(client._unstable_sendStickyDelayedEvent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("Should prefix log with MembershipManager used", () => {
|
||||
@@ -929,7 +980,7 @@ it("Should prefix log with MembershipManager used", () => {
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,187 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Mocked } from "jest-mock";
|
||||
|
||||
import { makeKey, makeMockEvent, makeMockRoom } from "./mocks";
|
||||
import { EventType, type IRoomTimelineData, type Room, RoomEvent, type MatrixClient } from "../../../src";
|
||||
import { ToDeviceKeyTransport } from "../../../src/matrixrtc/ToDeviceKeyTransport.ts";
|
||||
import {
|
||||
getMockClientWithEventEmitter,
|
||||
mockClientMethodsEvents,
|
||||
mockClientMethodsUser,
|
||||
} from "../../test-utils/client.ts";
|
||||
import { type ParticipantDeviceInfo, type Statistics } from "../../../src/matrixrtc";
|
||||
import { KeyTransportEvents } from "../../../src/matrixrtc/IKeyTransport.ts";
|
||||
import { type Logger } from "../../../src/logger.ts";
|
||||
import { RoomAndToDeviceEvents, RoomAndToDeviceTransport } from "../../../src/matrixrtc/RoomAndToDeviceKeyTransport.ts";
|
||||
import { RoomKeyTransport } from "../../../src/matrixrtc/RoomKeyTransport.ts";
|
||||
|
||||
describe("RoomAndToDeviceTransport", () => {
|
||||
const roomId = "!room:id";
|
||||
|
||||
let mockClient: Mocked<MatrixClient>;
|
||||
let statistics: Statistics;
|
||||
let mockLogger: Mocked<Logger>;
|
||||
let transport: RoomAndToDeviceTransport;
|
||||
let mockRoom: Room;
|
||||
let sendEventMock: jest.Mock;
|
||||
let roomKeyTransport: RoomKeyTransport;
|
||||
let toDeviceKeyTransport: ToDeviceKeyTransport;
|
||||
let toDeviceSendKeySpy: jest.SpyInstance;
|
||||
let roomSendKeySpy: jest.SpyInstance;
|
||||
beforeEach(() => {
|
||||
sendEventMock = jest.fn();
|
||||
mockClient = getMockClientWithEventEmitter({
|
||||
encryptAndSendToDevice: jest.fn(),
|
||||
getDeviceId: jest.fn().mockReturnValue("MYDEVICE"),
|
||||
...mockClientMethodsEvents(),
|
||||
...mockClientMethodsUser("@alice:example.org"),
|
||||
sendEvent: sendEventMock,
|
||||
});
|
||||
mockRoom = makeMockRoom([]);
|
||||
mockLogger = {
|
||||
debug: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
getChild: jest.fn(),
|
||||
} as unknown as Mocked<Logger>;
|
||||
mockLogger.getChild.mockReturnValue(mockLogger);
|
||||
statistics = {
|
||||
counters: {
|
||||
roomEventEncryptionKeysSent: 0,
|
||||
roomEventEncryptionKeysReceived: 0,
|
||||
},
|
||||
totals: {
|
||||
roomEventEncryptionKeysReceivedTotalAge: 0,
|
||||
},
|
||||
};
|
||||
roomKeyTransport = new RoomKeyTransport(mockRoom, mockClient, statistics);
|
||||
toDeviceKeyTransport = new ToDeviceKeyTransport(
|
||||
"@alice:example.org",
|
||||
"MYDEVICE",
|
||||
mockRoom.roomId,
|
||||
mockClient,
|
||||
statistics,
|
||||
);
|
||||
transport = new RoomAndToDeviceTransport(toDeviceKeyTransport, roomKeyTransport, mockLogger);
|
||||
toDeviceSendKeySpy = jest.spyOn(toDeviceKeyTransport, "sendKey");
|
||||
roomSendKeySpy = jest.spyOn(roomKeyTransport, "sendKey");
|
||||
});
|
||||
|
||||
it("should enable to device transport when starting", () => {
|
||||
transport.start();
|
||||
expect(transport.enabled.room).toBeFalsy();
|
||||
expect(transport.enabled.toDevice).toBeTruthy();
|
||||
});
|
||||
it("only sends to device keys when sending a key", async () => {
|
||||
transport.start();
|
||||
await transport.sendKey("1235", 0, [
|
||||
{ userId: "@alice:example.org", deviceId: "ALICEDEVICE", membershipTs: 1234 },
|
||||
]);
|
||||
expect(toDeviceSendKeySpy).toHaveBeenCalledTimes(1);
|
||||
expect(roomSendKeySpy).toHaveBeenCalledTimes(0);
|
||||
expect(transport.enabled.room).toBeFalsy();
|
||||
expect(transport.enabled.toDevice).toBeTruthy();
|
||||
});
|
||||
|
||||
it("enables room transport and disables to device transport when receiving a room key", async () => {
|
||||
transport.start();
|
||||
const onNewKeyFromTransport = jest.fn();
|
||||
const onTransportEnabled = jest.fn();
|
||||
transport.on(KeyTransportEvents.ReceivedKeys, onNewKeyFromTransport);
|
||||
transport.on(RoomAndToDeviceEvents.EnabledTransportsChanged, onTransportEnabled);
|
||||
mockRoom.emit(
|
||||
RoomEvent.Timeline,
|
||||
makeMockEvent(EventType.CallEncryptionKeysPrefix, "@bob:example.org", roomId, {
|
||||
call_id: "",
|
||||
keys: [makeKey(0, "testKey")],
|
||||
sent_ts: Date.now(),
|
||||
device_id: "AAAAAAA",
|
||||
}),
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
{} as IRoomTimelineData,
|
||||
);
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
expect(transport.enabled.room).toBeTruthy();
|
||||
expect(transport.enabled.toDevice).toBeFalsy();
|
||||
|
||||
await transport.sendKey("1235", 0, [
|
||||
{ userId: "@alice:example.org", deviceId: "AlICEDEV", membershipTs: 1234 },
|
||||
]);
|
||||
expect(sendEventMock).toHaveBeenCalledTimes(1);
|
||||
expect(roomSendKeySpy).toHaveBeenCalledTimes(1);
|
||||
expect(toDeviceSendKeySpy).toHaveBeenCalledTimes(0);
|
||||
expect(onTransportEnabled).toHaveBeenCalledWith({ toDevice: false, room: true });
|
||||
});
|
||||
|
||||
it("enables room transport and disables to device transport on widget driver error", async () => {
|
||||
mockClient.encryptAndSendToDevice.mockRejectedValue({
|
||||
message:
|
||||
"unknown variant `send_to_device`, expected one of `supported_api_versions`, `content_loaded`, `get_openid`, `org.matrix.msc2876.read_events`, `send_event`, `org.matrix.msc4157.update_delayed_event` at line 1 column 22",
|
||||
});
|
||||
|
||||
transport.start();
|
||||
const membership: ParticipantDeviceInfo = {
|
||||
userId: "@alice:example.org",
|
||||
deviceId: "ALICEDEVICE",
|
||||
membershipTs: 1234,
|
||||
};
|
||||
const onTransportEnabled = jest.fn();
|
||||
transport.on(RoomAndToDeviceEvents.EnabledTransportsChanged, onTransportEnabled);
|
||||
|
||||
// We start with toDevice transport enabled
|
||||
expect(transport.enabled.room).toBeFalsy();
|
||||
expect(transport.enabled.toDevice).toBeTruthy();
|
||||
|
||||
await transport.sendKey("1235", 0, [membership]);
|
||||
|
||||
// We switched transport, now room transport is enabled
|
||||
expect(onTransportEnabled).toHaveBeenCalledWith({ toDevice: false, room: true });
|
||||
expect(transport.enabled.room).toBeTruthy();
|
||||
expect(transport.enabled.toDevice).toBeFalsy();
|
||||
|
||||
// sanity check that we called the failang to device send key.
|
||||
expect(toDeviceKeyTransport.sendKey).toHaveBeenCalledWith("1235", 0, [membership]);
|
||||
expect(toDeviceKeyTransport.sendKey).toHaveBeenCalledTimes(1);
|
||||
// We re-sent the key via the room transport
|
||||
expect(roomKeyTransport.sendKey).toHaveBeenCalledWith("1235", 0, [membership]);
|
||||
expect(roomKeyTransport.sendKey).toHaveBeenCalledTimes(1);
|
||||
|
||||
mockClient.encryptAndSendToDevice.mockRestore();
|
||||
});
|
||||
|
||||
it("does log that it did nothing when disabled", () => {
|
||||
transport.start();
|
||||
const onNewKeyFromTransport = jest.fn();
|
||||
const onTransportEnabled = jest.fn();
|
||||
transport.on(KeyTransportEvents.ReceivedKeys, onNewKeyFromTransport);
|
||||
transport.on(RoomAndToDeviceEvents.EnabledTransportsChanged, onTransportEnabled);
|
||||
|
||||
transport.setEnabled({ toDevice: false, room: false });
|
||||
const dateNow = Date.now();
|
||||
roomKeyTransport.emit(KeyTransportEvents.ReceivedKeys, "user", "device", "roomKey", 0, dateNow);
|
||||
toDeviceKeyTransport.emit(KeyTransportEvents.ReceivedKeys, "user", "device", "toDeviceKey", 0, Date.now());
|
||||
|
||||
expect(mockLogger.debug).toHaveBeenCalledWith("To Device transport is disabled, ignoring received keys");
|
||||
// for room key transport we will never get a disabled message because its will always just turn on
|
||||
expect(onTransportEnabled).toHaveBeenNthCalledWith(1, { toDevice: false, room: false });
|
||||
expect(onTransportEnabled).toHaveBeenNthCalledWith(2, { toDevice: false, room: true });
|
||||
expect(onNewKeyFromTransport).toHaveBeenCalledTimes(1);
|
||||
expect(onNewKeyFromTransport).toHaveBeenCalledWith("user", "device", "roomKey", 0, dateNow);
|
||||
});
|
||||
});
|
||||
@@ -14,12 +14,13 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Mocked } from "vitest";
|
||||
|
||||
import { makeMockEvent, makeMockRoom, membershipTemplate, makeKey } from "./mocks";
|
||||
import { RoomKeyTransport } from "../../../src/matrixrtc/RoomKeyTransport";
|
||||
import { KeyTransportEvents } from "../../../src/matrixrtc/IKeyTransport";
|
||||
import { EventType, MatrixClient, RoomEvent } from "../../../src";
|
||||
import { type IRoomTimelineData, MatrixEvent, type Room } from "../../../src";
|
||||
import type { Mocked } from "jest-mock";
|
||||
import type { Logger } from "../../../src/logger.ts";
|
||||
|
||||
describe("RoomKeyTransport", () => {
|
||||
@@ -30,13 +31,13 @@ describe("RoomKeyTransport", () => {
|
||||
let transport: RoomKeyTransport;
|
||||
let mockLogger: Mocked<Logger>;
|
||||
|
||||
const onCallEncryptionMock = jest.fn();
|
||||
const onCallEncryptionMock = vi.fn();
|
||||
beforeEach(() => {
|
||||
onCallEncryptionMock.mockReset();
|
||||
mockLogger = {
|
||||
debug: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
info: jest.fn(),
|
||||
debug: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
info: vi.fn(),
|
||||
} as unknown as Mocked<Logger>;
|
||||
|
||||
const statistics = {
|
||||
@@ -52,7 +53,7 @@ describe("RoomKeyTransport", () => {
|
||||
client = new MatrixClient({ baseUrl: "base_url" });
|
||||
client.matrixRTC.start();
|
||||
transport = new RoomKeyTransport(room, client, statistics, {
|
||||
getChild: jest.fn().mockReturnValue(mockLogger),
|
||||
getChild: vi.fn().mockReturnValue(mockLogger),
|
||||
} as unknown as Mocked<Logger>);
|
||||
transport.on(KeyTransportEvents.ReceivedKeys, (...p) => {
|
||||
onCallEncryptionMock(...p);
|
||||
@@ -81,9 +82,9 @@ describe("RoomKeyTransport", () => {
|
||||
|
||||
describe("event decryption", () => {
|
||||
it("Retries decryption and processes success", async () => {
|
||||
jest.useFakeTimers();
|
||||
vi.useFakeTimers();
|
||||
let isDecryptionFailure = true;
|
||||
client.decryptEventIfNeeded = jest
|
||||
client.decryptEventIfNeeded = vi
|
||||
.fn()
|
||||
.mockReturnValueOnce(Promise.resolve())
|
||||
.mockImplementation(() => {
|
||||
@@ -98,7 +99,7 @@ describe("RoomKeyTransport", () => {
|
||||
sent_ts: Date.now(),
|
||||
device_id: "AAAAAAA",
|
||||
}),
|
||||
{ isDecryptionFailure: jest.fn().mockImplementation(() => isDecryptionFailure) },
|
||||
{ isDecryptionFailure: vi.fn().mockImplementation(() => isDecryptionFailure) },
|
||||
);
|
||||
room.emit(RoomEvent.Timeline, timelineEvent, undefined, undefined, false, {} as IRoomTimelineData);
|
||||
|
||||
@@ -106,18 +107,18 @@ describe("RoomKeyTransport", () => {
|
||||
expect(onCallEncryptionMock).toHaveBeenCalledTimes(0);
|
||||
|
||||
// should retry after one second:
|
||||
await jest.advanceTimersByTimeAsync(1500);
|
||||
await vi.advanceTimersByTimeAsync(1500);
|
||||
|
||||
expect(client.decryptEventIfNeeded).toHaveBeenCalledTimes(2);
|
||||
expect(onCallEncryptionMock).toHaveBeenCalledTimes(1);
|
||||
jest.useRealTimers();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("Retries decryption and processes failure", async () => {
|
||||
try {
|
||||
jest.useFakeTimers();
|
||||
const onCallEncryptionMock = jest.fn();
|
||||
client.decryptEventIfNeeded = jest.fn().mockReturnValue(Promise.resolve());
|
||||
vi.useFakeTimers();
|
||||
const onCallEncryptionMock = vi.fn();
|
||||
client.decryptEventIfNeeded = vi.fn().mockReturnValue(Promise.resolve());
|
||||
|
||||
const timelineEvent = Object.assign(
|
||||
makeMockEvent(EventType.CallEncryptionKeysPrefix, "@mock:user.example", "!room:id", {
|
||||
@@ -126,7 +127,7 @@ describe("RoomKeyTransport", () => {
|
||||
sent_ts: Date.now(),
|
||||
device_id: "AAAAAAA",
|
||||
}),
|
||||
{ isDecryptionFailure: jest.fn().mockReturnValue(true) },
|
||||
{ isDecryptionFailure: vi.fn().mockReturnValue(true) },
|
||||
);
|
||||
|
||||
room.emit(RoomEvent.Timeline, timelineEvent, undefined, undefined, false, {} as IRoomTimelineData);
|
||||
@@ -135,18 +136,18 @@ describe("RoomKeyTransport", () => {
|
||||
expect(onCallEncryptionMock).toHaveBeenCalledTimes(0);
|
||||
|
||||
// should retry after one second:
|
||||
await jest.advanceTimersByTimeAsync(1500);
|
||||
await vi.advanceTimersByTimeAsync(1500);
|
||||
|
||||
expect(client.decryptEventIfNeeded).toHaveBeenCalledTimes(2);
|
||||
expect(onCallEncryptionMock).toHaveBeenCalledTimes(0);
|
||||
|
||||
// doesn't retry again:
|
||||
await jest.advanceTimersByTimeAsync(1500);
|
||||
await vi.advanceTimersByTimeAsync(1500);
|
||||
|
||||
expect(client.decryptEventIfNeeded).toHaveBeenCalledTimes(2);
|
||||
expect(onCallEncryptionMock).toHaveBeenCalledTimes(0);
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Mocked } from "jest-mock";
|
||||
import { type Mocked } from "vitest";
|
||||
|
||||
import { makeMockEvent } from "./mocks";
|
||||
import { ClientEvent, EventType, type MatrixClient } from "../../../src";
|
||||
@@ -34,11 +34,11 @@ describe("ToDeviceKeyTransport", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = getMockClientWithEventEmitter({
|
||||
encryptAndSendToDevice: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||
encryptAndSendToDevice: vi.fn().mockImplementation(() => Promise.resolve()),
|
||||
});
|
||||
mockLogger = {
|
||||
debug: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
debug: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
} as unknown as Mocked<Logger>;
|
||||
statistics = {
|
||||
counters: {
|
||||
@@ -50,9 +50,15 @@ describe("ToDeviceKeyTransport", () => {
|
||||
},
|
||||
};
|
||||
|
||||
transport = new ToDeviceKeyTransport("@alice:example.org", "MYDEVICE", roomId, mockClient, statistics, {
|
||||
getChild: jest.fn().mockReturnValue(mockLogger),
|
||||
} as unknown as Mocked<Logger>);
|
||||
transport = new ToDeviceKeyTransport(
|
||||
{ userId: "@alice:example.org", deviceId: "MYDEVICE", memberId: "@alice:example.org:MYDEVICE" },
|
||||
roomId,
|
||||
mockClient,
|
||||
statistics,
|
||||
{
|
||||
getChild: vi.fn().mockReturnValue(mockLogger),
|
||||
} as unknown as Mocked<Logger>,
|
||||
);
|
||||
});
|
||||
|
||||
it("should send my keys on via to device", async () => {
|
||||
@@ -81,6 +87,7 @@ describe("ToDeviceKeyTransport", () => {
|
||||
},
|
||||
member: {
|
||||
claimed_device_id: "MYDEVICE",
|
||||
id: "@alice:example.org:MYDEVICE",
|
||||
},
|
||||
room_id: roomId,
|
||||
session: {
|
||||
@@ -102,8 +109,13 @@ describe("ToDeviceKeyTransport", () => {
|
||||
keyBase64Encoded: string;
|
||||
index: number;
|
||||
}>();
|
||||
transport.on(KeyTransportEvents.ReceivedKeys, (userId, deviceId, keyBase64Encoded, index, timestamp) => {
|
||||
receivedKeyResolvers.resolve({ userId, deviceId, keyBase64Encoded, index });
|
||||
transport.on(KeyTransportEvents.ReceivedKeys, (membership, keyBase64Encoded, index, _timestamp) => {
|
||||
receivedKeyResolvers.resolve({
|
||||
userId: membership.userId,
|
||||
deviceId: membership.deviceId,
|
||||
keyBase64Encoded,
|
||||
index,
|
||||
});
|
||||
});
|
||||
transport.start();
|
||||
|
||||
|
||||
@@ -15,13 +15,22 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "stream";
|
||||
import { type Mocked, type MockedObject } from "vitest";
|
||||
|
||||
import { EventType, type Room, RoomEvent, type MatrixClient, type MatrixEvent } from "../../../src";
|
||||
import { CallMembership, type SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
|
||||
import { CallMembership, type SessionMembershipData } from "../../../src/matrixrtc";
|
||||
import { secureRandomString } from "../../../src/randomstring";
|
||||
import { type CallMembershipIdentityParts } from "src/matrixrtc/EncryptionManager";
|
||||
import { logger } from "../../../src/logger.ts";
|
||||
|
||||
export type MembershipData = (SessionMembershipData | {}) & { user_id: string };
|
||||
|
||||
export const owmMemberIdentity: CallMembershipIdentityParts = {
|
||||
deviceId: "AAAAAAA",
|
||||
memberId: "@alice:example.org:AAAAAAA",
|
||||
userId: "@alice:example.org",
|
||||
};
|
||||
|
||||
export const membershipTemplate: SessionMembershipData & { user_id: string } = {
|
||||
application: "m.call",
|
||||
call_id: "",
|
||||
@@ -43,49 +52,68 @@ export const membershipTemplate: SessionMembershipData & { user_id: string } = {
|
||||
],
|
||||
};
|
||||
|
||||
export type MockClient = Pick<
|
||||
MatrixClient,
|
||||
| "getUserId"
|
||||
| "getDeviceId"
|
||||
| "sendEvent"
|
||||
| "sendStateEvent"
|
||||
| "_unstable_sendDelayedStateEvent"
|
||||
| "_unstable_updateDelayedEvent"
|
||||
| "cancelPendingEvent"
|
||||
export type MockClient = MockedObject<
|
||||
Pick<
|
||||
MatrixClient,
|
||||
| "getUserId"
|
||||
| "getDeviceId"
|
||||
| "sendEvent"
|
||||
| "sendStateEvent"
|
||||
| "_unstable_sendDelayedStateEvent"
|
||||
| "_unstable_updateDelayedEvent"
|
||||
| "_unstable_cancelScheduledDelayedEvent"
|
||||
| "_unstable_restartScheduledDelayedEvent"
|
||||
| "_unstable_sendScheduledDelayedEvent"
|
||||
| "_unstable_sendStickyEvent"
|
||||
| "_unstable_sendStickyDelayedEvent"
|
||||
| "cancelPendingEvent"
|
||||
>
|
||||
>;
|
||||
/**
|
||||
* Mocks a object that has all required methods for a MatrixRTC session client.
|
||||
*/
|
||||
export function makeMockClient(userId: string, deviceId: string): MockClient {
|
||||
return {
|
||||
getDeviceId: () => deviceId,
|
||||
getUserId: () => userId,
|
||||
sendEvent: jest.fn(),
|
||||
sendStateEvent: jest.fn(),
|
||||
cancelPendingEvent: jest.fn(),
|
||||
_unstable_updateDelayedEvent: jest.fn(),
|
||||
_unstable_sendDelayedStateEvent: jest.fn(),
|
||||
};
|
||||
getDeviceId: vi.fn(() => deviceId),
|
||||
getUserId: vi.fn(() => userId),
|
||||
sendEvent: vi.fn(),
|
||||
sendStateEvent: vi.fn(),
|
||||
cancelPendingEvent: vi.fn(),
|
||||
_unstable_updateDelayedEvent: vi.fn(),
|
||||
_unstable_cancelScheduledDelayedEvent: vi.fn(),
|
||||
_unstable_restartScheduledDelayedEvent: vi.fn(),
|
||||
_unstable_sendScheduledDelayedEvent: vi.fn(),
|
||||
_unstable_sendDelayedStateEvent: vi.fn(),
|
||||
_unstable_sendStickyEvent: vi.fn(),
|
||||
_unstable_sendStickyDelayedEvent: vi.fn(),
|
||||
} as MockClient;
|
||||
}
|
||||
|
||||
export function makeMockRoom(
|
||||
membershipData: MembershipData[],
|
||||
): Room & { emitTimelineEvent: (event: MatrixEvent) => void } {
|
||||
useStickyEvents = false,
|
||||
): Mocked<Room & { emitTimelineEvent: (event: MatrixEvent) => void }> {
|
||||
const roomId = secureRandomString(8);
|
||||
// Caching roomState here so it does not get recreated when calling `getLiveTimeline.getState()`
|
||||
const roomState = makeMockRoomState(membershipData, roomId);
|
||||
const roomState = makeMockRoomState(useStickyEvents ? [] : membershipData, roomId);
|
||||
const ts = Date.now();
|
||||
const room = Object.assign(new EventEmitter(), {
|
||||
roomId: roomId,
|
||||
hasMembershipState: jest.fn().mockReturnValue(true),
|
||||
getLiveTimeline: jest.fn().mockReturnValue({
|
||||
getState: jest.fn().mockReturnValue(roomState),
|
||||
hasMembershipState: vi.fn().mockReturnValue(true),
|
||||
getLiveTimeline: vi.fn().mockReturnValue({
|
||||
getState: vi.fn().mockReturnValue(roomState),
|
||||
}),
|
||||
getVersion: jest.fn().mockReturnValue("default"),
|
||||
}) as unknown as Room;
|
||||
getVersion: vi.fn().mockReturnValue("default"),
|
||||
_unstable_getStickyEvents: vi
|
||||
.fn()
|
||||
.mockImplementation(() =>
|
||||
useStickyEvents ? membershipData.map((m) => mockRTCEvent(m, roomId, 10000, ts)) : [],
|
||||
) as any,
|
||||
});
|
||||
return Object.assign(room, {
|
||||
emitTimelineEvent: (event: MatrixEvent) =>
|
||||
room.emit(RoomEvent.Timeline, event, room, undefined, false, {} as any),
|
||||
});
|
||||
}) as unknown as Mocked<Room & { emitTimelineEvent: (event: MatrixEvent) => void }>;
|
||||
}
|
||||
|
||||
function makeMockRoomState(membershipData: MembershipData[], roomId: string) {
|
||||
@@ -96,8 +124,8 @@ function makeMockRoomState(membershipData: MembershipData[], roomId: string) {
|
||||
});
|
||||
|
||||
return {
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
getStateEvents: (_: string, stateKey: string) => {
|
||||
if (stateKey !== undefined) return keysAndEvents.find(([k]) => k === stateKey)?.[1];
|
||||
return events;
|
||||
@@ -120,7 +148,7 @@ function makeMockRoomState(membershipData: MembershipData[], roomId: string) {
|
||||
}
|
||||
|
||||
export function mockRoomState(room: Room, membershipData: MembershipData[]): void {
|
||||
room.getLiveTimeline().getState = jest.fn().mockReturnValue(makeMockRoomState(membershipData, room.roomId));
|
||||
room.getLiveTimeline().getState = vi.fn().mockReturnValue(makeMockRoomState(membershipData, room.roomId));
|
||||
}
|
||||
|
||||
export function makeMockEvent(
|
||||
@@ -129,24 +157,48 @@ export function makeMockEvent(
|
||||
roomId: string | undefined,
|
||||
content: any,
|
||||
timestamp?: number,
|
||||
stateKey?: string,
|
||||
): MatrixEvent {
|
||||
return {
|
||||
getType: jest.fn().mockReturnValue(type),
|
||||
getContent: jest.fn().mockReturnValue(content),
|
||||
getSender: jest.fn().mockReturnValue(sender),
|
||||
getTs: jest.fn().mockReturnValue(timestamp ?? Date.now()),
|
||||
getRoomId: jest.fn().mockReturnValue(roomId),
|
||||
getId: jest.fn().mockReturnValue(secureRandomString(8)),
|
||||
isDecryptionFailure: jest.fn().mockReturnValue(false),
|
||||
getType: vi.fn().mockReturnValue(type),
|
||||
getContent: vi.fn().mockReturnValue(content),
|
||||
getSender: vi.fn().mockReturnValue(sender),
|
||||
getTs: vi.fn().mockReturnValue(timestamp ?? Date.now()),
|
||||
getRoomId: vi.fn().mockReturnValue(roomId),
|
||||
getId: vi.fn().mockReturnValue(secureRandomString(8)),
|
||||
getStateKey: vi.fn().mockReturnValue(stateKey),
|
||||
isDecryptionFailure: vi.fn().mockReturnValue(false),
|
||||
} as unknown as MatrixEvent;
|
||||
}
|
||||
|
||||
export function mockRTCEvent({ user_id: sender, ...membershipData }: MembershipData, roomId: string): MatrixEvent {
|
||||
return makeMockEvent(EventType.GroupCallMemberPrefix, sender, roomId, membershipData);
|
||||
export function mockRTCEvent(
|
||||
{ user_id: sender, ...membershipData }: MembershipData,
|
||||
roomId: string,
|
||||
stickyDuration?: number,
|
||||
timestamp?: number,
|
||||
): MatrixEvent {
|
||||
return {
|
||||
...makeMockEvent(
|
||||
stickyDuration !== undefined ? EventType.RTCMembership : EventType.GroupCallMemberPrefix,
|
||||
sender,
|
||||
roomId,
|
||||
membershipData,
|
||||
timestamp,
|
||||
!stickyDuration && "device_id" in membershipData ? `_${sender}_${membershipData.device_id}` : "",
|
||||
),
|
||||
unstableStickyExpiresAt: stickyDuration,
|
||||
} as unknown as MatrixEvent;
|
||||
}
|
||||
|
||||
export function mockCallMembership(membershipData: MembershipData, roomId: string): CallMembership {
|
||||
return new CallMembership(mockRTCEvent(membershipData, roomId), membershipData);
|
||||
export function mockCallMembership(
|
||||
membershipData: MembershipData,
|
||||
roomId: string,
|
||||
rtcBackendIdentity?: string,
|
||||
): CallMembership {
|
||||
const ev = mockRTCEvent(membershipData, roomId);
|
||||
vi.mocked(ev.getContent).mockReturnValue(membershipData);
|
||||
const data = CallMembership.membershipDataFromMatrixEvent(ev);
|
||||
return new CallMembership(ev, data, rtcBackendIdentity ?? "xx", logger);
|
||||
}
|
||||
|
||||
export function makeKey(id: number, key: string): { key: string; index: number } {
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type CallMembership } from "../../../src/matrixrtc";
|
||||
import { isMyMembership, parseCallNotificationContent } from "../../../src/matrixrtc/types";
|
||||
|
||||
describe("types", () => {
|
||||
describe("isMyMembership", () => {
|
||||
it("returns false if userId is different", () => {
|
||||
expect(
|
||||
isMyMembership(
|
||||
{ sender: "@alice:example.org", deviceId: "DEVICE" } as CallMembership,
|
||||
"@bob:example.org",
|
||||
"DEVICE",
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
it("returns true if userId and device is the same", () => {
|
||||
expect(
|
||||
isMyMembership(
|
||||
{ sender: "@alice:example.org", deviceId: "DEVICE" } as CallMembership,
|
||||
"@alice:example.org",
|
||||
"DEVICE",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("IRTCNotificationContent", () => {
|
||||
const validBase = Object.freeze({
|
||||
"m.mentions": { user_ids: [], room: true },
|
||||
"notification_type": "notification",
|
||||
"sender_ts": 123,
|
||||
"lifetime": 1000,
|
||||
});
|
||||
|
||||
it("parses valid content", () => {
|
||||
const res = parseCallNotificationContent({ ...validBase });
|
||||
expect(res).toMatchObject(validBase);
|
||||
});
|
||||
|
||||
it("caps lifetime to 90000ms", () => {
|
||||
const res = parseCallNotificationContent({ ...validBase, lifetime: 130000 });
|
||||
expect(res.lifetime).toBe(90000);
|
||||
});
|
||||
|
||||
it("throws on malformed m.mentions", () => {
|
||||
expect(() =>
|
||||
parseCallNotificationContent({
|
||||
...validBase,
|
||||
"m.mentions": "not an object",
|
||||
} as any),
|
||||
).toThrow("malformed m.mentions");
|
||||
});
|
||||
|
||||
it("throws on missing or invalid notification_type", () => {
|
||||
expect(() =>
|
||||
parseCallNotificationContent({
|
||||
...validBase,
|
||||
notification_type: undefined,
|
||||
} as any),
|
||||
).toThrow("Missing or invalid notification_type");
|
||||
|
||||
expect(() =>
|
||||
parseCallNotificationContent({
|
||||
...validBase,
|
||||
notification_type: 123 as any,
|
||||
} as any),
|
||||
).toThrow("Missing or invalid notification_type");
|
||||
});
|
||||
|
||||
it("throws on missing or invalid sender_ts", () => {
|
||||
expect(() =>
|
||||
parseCallNotificationContent({
|
||||
...validBase,
|
||||
sender_ts: undefined,
|
||||
} as any),
|
||||
).toThrow("Missing or invalid sender_ts");
|
||||
|
||||
expect(() =>
|
||||
parseCallNotificationContent({
|
||||
...validBase,
|
||||
sender_ts: "123" as any,
|
||||
} as any),
|
||||
).toThrow("Missing or invalid sender_ts");
|
||||
});
|
||||
|
||||
it("throws on missing or invalid lifetime", () => {
|
||||
expect(() =>
|
||||
parseCallNotificationContent({
|
||||
...validBase,
|
||||
lifetime: undefined,
|
||||
} as any),
|
||||
).toThrow("Missing or invalid lifetime");
|
||||
|
||||
expect(() =>
|
||||
parseCallNotificationContent({
|
||||
...validBase,
|
||||
lifetime: "1000" as any,
|
||||
} as any),
|
||||
).toThrow("Missing or invalid lifetime");
|
||||
});
|
||||
|
||||
it("accepts valid relation (m.reference)", () => {
|
||||
// Note: parseCallNotificationContent currently checks `relation.rel_type` rather than `m.relates_to`.
|
||||
const res = parseCallNotificationContent({
|
||||
...validBase,
|
||||
relation: { rel_type: "m.reference", event_id: "$ev" },
|
||||
} as any);
|
||||
expect(res).toBeTruthy();
|
||||
});
|
||||
|
||||
it("throws on invalid relation rel_type", () => {
|
||||
expect(() =>
|
||||
parseCallNotificationContent({
|
||||
...validBase,
|
||||
relation: { rel_type: "m.annotation", event_id: "$ev" },
|
||||
} as any),
|
||||
).toThrow("Invalid relation");
|
||||
});
|
||||
});
|
||||
@@ -85,7 +85,7 @@ describe("MSC3089Branch", () => {
|
||||
it("should be able to delete the file", async () => {
|
||||
const eventIdOrder = [fileEventId, fileEventId2];
|
||||
|
||||
const stateFn = jest
|
||||
const stateFn = vi
|
||||
.fn()
|
||||
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
|
||||
expect(roomId).toEqual(branchRoomId);
|
||||
@@ -98,7 +98,7 @@ describe("MSC3089Branch", () => {
|
||||
});
|
||||
client.sendStateEvent = stateFn;
|
||||
|
||||
const redactFn = jest.fn().mockImplementation((roomId: string, eventId: string) => {
|
||||
const redactFn = vi.fn().mockImplementation((roomId: string, eventId: string) => {
|
||||
expect(roomId).toEqual(branchRoomId);
|
||||
expect(eventId).toEqual(eventIdOrder[stateFn.mock.calls.length - 1]);
|
||||
|
||||
@@ -127,7 +127,7 @@ describe("MSC3089Branch", () => {
|
||||
it("should be able to change its name", async () => {
|
||||
const name = "My File.txt";
|
||||
indexEvent.getContent = () => ({ active: true, retained: true });
|
||||
const stateFn = jest
|
||||
const stateFn = vi
|
||||
.fn()
|
||||
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
|
||||
expect(roomId).toEqual(branchRoomId);
|
||||
@@ -183,7 +183,7 @@ describe("MSC3089Branch", () => {
|
||||
it("should be able to change its locked status", async () => {
|
||||
const locked = true;
|
||||
indexEvent.getContent = () => ({ active: true, retained: true });
|
||||
const stateFn = jest
|
||||
const stateFn = vi
|
||||
.fn()
|
||||
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
|
||||
expect(roomId).toEqual(branchRoomId);
|
||||
@@ -259,20 +259,20 @@ describe("MSC3089Branch", () => {
|
||||
const canaryAddl = { canary: true };
|
||||
indexEvent.getContent = () => ({ active: true, retained: true });
|
||||
const stateKeyOrder = [fileEventId2, fileEventId];
|
||||
const stateFn = jest
|
||||
const stateFn = vi
|
||||
.fn()
|
||||
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
|
||||
expect(roomId).toEqual(branchRoomId);
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value
|
||||
expect(stateKey).toEqual(stateKeyOrder[stateFn.mock.calls.length - 1]);
|
||||
if (stateKey === fileEventId) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
// eslint-disable-next-line @vitest/no-conditional-expect
|
||||
expect(content).toMatchObject({
|
||||
retained: true, // canary for copying state
|
||||
active: false,
|
||||
});
|
||||
} else if (stateKey === fileEventId2) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
// eslint-disable-next-line @vitest/no-conditional-expect
|
||||
expect(content).toMatchObject({
|
||||
active: true,
|
||||
version: 2,
|
||||
@@ -286,7 +286,7 @@ describe("MSC3089Branch", () => {
|
||||
});
|
||||
client.sendStateEvent = stateFn;
|
||||
|
||||
const createFn = jest
|
||||
const createFn = vi
|
||||
.fn()
|
||||
.mockImplementation((name: string, contents: ArrayBuffer, info: Partial<EncryptedFile>, addl: IContent) => {
|
||||
expect(name).toEqual(canaryName);
|
||||
|
||||
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type MockedFunction } from "vitest";
|
||||
|
||||
import { type IContent, type MatrixClient } from "../../../src";
|
||||
import { type Room } from "../../../src/models/room";
|
||||
import { MatrixEvent } from "../../../src/models/event";
|
||||
@@ -85,7 +87,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
|
||||
it("should support setting the name of the space", async () => {
|
||||
const newName = "NEW NAME";
|
||||
const fn = jest
|
||||
const fn = vi
|
||||
.fn()
|
||||
.mockImplementation((stateRoomId: string, eventType: EventType, content: any, stateKey: string) => {
|
||||
expect(stateRoomId).toEqual(roomId);
|
||||
@@ -101,7 +103,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
|
||||
it("should support inviting users to the space", async () => {
|
||||
const target = targetUser;
|
||||
const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => {
|
||||
const fn = vi.fn().mockImplementation((inviteRoomId: string, userId: string) => {
|
||||
expect(inviteRoomId).toEqual(roomId);
|
||||
expect(userId).toEqual(target);
|
||||
return Promise.resolve();
|
||||
@@ -113,7 +115,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
|
||||
it("should retry invites to the space", async () => {
|
||||
const target = targetUser;
|
||||
const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => {
|
||||
const fn = vi.fn().mockImplementation((inviteRoomId: string, userId: string) => {
|
||||
expect(inviteRoomId).toEqual(roomId);
|
||||
expect(userId).toEqual(target);
|
||||
if (fn.mock.calls.length === 1) return Promise.reject(new Error("Sample Failure"));
|
||||
@@ -126,7 +128,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
|
||||
it("should not retry invite permission errors", async () => {
|
||||
const target = targetUser;
|
||||
const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => {
|
||||
const fn = vi.fn().mockImplementation((inviteRoomId: string, userId: string) => {
|
||||
expect(inviteRoomId).toEqual(roomId);
|
||||
expect(userId).toEqual(target);
|
||||
return Promise.reject(new MatrixError({ errcode: "M_FORBIDDEN", error: "Sample Failure" }));
|
||||
@@ -140,7 +142,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
|
||||
it("should invite to subspaces", async () => {
|
||||
const target = targetUser;
|
||||
const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => {
|
||||
const fn = vi.fn().mockImplementation((inviteRoomId: string, userId: string) => {
|
||||
expect(inviteRoomId).toEqual(roomId);
|
||||
expect(userId).toEqual(target);
|
||||
return Promise.resolve();
|
||||
@@ -161,7 +163,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
|
||||
async function evaluatePowerLevels(pls: any, role: TreePermissions, expectedPl: number) {
|
||||
makePowerLevels(pls);
|
||||
const fn = jest
|
||||
const fn = vi
|
||||
.fn()
|
||||
.mockImplementation((stateRoomId: string, eventType: EventType, content: any, stateKey: string) => {
|
||||
expect(stateRoomId).toEqual(roomId);
|
||||
@@ -187,6 +189,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
expect(finalPermissions).toEqual(role);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should support setting Viewer permissions", () => {
|
||||
return evaluatePowerLevels(
|
||||
{
|
||||
@@ -202,6 +205,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should support setting Editor permissions", () => {
|
||||
return evaluatePowerLevels(
|
||||
{
|
||||
@@ -217,6 +221,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should support setting Owner permissions", () => {
|
||||
return evaluatePowerLevels(
|
||||
{
|
||||
@@ -232,6 +237,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should support demoting permissions", () => {
|
||||
return evaluatePowerLevels(
|
||||
{
|
||||
@@ -250,6 +256,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should support promoting permissions", () => {
|
||||
return evaluatePowerLevels(
|
||||
{
|
||||
@@ -268,14 +275,17 @@ describe("MSC3089TreeSpace", () => {
|
||||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should support defaults: Viewer", () => {
|
||||
return evaluatePowerLevels({}, TreePermissions.Viewer, 0);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should support defaults: Editor", () => {
|
||||
return evaluatePowerLevels({}, TreePermissions.Editor, 50);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should support defaults: Owner", () => {
|
||||
return evaluatePowerLevels({}, TreePermissions.Owner, 100);
|
||||
});
|
||||
@@ -294,11 +304,11 @@ describe("MSC3089TreeSpace", () => {
|
||||
}
|
||||
};
|
||||
client.getDomain = () => domain;
|
||||
const createFn = jest.fn().mockImplementation(async (name: string) => {
|
||||
const createFn = vi.fn().mockImplementation(async (name: string) => {
|
||||
expect(name).toEqual(subspaceName);
|
||||
return new MSC3089TreeSpace(client, subspaceId);
|
||||
});
|
||||
const sendStateFn = jest
|
||||
const sendStateFn = vi
|
||||
.fn()
|
||||
.mockImplementation(async (roomId: string, eventType: EventType, content: any, stateKey: string) => {
|
||||
expect([tree.roomId, subspaceId]).toContain(roomId);
|
||||
@@ -352,7 +362,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
};
|
||||
client.getRoom = () => ({}) as Room; // to appease the TreeSpace constructor
|
||||
|
||||
const getFn = jest.fn().mockImplementation((roomId: string) => {
|
||||
const getFn = vi.fn().mockImplementation((roomId: string) => {
|
||||
if (roomId === thirdChildRoom) {
|
||||
throw new Error("Mock not-a-space room case called (expected)");
|
||||
}
|
||||
@@ -389,10 +399,10 @@ describe("MSC3089TreeSpace", () => {
|
||||
});
|
||||
|
||||
it("should be able to delete itself", async () => {
|
||||
const delete1 = jest.fn().mockImplementation(() => Promise.resolve());
|
||||
const delete1 = vi.fn().mockImplementation(() => Promise.resolve());
|
||||
const subdir1 = { delete: delete1 } as any as MSC3089TreeSpace; // mock tested bits
|
||||
|
||||
const delete2 = jest.fn().mockImplementation(() => Promise.resolve());
|
||||
const delete2 = vi.fn().mockImplementation(() => Promise.resolve());
|
||||
const subdir2 = { delete: delete2 } as any as MSC3089TreeSpace; // mock tested bits
|
||||
|
||||
const joinMemberId = "@join:example.org";
|
||||
@@ -422,8 +432,8 @@ describe("MSC3089TreeSpace", () => {
|
||||
};
|
||||
|
||||
// These two functions are tested by input expectations, so no expectations in the function bodies
|
||||
const kickFn = jest.fn().mockImplementation((userId) => Promise.resolve());
|
||||
const leaveFn = jest.fn().mockImplementation(() => Promise.resolve());
|
||||
const kickFn = vi.fn().mockImplementation((userId) => Promise.resolve());
|
||||
const leaveFn = vi.fn().mockImplementation(() => Promise.resolve());
|
||||
client.kick = kickFn;
|
||||
client.leave = leaveFn;
|
||||
client.getUserId = () => selfUserId;
|
||||
@@ -449,7 +459,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
let parentRoom: Room;
|
||||
let childTrees: MSC3089TreeSpace[];
|
||||
let rooms: { [roomId: string]: Room };
|
||||
let clientSendStateFn: jest.MockedFunction<typeof client.sendStateEvent>;
|
||||
let clientSendStateFn: MockedFunction<typeof client.sendStateEvent>;
|
||||
const staticDomain = "static.example.org";
|
||||
|
||||
function addSubspace(roomId: string, createTs?: number, order?: string) {
|
||||
@@ -540,7 +550,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
(<any>tree).room = parentRoom; // override readonly
|
||||
client.getRoom = (r) => rooms[r ?? ""];
|
||||
|
||||
clientSendStateFn = jest
|
||||
clientSendStateFn = vi
|
||||
.fn()
|
||||
.mockImplementation((roomId: string, eventType: EventType, content: any, stateKey: string) => {
|
||||
expect(roomId).toEqual(tree.roomId);
|
||||
@@ -583,6 +593,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
await expect(tree.setOrder(2)).rejects.toThrow("Cannot set order of top level spaces currently");
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should return a stable order for unordered children", () => {
|
||||
const a = "!a:example.org";
|
||||
const b = "!b:example.org";
|
||||
@@ -598,6 +609,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
expectOrder(c, 2);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should return a stable order for ordered children", () => {
|
||||
const a = "!a:example.org";
|
||||
const b = "!b:example.org";
|
||||
@@ -613,6 +625,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
expectOrder(a, 2);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should return a stable order for partially ordered children", () => {
|
||||
const a = "!a:example.org";
|
||||
const b = "!b:example.org";
|
||||
@@ -631,6 +644,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
expectOrder(a, 2);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should return a stable order if the create event timestamps are the same", () => {
|
||||
const a = "!a:example.org";
|
||||
const b = "!b:example.org";
|
||||
@@ -646,6 +660,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
expectOrder(c, 2);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should return a stable order if there are no known create events", () => {
|
||||
const a = "!a:example.org";
|
||||
const b = "!b:example.org";
|
||||
@@ -902,7 +917,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
const fileName = "My File.txt";
|
||||
const fileContents = "This is a test file";
|
||||
|
||||
const uploadFn = jest.fn().mockImplementation((contents: Buffer, opts: any) => {
|
||||
const uploadFn = vi.fn().mockImplementation((contents: Buffer, opts: any) => {
|
||||
expect(contents.length).toEqual(fileContents.length);
|
||||
expect(opts).toMatchObject({
|
||||
includeFilename: false,
|
||||
@@ -911,7 +926,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
});
|
||||
client.uploadContent = uploadFn;
|
||||
|
||||
const sendMsgFn = jest.fn().mockImplementation((roomId: string, contents: any) => {
|
||||
const sendMsgFn = vi.fn().mockImplementation((roomId: string, contents: any) => {
|
||||
expect(roomId).toEqual(tree.roomId);
|
||||
expect(contents).toMatchObject({
|
||||
msgtype: MsgType.File,
|
||||
@@ -926,7 +941,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
});
|
||||
client.sendMessage = sendMsgFn;
|
||||
|
||||
const sendStateFn = jest
|
||||
const sendStateFn = vi
|
||||
.fn()
|
||||
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
|
||||
expect(roomId).toEqual(tree.roomId);
|
||||
@@ -962,7 +977,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
const fileName = "My File.txt";
|
||||
const fileContents = "This is a test file";
|
||||
|
||||
const uploadFn = jest.fn().mockImplementation((contents: Buffer, opts: any) => {
|
||||
const uploadFn = vi.fn().mockImplementation((contents: Buffer, opts: any) => {
|
||||
expect(contents.length).toEqual(fileContents.length);
|
||||
expect(opts).toMatchObject({
|
||||
includeFilename: false,
|
||||
@@ -971,7 +986,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
});
|
||||
client.uploadContent = uploadFn;
|
||||
|
||||
const sendMsgFn = jest.fn().mockImplementation((roomId: string, contents: any) => {
|
||||
const sendMsgFn = vi.fn().mockImplementation((roomId: string, contents: any) => {
|
||||
expect(roomId).toEqual(tree.roomId);
|
||||
const content = {
|
||||
msgtype: MsgType.File,
|
||||
@@ -989,7 +1004,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
});
|
||||
client.sendMessage = sendMsgFn;
|
||||
|
||||
const sendStateFn = jest
|
||||
const sendStateFn = vi
|
||||
.fn()
|
||||
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
|
||||
expect(roomId).toEqual(tree.roomId);
|
||||
|
||||
@@ -20,7 +20,7 @@ import { M_BEACON_INFO } from "../../../src/@types/beacon";
|
||||
import { isTimestampInDuration, Beacon, BeaconEvent } from "../../../src/models/beacon";
|
||||
import { makeBeaconEvent, makeBeaconInfoEvent } from "../../test-utils/beacon";
|
||||
|
||||
jest.useFakeTimers();
|
||||
vi.useFakeTimers();
|
||||
|
||||
describe("Beacon", () => {
|
||||
describe("isTimestampInDuration()", () => {
|
||||
@@ -70,9 +70,9 @@ describe("Beacon", () => {
|
||||
|
||||
const advanceDateAndTime = (ms: number) => {
|
||||
// bc liveness check uses Date.now we have to advance this mock
|
||||
jest.spyOn(globalThis.Date, "now").mockReturnValue(Date.now() + ms);
|
||||
vi.spyOn(globalThis.Date, "now").mockReturnValue(Date.now() + ms);
|
||||
// then advance time for the interval by the same amount
|
||||
jest.advanceTimersByTime(ms);
|
||||
vi.advanceTimersByTime(ms);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -108,11 +108,11 @@ describe("Beacon", () => {
|
||||
);
|
||||
|
||||
// back to 'now'
|
||||
jest.spyOn(globalThis.Date, "now").mockReturnValue(now);
|
||||
vi.spyOn(globalThis.Date, "now").mockReturnValue(now);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.spyOn(globalThis.Date, "now").mockRestore();
|
||||
vi.spyOn(globalThis.Date, "now").mockRestore();
|
||||
});
|
||||
|
||||
it("creates beacon from event", () => {
|
||||
@@ -250,7 +250,7 @@ describe("Beacon", () => {
|
||||
|
||||
it("does not update with an older event", () => {
|
||||
const beacon = new Beacon(liveBeaconEvent);
|
||||
const emitSpy = jest.spyOn(beacon, "emit").mockClear();
|
||||
const emitSpy = vi.spyOn(beacon, "emit").mockClear();
|
||||
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
|
||||
|
||||
const oldUpdateEvent = makeBeaconInfoEvent(userId, roomId);
|
||||
@@ -265,7 +265,7 @@ describe("Beacon", () => {
|
||||
|
||||
it("updates event", () => {
|
||||
const beacon = new Beacon(liveBeaconEvent);
|
||||
const emitSpy = jest.spyOn(beacon, "emit");
|
||||
const emitSpy = vi.spyOn(beacon, "emit");
|
||||
|
||||
expect(beacon.isLive).toEqual(true);
|
||||
|
||||
@@ -283,7 +283,7 @@ describe("Beacon", () => {
|
||||
|
||||
it("emits livenesschange event when beacon liveness changes", () => {
|
||||
const beacon = new Beacon(liveBeaconEvent);
|
||||
const emitSpy = jest.spyOn(beacon, "emit");
|
||||
const emitSpy = vi.spyOn(beacon, "emit");
|
||||
|
||||
expect(beacon.isLive).toEqual(true);
|
||||
|
||||
@@ -305,7 +305,7 @@ describe("Beacon", () => {
|
||||
// beacon was created an hour ago
|
||||
// and has a 3hr duration
|
||||
const beacon = new Beacon(notLiveBeaconEvent);
|
||||
const emitSpy = jest.spyOn(beacon, "emit");
|
||||
const emitSpy = vi.spyOn(beacon, "emit");
|
||||
|
||||
beacon.monitorLiveness();
|
||||
|
||||
@@ -332,7 +332,7 @@ describe("Beacon", () => {
|
||||
|
||||
const beacon = new Beacon(futureBeaconEvent);
|
||||
expect(beacon.isLive).toBeFalsy();
|
||||
const emitSpy = jest.spyOn(beacon, "emit");
|
||||
const emitSpy = vi.spyOn(beacon, "emit");
|
||||
|
||||
beacon.monitorLiveness();
|
||||
|
||||
@@ -356,7 +356,7 @@ describe("Beacon", () => {
|
||||
// and has a 3hr duration
|
||||
const beacon = new Beacon(liveBeaconEvent);
|
||||
expect(beacon.isLive).toBeTruthy();
|
||||
const emitSpy = jest.spyOn(beacon, "emit");
|
||||
const emitSpy = vi.spyOn(beacon, "emit");
|
||||
|
||||
beacon.monitorLiveness();
|
||||
advanceDateAndTime(HOUR_MS * 2 + 1);
|
||||
@@ -386,7 +386,7 @@ describe("Beacon", () => {
|
||||
// and has a 3hr duration
|
||||
const beacon = new Beacon(liveBeaconEvent);
|
||||
expect(beacon.isLive).toBeTruthy();
|
||||
const emitSpy = jest.spyOn(beacon, "emit");
|
||||
const emitSpy = vi.spyOn(beacon, "emit");
|
||||
|
||||
beacon.monitorLiveness();
|
||||
|
||||
@@ -406,7 +406,7 @@ describe("Beacon", () => {
|
||||
describe("addLocations", () => {
|
||||
it("ignores locations when beacon is not live", () => {
|
||||
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: false }));
|
||||
const emitSpy = jest.spyOn(beacon, "emit");
|
||||
const emitSpy = vi.spyOn(beacon, "emit");
|
||||
|
||||
beacon.addLocations([
|
||||
makeBeaconEvent(userId, { beaconInfoId: beacon.beaconInfoId, timestamp: now + 1 }),
|
||||
@@ -418,7 +418,7 @@ describe("Beacon", () => {
|
||||
|
||||
it("ignores locations outside the beacon live duration", () => {
|
||||
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
|
||||
const emitSpy = jest.spyOn(beacon, "emit");
|
||||
const emitSpy = vi.spyOn(beacon, "emit");
|
||||
|
||||
beacon.addLocations([
|
||||
// beacon has now + 60000 live period
|
||||
@@ -431,7 +431,7 @@ describe("Beacon", () => {
|
||||
|
||||
it("should ignore invalid beacon events", () => {
|
||||
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
|
||||
const emitSpy = jest.spyOn(beacon, "emit");
|
||||
const emitSpy = vi.spyOn(beacon, "emit");
|
||||
|
||||
const ev = new MatrixEvent({
|
||||
type: M_BEACON_INFO.name,
|
||||
@@ -460,7 +460,7 @@ describe("Beacon", () => {
|
||||
timestamp: startTimestamp,
|
||||
}),
|
||||
);
|
||||
const emitSpy = jest.spyOn(beacon, "emit");
|
||||
const emitSpy = vi.spyOn(beacon, "emit");
|
||||
|
||||
beacon.addLocations([
|
||||
// beacon has now + 60000 live period
|
||||
@@ -483,7 +483,7 @@ describe("Beacon", () => {
|
||||
timestamp: startTimestamp,
|
||||
}),
|
||||
);
|
||||
const emitSpy = jest.spyOn(beacon, "emit");
|
||||
const emitSpy = vi.spyOn(beacon, "emit");
|
||||
|
||||
beacon.addLocations([
|
||||
// beacon has now + 600000 live period
|
||||
@@ -501,7 +501,7 @@ describe("Beacon", () => {
|
||||
|
||||
it("sets latest location state to most recent location", () => {
|
||||
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
|
||||
const emitSpy = jest.spyOn(beacon, "emit");
|
||||
const emitSpy = vi.spyOn(beacon, "emit");
|
||||
|
||||
const locations = [
|
||||
// older
|
||||
@@ -553,7 +553,7 @@ describe("Beacon", () => {
|
||||
);
|
||||
expect(beacon.latestLocationEvent).toEqual(newerLocation);
|
||||
|
||||
const emitSpy = jest.spyOn(beacon, "emit").mockClear();
|
||||
const emitSpy = vi.spyOn(beacon, "emit").mockClear();
|
||||
|
||||
// add older location
|
||||
beacon.addLocations([olderLocation]);
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type MockedObject } from "jest-mock";
|
||||
import { type MockedObject } from "vitest";
|
||||
|
||||
import { type IContent, MatrixEvent, MatrixEventEvent } from "../../../src/models/event";
|
||||
import { emitPromise } from "../../test-utils/test-utils";
|
||||
@@ -159,7 +159,7 @@ describe("MatrixEvent", () => {
|
||||
|
||||
// Then it disappears from the thread and appears in the main timeline
|
||||
expect(ev.threadRootId).toBeUndefined();
|
||||
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId(), ev.getId()]);
|
||||
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId(), ev.getId(), redaction.getId()]);
|
||||
expect(threadLiveEventIds(room, 0)).not.toContain(ev.getId());
|
||||
});
|
||||
|
||||
@@ -183,7 +183,12 @@ describe("MatrixEvent", () => {
|
||||
|
||||
// Then the reaction moves into the main timeline
|
||||
expect(reaction.threadRootId).toBeUndefined();
|
||||
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId(), ev.getId(), reaction.getId()]);
|
||||
expect(mainTimelineLiveEventIds(room)).toEqual([
|
||||
threadRoot.getId(),
|
||||
ev.getId(),
|
||||
reaction.getId(),
|
||||
redaction.getId(),
|
||||
]);
|
||||
expect(threadLiveEventIds(room, 0)).not.toContain(reaction.getId());
|
||||
});
|
||||
|
||||
@@ -207,7 +212,12 @@ describe("MatrixEvent", () => {
|
||||
|
||||
// Then the edit moves into the main timeline
|
||||
expect(edit.threadRootId).toBeUndefined();
|
||||
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId(), ev.getId(), edit.getId()]);
|
||||
expect(mainTimelineLiveEventIds(room)).toEqual([
|
||||
threadRoot.getId(),
|
||||
ev.getId(),
|
||||
edit.getId(),
|
||||
redaction.getId(),
|
||||
]);
|
||||
expect(threadLiveEventIds(room, 0)).not.toContain(edit.getId());
|
||||
});
|
||||
|
||||
@@ -245,6 +255,7 @@ describe("MatrixEvent", () => {
|
||||
reply1.getId(),
|
||||
reply2.getId(),
|
||||
reaction.getId(),
|
||||
redaction.getId(),
|
||||
]);
|
||||
expect(threadLiveEventIds(room, 0)).not.toContain(reply1.getId());
|
||||
expect(threadLiveEventIds(room, 0)).not.toContain(reply2.getId());
|
||||
@@ -253,9 +264,9 @@ describe("MatrixEvent", () => {
|
||||
|
||||
function createMockClient(): MatrixClient {
|
||||
return {
|
||||
supportsThreads: jest.fn().mockReturnValue(true),
|
||||
decryptEventIfNeeded: jest.fn().mockReturnThis(),
|
||||
getUserId: jest.fn().mockReturnValue("@user:server"),
|
||||
supportsThreads: vi.fn().mockReturnValue(true),
|
||||
decryptEventIfNeeded: vi.fn().mockReturnThis(),
|
||||
getUserId: vi.fn().mockReturnValue("@user:server"),
|
||||
} as unknown as MockedObject<MatrixClient>;
|
||||
}
|
||||
|
||||
@@ -330,6 +341,7 @@ describe("MatrixEvent", () => {
|
||||
|
||||
function createRedaction(redactedEventid: string): MatrixEvent {
|
||||
return new MatrixEvent({
|
||||
event_id: `$redact-${redactedEventid}`,
|
||||
type: "m.room.redaction",
|
||||
redacts: redactedEventid,
|
||||
});
|
||||
@@ -362,6 +374,7 @@ describe("MatrixEvent", () => {
|
||||
});
|
||||
|
||||
describe("applyVisibilityEvent", () => {
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should emit VisibilityChange if a change was made", async () => {
|
||||
const ev = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
@@ -392,12 +405,12 @@ describe("MatrixEvent", () => {
|
||||
});
|
||||
|
||||
it("should report unknown decryption errors", async () => {
|
||||
const decryptionListener = jest.fn();
|
||||
const decryptionListener = vi.fn();
|
||||
encryptedEvent.addListener(MatrixEventEvent.Decrypted, decryptionListener);
|
||||
|
||||
const testError = new Error("test error");
|
||||
const crypto = {
|
||||
decryptEvent: jest.fn().mockRejectedValue(testError),
|
||||
decryptEvent: vi.fn().mockRejectedValue(testError),
|
||||
} as unknown as CryptoBackend;
|
||||
|
||||
await encryptedEvent.attemptDecryption(crypto);
|
||||
@@ -408,7 +421,7 @@ describe("MatrixEvent", () => {
|
||||
expect(encryptedEvent.decryptionFailureReason).not.toBe(
|
||||
DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE,
|
||||
);
|
||||
expect(encryptedEvent.getContent()).toEqual({
|
||||
expect(encryptedEvent.getContent<IContent>()).toEqual({
|
||||
msgtype: "m.bad.encrypted",
|
||||
body: "** Unable to decrypt: Error: test error **",
|
||||
});
|
||||
@@ -416,12 +429,12 @@ describe("MatrixEvent", () => {
|
||||
});
|
||||
|
||||
it("should report known decryption errors", async () => {
|
||||
const decryptionListener = jest.fn();
|
||||
const decryptionListener = vi.fn();
|
||||
encryptedEvent.addListener(MatrixEventEvent.Decrypted, decryptionListener);
|
||||
|
||||
const testError = new DecryptionError(DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID, "uisi");
|
||||
const crypto = {
|
||||
decryptEvent: jest.fn().mockRejectedValue(testError),
|
||||
decryptEvent: vi.fn().mockRejectedValue(testError),
|
||||
} as unknown as CryptoBackend;
|
||||
|
||||
await encryptedEvent.attemptDecryption(crypto);
|
||||
@@ -434,7 +447,7 @@ describe("MatrixEvent", () => {
|
||||
expect(encryptedEvent.decryptionFailureReason).not.toBe(
|
||||
DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE,
|
||||
);
|
||||
expect(encryptedEvent.getContent()).toEqual({
|
||||
expect(encryptedEvent.getContent<IContent>()).toEqual({
|
||||
msgtype: "m.bad.encrypted",
|
||||
body: "** Unable to decrypt: DecryptionError: uisi **",
|
||||
});
|
||||
@@ -443,7 +456,7 @@ describe("MatrixEvent", () => {
|
||||
|
||||
it(`should report "DecryptionError: The sender has disabled encrypting to unverified devices."`, async () => {
|
||||
const crypto = {
|
||||
decryptEvent: jest
|
||||
decryptEvent: vi
|
||||
.fn()
|
||||
.mockRejectedValue(
|
||||
new DecryptionError(
|
||||
@@ -460,17 +473,17 @@ describe("MatrixEvent", () => {
|
||||
expect(encryptedEvent.decryptionFailureReason).toBe(
|
||||
DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE,
|
||||
);
|
||||
expect(encryptedEvent.getContent()).toEqual({
|
||||
expect(encryptedEvent.getContent<IContent>()).toEqual({
|
||||
msgtype: "m.bad.encrypted",
|
||||
body: "** Unable to decrypt: DecryptionError: The sender has disabled encrypting to unverified devices. **",
|
||||
});
|
||||
});
|
||||
|
||||
it("should retry decryption if a retry is queued", async () => {
|
||||
const eventAttemptDecryptionSpy = jest.spyOn(encryptedEvent, "attemptDecryption");
|
||||
const eventAttemptDecryptionSpy = vi.spyOn(encryptedEvent, "attemptDecryption");
|
||||
|
||||
const crypto = {
|
||||
decryptEvent: jest
|
||||
decryptEvent: vi
|
||||
.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
// schedule a second decryption attempt while
|
||||
@@ -512,7 +525,7 @@ describe("MatrixEvent", () => {
|
||||
});
|
||||
|
||||
const crypto = {
|
||||
decryptEvent: jest.fn().mockImplementationOnce(() => {
|
||||
decryptEvent: vi.fn().mockImplementationOnce(() => {
|
||||
return Promise.resolve<EventDecryptionResult>({
|
||||
clearEvent: {
|
||||
type: "m.room.message",
|
||||
@@ -615,8 +628,8 @@ describe("MatrixEvent", () => {
|
||||
},
|
||||
};
|
||||
try {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(50);
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(50);
|
||||
// Prefer unsigned
|
||||
expect(new MatrixEvent({ ...evData } satisfies IStickyEvent).unstableStickyExpiresAt).toEqual(5050);
|
||||
// Fall back to `duration_ms`
|
||||
@@ -629,7 +642,7 @@ describe("MatrixEvent", () => {
|
||||
.unstableStickyExpiresAt,
|
||||
).toEqual(1050);
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,18 +25,18 @@ import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../test
|
||||
import { flushPromises } from "../../test-utils/flushPromises";
|
||||
import { mkEvent } from "../../test-utils/test-utils";
|
||||
|
||||
jest.useFakeTimers();
|
||||
vi.useFakeTimers();
|
||||
|
||||
describe("Poll", () => {
|
||||
const userId = "@alice:server.org";
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
decryptEventIfNeeded: jest.fn().mockResolvedValue(true),
|
||||
relations: jest.fn(),
|
||||
decryptEventIfNeeded: vi.fn().mockResolvedValue(true),
|
||||
relations: vi.fn(),
|
||||
});
|
||||
const roomId = "!room:server";
|
||||
const room = new Room(roomId, mockClient, userId);
|
||||
const maySendRedactionForEventSpy = jest.spyOn(room.currentState, "maySendRedactionForEvent");
|
||||
const maySendRedactionForEventSpy = vi.spyOn(room.currentState, "maySendRedactionForEvent");
|
||||
// 14.03.2022 16:15
|
||||
const now = 1647270879403;
|
||||
|
||||
@@ -48,8 +48,8 @@ describe("Poll", () => {
|
||||
basePollStartEvent.event.event_id = "$12345";
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.setSystemTime(now);
|
||||
vi.clearAllMocks();
|
||||
vi.setSystemTime(now);
|
||||
|
||||
mockClient.relations.mockReset().mockResolvedValue({ events: [] });
|
||||
|
||||
@@ -100,7 +100,7 @@ describe("Poll", () => {
|
||||
describe("fetching responses", () => {
|
||||
it("calls relations api and emits", async () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
const emitSpy = jest.spyOn(poll, "emit");
|
||||
const emitSpy = vi.spyOn(poll, "emit");
|
||||
const fetchResponsePromise = poll.getResponses();
|
||||
expect(poll.isFetchingResponses).toBe(true);
|
||||
const responses = await fetchResponsePromise;
|
||||
@@ -127,15 +127,17 @@ describe("Poll", () => {
|
||||
|
||||
it("waits for existing relations request to finish when getting responses", async () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
// @ts-expect-error TS2769
|
||||
const spy = vi.spyOn(poll, "fetchResponses");
|
||||
const firstResponsePromise = poll.getResponses();
|
||||
const secondResponsePromise = poll.getResponses();
|
||||
await firstResponsePromise;
|
||||
expect(firstResponsePromise).toEqual(secondResponsePromise);
|
||||
await secondResponsePromise;
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
expect(mockClient.relations).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("filters relations for relevent response events", async () => {
|
||||
it("filters relations for relevant response events", async () => {
|
||||
const replyEvent = makeRelatedEvent({ type: "m.room.message" });
|
||||
const stableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.stable! });
|
||||
const unstableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable });
|
||||
@@ -175,7 +177,7 @@ describe("Poll", () => {
|
||||
});
|
||||
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
vi.spyOn(poll, "emit");
|
||||
const responses = await poll.getResponses();
|
||||
|
||||
await flushPromises();
|
||||
@@ -197,13 +199,13 @@ describe("Poll", () => {
|
||||
const replyEvent = makeRelatedEvent({ type: "m.room.message" });
|
||||
const stableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.stable! });
|
||||
const undecryptableEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable });
|
||||
jest.spyOn(undecryptableEvent, "isDecryptionFailure").mockReturnValue(true);
|
||||
vi.spyOn(undecryptableEvent, "isDecryptionFailure").mockReturnValue(true);
|
||||
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [replyEvent, stableResponseEvent, undecryptableEvent],
|
||||
});
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
vi.spyOn(poll, "emit");
|
||||
await poll.getResponses();
|
||||
expect(poll.undecryptableRelationsCount).toBe(1);
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.UndecryptableRelations, 1);
|
||||
@@ -214,14 +216,14 @@ describe("Poll", () => {
|
||||
const stableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.stable! });
|
||||
const undecryptableEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable });
|
||||
const undecryptableEvent2 = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable });
|
||||
jest.spyOn(undecryptableEvent, "isDecryptionFailure").mockReturnValue(true);
|
||||
jest.spyOn(undecryptableEvent2, "isDecryptionFailure").mockReturnValue(true);
|
||||
vi.spyOn(undecryptableEvent, "isDecryptionFailure").mockReturnValue(true);
|
||||
vi.spyOn(undecryptableEvent2, "isDecryptionFailure").mockReturnValue(true);
|
||||
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [replyEvent, stableResponseEvent, undecryptableEvent],
|
||||
});
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
vi.spyOn(poll, "emit");
|
||||
await poll.getResponses();
|
||||
expect(poll.undecryptableRelationsCount).toBe(1);
|
||||
|
||||
@@ -248,7 +250,7 @@ describe("Poll", () => {
|
||||
|
||||
it("sets poll end event with stable event type", async () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
vi.spyOn(poll, "emit");
|
||||
await poll.getResponses();
|
||||
|
||||
expect(maySendRedactionForEventSpy).toHaveBeenCalledWith(basePollStartEvent, "@bob@server.org");
|
||||
@@ -270,7 +272,7 @@ describe("Poll", () => {
|
||||
events: [pollEndEvent],
|
||||
});
|
||||
maySendRedactionForEventSpy.mockReturnValue(false);
|
||||
jest.spyOn(poll, "emit");
|
||||
vi.spyOn(poll, "emit");
|
||||
await poll.getResponses();
|
||||
|
||||
expect(maySendRedactionForEventSpy).not.toHaveBeenCalled();
|
||||
@@ -283,7 +285,7 @@ describe("Poll", () => {
|
||||
events: [unstablePollEndEvent],
|
||||
});
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
vi.spyOn(poll, "emit");
|
||||
await poll.getResponses();
|
||||
|
||||
expect(poll.isEnded).toBe(true);
|
||||
@@ -304,7 +306,7 @@ describe("Poll", () => {
|
||||
describe("onNewRelation()", () => {
|
||||
it("discards response if poll responses have not been initialised", () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
vi.spyOn(poll, "emit");
|
||||
const responseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now);
|
||||
|
||||
poll.onNewRelation(responseEvent);
|
||||
@@ -315,7 +317,7 @@ describe("Poll", () => {
|
||||
|
||||
it("sets poll end event when responses are not initialised", () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
vi.spyOn(poll, "emit");
|
||||
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: userId });
|
||||
|
||||
poll.onNewRelation(stablePollEndEvent);
|
||||
@@ -332,7 +334,7 @@ describe("Poll", () => {
|
||||
});
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
await poll.getResponses();
|
||||
jest.spyOn(poll, "emit");
|
||||
vi.spyOn(poll, "emit");
|
||||
|
||||
poll.onNewRelation(stablePollEndEvent);
|
||||
|
||||
@@ -367,7 +369,7 @@ describe("Poll", () => {
|
||||
expect(poll.isEnded).toBeTruthy();
|
||||
|
||||
// reset spy count
|
||||
jest.spyOn(poll, "emit").mockClear();
|
||||
vi.spyOn(poll, "emit").mockClear();
|
||||
|
||||
// add a valid end event with earlier timestamp
|
||||
poll.onNewRelation(earlierPollEndEvent);
|
||||
@@ -398,7 +400,7 @@ describe("Poll", () => {
|
||||
expect(poll.isEnded).toBeTruthy();
|
||||
|
||||
// reset spy count
|
||||
jest.spyOn(poll, "emit").mockClear();
|
||||
vi.spyOn(poll, "emit").mockClear();
|
||||
|
||||
poll.onNewRelation(laterPollEndEvent);
|
||||
// didn't set new end event, didn't refilter responses
|
||||
@@ -416,7 +418,7 @@ describe("Poll", () => {
|
||||
});
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
const responses = await poll.getResponses();
|
||||
jest.spyOn(poll, "emit");
|
||||
vi.spyOn(poll, "emit");
|
||||
|
||||
expect(responses.getRelations().length).toEqual(3);
|
||||
poll.onNewRelation(stablePollEndEvent);
|
||||
@@ -432,7 +434,7 @@ describe("Poll", () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
// init responses
|
||||
const responses = await poll.getResponses();
|
||||
jest.spyOn(poll, "emit");
|
||||
vi.spyOn(poll, "emit");
|
||||
const replyEvent = new MatrixEvent({ type: "m.room.message" });
|
||||
|
||||
poll.onNewRelation(replyEvent);
|
||||
@@ -446,7 +448,7 @@ describe("Poll", () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
// init responses
|
||||
const responses = await poll.getResponses();
|
||||
jest.spyOn(poll, "emit");
|
||||
vi.spyOn(poll, "emit");
|
||||
const responseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now);
|
||||
|
||||
poll.onNewRelation(responseEvent);
|
||||
|
||||
@@ -32,11 +32,11 @@ import { Room } from "../../../src/models/room";
|
||||
*/
|
||||
describe("RoomReceipts", () => {
|
||||
beforeAll(() => {
|
||||
jest.replaceProperty(Thread, "hasServerSideSupport", FeatureSupport.Stable);
|
||||
vi.spyOn(Thread, "hasServerSideSupport", "get").mockReturnValue(FeatureSupport.Stable);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("reports events unread if there are no receipts", () => {
|
||||
@@ -436,13 +436,13 @@ describe("RoomReceipts", () => {
|
||||
|
||||
function createFakeClient(): MatrixClient {
|
||||
return {
|
||||
getUserId: jest.fn(),
|
||||
getEventMapper: jest.fn().mockReturnValue(jest.fn()),
|
||||
isInitialSyncComplete: jest.fn().mockReturnValue(true),
|
||||
supportsThreads: jest.fn().mockReturnValue(true),
|
||||
fetchRoomEvent: jest.fn().mockResolvedValue({}),
|
||||
paginateEventTimeline: jest.fn(),
|
||||
canSupport: { get: jest.fn() },
|
||||
getUserId: vi.fn(),
|
||||
getEventMapper: vi.fn().mockReturnValue(vi.fn()),
|
||||
isInitialSyncComplete: vi.fn().mockReturnValue(true),
|
||||
supportsThreads: vi.fn().mockReturnValue(true),
|
||||
fetchRoomEvent: vi.fn().mockResolvedValue({}),
|
||||
paginateEventTimeline: vi.fn(),
|
||||
canSupport: { get: vi.fn() },
|
||||
} as unknown as MatrixClient;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { type Mock } from "vitest";
|
||||
|
||||
import { type IStickyEvent, MatrixEvent } from "../../../src";
|
||||
import { RoomStickyEventsStore, RoomStickyEventsEvent } from "../../../src/models/room-sticky-events";
|
||||
|
||||
describe("RoomStickyEvents", () => {
|
||||
let stickyEvents: RoomStickyEventsStore;
|
||||
const emitSpy: jest.Mock = jest.fn();
|
||||
const emitSpy: Mock = vi.fn();
|
||||
const stickyEvent: IStickyEvent = {
|
||||
event_id: "$foo:bar",
|
||||
room_id: "!roomId",
|
||||
@@ -196,14 +198,14 @@ describe("RoomStickyEvents", () => {
|
||||
|
||||
describe("cleanExpiredStickyEvents", () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("should emit when a sticky event expires", () => {
|
||||
jest.setSystemTime(1000);
|
||||
vi.setSystemTime(1000);
|
||||
const ev = new MatrixEvent({
|
||||
...stickyEvent,
|
||||
origin_server_ts: 0,
|
||||
@@ -215,18 +217,18 @@ describe("RoomStickyEvents", () => {
|
||||
origin_server_ts: 1000,
|
||||
});
|
||||
stickyEvents.addStickyEvents([ev, evLater]);
|
||||
const emitSpy = jest.fn();
|
||||
const emitSpy = vi.fn();
|
||||
stickyEvents.on(RoomStickyEventsEvent.Update, emitSpy);
|
||||
jest.advanceTimersByTime(15000);
|
||||
vi.advanceTimersByTime(15000);
|
||||
expect(emitSpy).toHaveBeenCalledWith([], [], [ev]);
|
||||
// Then expire the next event
|
||||
jest.advanceTimersByTime(1000);
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(emitSpy).toHaveBeenCalledWith([], [], [evLater]);
|
||||
});
|
||||
it("should emit two events when both expire at the same time", () => {
|
||||
const emitSpy = jest.fn();
|
||||
const emitSpy = vi.fn();
|
||||
stickyEvents.on(RoomStickyEventsEvent.Update, emitSpy);
|
||||
jest.setSystemTime(0);
|
||||
vi.setSystemTime(0);
|
||||
const ev1 = new MatrixEvent({
|
||||
...stickyEvent,
|
||||
event_id: "$eventA",
|
||||
@@ -242,21 +244,181 @@ describe("RoomStickyEvents", () => {
|
||||
});
|
||||
stickyEvents.addStickyEvents([ev1, ev2]);
|
||||
expect(emitSpy).toHaveBeenCalledWith([ev1, ev2], [], []);
|
||||
jest.advanceTimersByTime(15000);
|
||||
vi.advanceTimersByTime(15000);
|
||||
expect(emitSpy).toHaveBeenCalledWith([], [], [ev1, ev2]);
|
||||
});
|
||||
it("should emit when a unkeyed sticky event expires", () => {
|
||||
const emitSpy = jest.fn();
|
||||
const emitSpy = vi.fn();
|
||||
stickyEvents.on(RoomStickyEventsEvent.Update, emitSpy);
|
||||
jest.setSystemTime(0);
|
||||
vi.setSystemTime(0);
|
||||
const ev = new MatrixEvent({
|
||||
...stickyEvent,
|
||||
content: {},
|
||||
origin_server_ts: Date.now(),
|
||||
});
|
||||
stickyEvents.addStickyEvents([ev]);
|
||||
jest.advanceTimersByTime(15000);
|
||||
vi.advanceTimersByTime(15000);
|
||||
expect(emitSpy).toHaveBeenCalledWith([], [], [ev]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleRedaction", () => {
|
||||
beforeAll(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
afterAll(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
it("should not emit if the event does not exist in the map", () => {
|
||||
const emitSpy = vi.fn();
|
||||
const ev = new MatrixEvent({
|
||||
...stickyEvent,
|
||||
content: {},
|
||||
origin_server_ts: Date.now(),
|
||||
});
|
||||
stickyEvents.addStickyEvents([ev]);
|
||||
stickyEvents.on(RoomStickyEventsEvent.Update, emitSpy);
|
||||
stickyEvents.handleRedaction("$123456");
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
it("should emit a remove when the event exists in the map without a predecessor", () => {
|
||||
const emitSpy = vi.fn();
|
||||
const ev = new MatrixEvent({
|
||||
...stickyEvent,
|
||||
origin_server_ts: Date.now(),
|
||||
});
|
||||
stickyEvents.addStickyEvents([ev]);
|
||||
stickyEvents.on(RoomStickyEventsEvent.Update, emitSpy);
|
||||
stickyEvents.handleRedaction(stickyEvent.event_id);
|
||||
expect(emitSpy).toHaveBeenCalledWith([], [], [ev]);
|
||||
});
|
||||
it("should emit a remove when the event has no sticky key", () => {
|
||||
const emitSpy = vi.fn();
|
||||
const ev = new MatrixEvent({
|
||||
...stickyEvent,
|
||||
content: {},
|
||||
origin_server_ts: Date.now(),
|
||||
});
|
||||
stickyEvents.addStickyEvents([ev]);
|
||||
stickyEvents.on(RoomStickyEventsEvent.Update, emitSpy);
|
||||
stickyEvents.handleRedaction(stickyEvent.event_id);
|
||||
expect(emitSpy).toHaveBeenCalledWith([], [], [ev]);
|
||||
});
|
||||
it("should emit an update when the event exists in the map with a predecessor", () => {
|
||||
const emitSpy = vi.fn();
|
||||
const ev = new MatrixEvent({
|
||||
...stickyEvent,
|
||||
origin_server_ts: Date.now(),
|
||||
});
|
||||
vi.advanceTimersByTime(1000); // Advance time so we can insert a newer event.
|
||||
const newerEv = new MatrixEvent({
|
||||
...stickyEvent,
|
||||
event_id: "$newer-ev",
|
||||
origin_server_ts: Date.now() + 1000,
|
||||
});
|
||||
stickyEvents.addStickyEvents([ev, newerEv]);
|
||||
stickyEvents.on(RoomStickyEventsEvent.Update, emitSpy);
|
||||
stickyEvents.handleRedaction(newerEv.getId()!);
|
||||
expect(emitSpy).toHaveBeenCalledWith([], [{ current: ev, previous: newerEv }], []);
|
||||
});
|
||||
it("should emit a remove if the previous event has expired", () => {
|
||||
const emitSpy = vi.fn();
|
||||
const ev = new MatrixEvent({
|
||||
...stickyEvent,
|
||||
origin_server_ts: Date.now(),
|
||||
});
|
||||
vi.advanceTimersByTime(1000); // Advance time so we can insert a newer event.
|
||||
const newerEv = new MatrixEvent({
|
||||
...stickyEvent,
|
||||
event_id: "$newer-ev",
|
||||
origin_server_ts: Date.now() + 1000,
|
||||
});
|
||||
stickyEvents.addStickyEvents([ev, newerEv]);
|
||||
stickyEvents.on(RoomStickyEventsEvent.Update, emitSpy);
|
||||
// Expire the older event.
|
||||
vi.advanceTimersByTime(stickyEvent.msc4354_sticky.duration_ms);
|
||||
// Redact the newer event
|
||||
stickyEvents.handleRedaction(newerEv.getId()!);
|
||||
expect(emitSpy).toHaveBeenCalledWith([], [], [newerEv]);
|
||||
});
|
||||
it("should recurse the chain of events if the previous event has been redacted", () => {
|
||||
const emitSpy = vi.fn();
|
||||
const ev = new MatrixEvent({
|
||||
...stickyEvent,
|
||||
origin_server_ts: Date.now(),
|
||||
});
|
||||
vi.advanceTimersByTime(1000); // Advance time so we can insert a newer event.
|
||||
const middleEv = new MatrixEvent({
|
||||
...stickyEvent,
|
||||
event_id: "$newer-ev",
|
||||
origin_server_ts: Date.now() + 1000,
|
||||
});
|
||||
vi.advanceTimersByTime(1000);
|
||||
const newestEv = new MatrixEvent({
|
||||
...stickyEvent,
|
||||
event_id: "$newest-ev",
|
||||
origin_server_ts: Date.now() + 2000,
|
||||
});
|
||||
stickyEvents.addStickyEvents([ev, middleEv, newestEv]);
|
||||
stickyEvents.on(RoomStickyEventsEvent.Update, emitSpy);
|
||||
// Mark the middle event as redacted.
|
||||
middleEv.setUnsigned({
|
||||
redacted_because: {
|
||||
event_id: "$foo",
|
||||
} as any,
|
||||
});
|
||||
// Redact the newer event
|
||||
stickyEvents.handleRedaction(newestEv.getId()!);
|
||||
// expect immediate transition from newestEv -> ev and skipping middleEv
|
||||
expect(emitSpy).toHaveBeenCalledWith([], [{ current: ev, previous: newestEv }], []);
|
||||
});
|
||||
it("should revert to the most recent valid event regardless of insertion order", () => {
|
||||
const emitSpy = vi.fn();
|
||||
const ev = new MatrixEvent({
|
||||
...stickyEvent,
|
||||
origin_server_ts: Date.now(),
|
||||
});
|
||||
vi.advanceTimersByTime(1000); // Advance time so we can insert a newer event.
|
||||
const middleEv = new MatrixEvent({
|
||||
...stickyEvent,
|
||||
event_id: "$newer-ev",
|
||||
origin_server_ts: Date.now() + 1000,
|
||||
});
|
||||
vi.advanceTimersByTime(1000);
|
||||
const newestEv = new MatrixEvent({
|
||||
...stickyEvent,
|
||||
event_id: "$newest-ev",
|
||||
origin_server_ts: Date.now() + 2000,
|
||||
});
|
||||
// Invert in reverse order, to make sure we retain the older events.
|
||||
stickyEvents.addStickyEvents([newestEv, middleEv, ev]);
|
||||
stickyEvents.on(RoomStickyEventsEvent.Update, emitSpy);
|
||||
// Mark the middle event as redacted.
|
||||
middleEv.setUnsigned({
|
||||
redacted_because: {
|
||||
event_id: "$foo",
|
||||
} as any,
|
||||
});
|
||||
// Redact the newer event
|
||||
stickyEvents.handleRedaction(newestEv.getId()!);
|
||||
expect(emitSpy).toHaveBeenCalledWith([], [{ current: ev, previous: newestEv }], []);
|
||||
});
|
||||
it("should handle redaction when using `handleRedaction` with a `MatrixEvent` parameter", () => {
|
||||
const emitSpy = vi.fn();
|
||||
const ev = new MatrixEvent({
|
||||
...stickyEvent,
|
||||
origin_server_ts: Date.now(),
|
||||
});
|
||||
vi.advanceTimersByTime(1000); // Advance time so we can insert a newer event.
|
||||
const newerEv = new MatrixEvent({
|
||||
...stickyEvent,
|
||||
event_id: "$newer-ev",
|
||||
origin_server_ts: Date.now() + 1000,
|
||||
});
|
||||
stickyEvents.addStickyEvents([ev, newerEv]);
|
||||
stickyEvents.on(RoomStickyEventsEvent.Update, emitSpy);
|
||||
stickyEvents.handleRedaction(newerEv);
|
||||
expect(emitSpy).toHaveBeenCalledWith([], [{ current: ev, previous: newerEv }], []);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,8 +14,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Direction, type MatrixClient, MatrixEvent, Room } from "../../../src";
|
||||
import type { MockedObject } from "jest-mock";
|
||||
import { type MockedObject } from "vitest";
|
||||
|
||||
import { Direction, EventType, type MatrixClient, MatrixEvent, Room } from "../../../src";
|
||||
|
||||
const CREATOR_USER_ID = "@creator:example.org";
|
||||
const MODERATOR_USER_ID = "@moderator:example.org";
|
||||
@@ -23,9 +24,9 @@ const MODERATOR_USER_ID = "@moderator:example.org";
|
||||
describe("Room", () => {
|
||||
function createMockClient(): MatrixClient {
|
||||
return {
|
||||
supportsThreads: jest.fn().mockReturnValue(true),
|
||||
decryptEventIfNeeded: jest.fn().mockReturnThis(),
|
||||
getUserId: jest.fn().mockReturnValue(CREATOR_USER_ID),
|
||||
supportsThreads: vi.fn().mockReturnValue(true),
|
||||
decryptEventIfNeeded: vi.fn().mockReturnThis(),
|
||||
getUserId: vi.fn().mockReturnValue(CREATOR_USER_ID),
|
||||
} as unknown as MockedObject<MatrixClient>;
|
||||
}
|
||||
|
||||
@@ -129,6 +130,7 @@ describe("Room", () => {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should apply on ban", async () => {
|
||||
const { room, messageEvents } = await setupRoom(true);
|
||||
const banEvent = createRedactOnMembershipChange(CREATOR_USER_ID, MODERATOR_USER_ID, "ban");
|
||||
@@ -137,6 +139,7 @@ describe("Room", () => {
|
||||
expectRedacted(messageEvents, room, true);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should apply on kick", async () => {
|
||||
const { room, messageEvents } = await setupRoom(true);
|
||||
const kickEvent = createRedactOnMembershipChange(CREATOR_USER_ID, MODERATOR_USER_ID, "leave");
|
||||
@@ -145,6 +148,7 @@ describe("Room", () => {
|
||||
expectRedacted(messageEvents, room, true);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should not apply if the user doesn't have permission to redact", async () => {
|
||||
const { room, messageEvents } = await setupRoom(false); // difference from other tests here
|
||||
const banEvent = createRedactOnMembershipChange(CREATOR_USER_ID, MODERATOR_USER_ID, "ban");
|
||||
@@ -153,6 +157,7 @@ describe("Room", () => {
|
||||
expectRedacted(messageEvents, room, false);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should not apply to self-leaves", async () => {
|
||||
const { room, messageEvents } = await setupRoom(true);
|
||||
const leaveEvent = createRedactOnMembershipChange(CREATOR_USER_ID, CREATOR_USER_ID, "leave");
|
||||
@@ -161,6 +166,7 @@ describe("Room", () => {
|
||||
expectRedacted(messageEvents, room, false);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should not apply to invites", async () => {
|
||||
const { room, messageEvents } = await setupRoom(true);
|
||||
const leaveEvent = createRedactOnMembershipChange(CREATOR_USER_ID, CREATOR_USER_ID, "invite");
|
||||
@@ -169,6 +175,7 @@ describe("Room", () => {
|
||||
expectRedacted(messageEvents, room, false);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should not apply to joins", async () => {
|
||||
const { room, messageEvents } = await setupRoom(true);
|
||||
const leaveEvent = createRedactOnMembershipChange(CREATOR_USER_ID, CREATOR_USER_ID, "join");
|
||||
@@ -177,6 +184,7 @@ describe("Room", () => {
|
||||
expectRedacted(messageEvents, room, false);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should not apply to knocks", async () => {
|
||||
const { room, messageEvents } = await setupRoom(true);
|
||||
const leaveEvent = createRedactOnMembershipChange(CREATOR_USER_ID, CREATOR_USER_ID, "knock");
|
||||
@@ -185,4 +193,71 @@ describe("Room", () => {
|
||||
expectRedacted(messageEvents, room, false);
|
||||
});
|
||||
});
|
||||
|
||||
it("should ignore invalid m.room.name events", async () => {
|
||||
const mockClient = createMockClient();
|
||||
const room = new Room("!room:example.org", mockClient, CREATOR_USER_ID);
|
||||
const invalidNameEvent = new MatrixEvent({
|
||||
type: EventType.RoomName,
|
||||
content: {
|
||||
name: { invalid: 123 },
|
||||
},
|
||||
state_key: "",
|
||||
event_id: "$123",
|
||||
room_id: room.roomId,
|
||||
sender: CREATOR_USER_ID,
|
||||
});
|
||||
|
||||
// Set up the room
|
||||
room.currentState.setStateEvents([invalidNameEvent]);
|
||||
room.recalculate();
|
||||
|
||||
expect(room.name).toEqual("Empty room");
|
||||
});
|
||||
|
||||
describe("getAltAliases()", () => {
|
||||
it("should ignore invalid events", async () => {
|
||||
const mockClient = createMockClient();
|
||||
const room = new Room("!room:example.org", mockClient, CREATOR_USER_ID);
|
||||
const invalidAliasEvent = new MatrixEvent({
|
||||
type: EventType.RoomCanonicalAlias,
|
||||
content: {
|
||||
alt_aliases: [123, "#foo:bar"],
|
||||
},
|
||||
state_key: "",
|
||||
event_id: "$123",
|
||||
room_id: room.roomId,
|
||||
sender: CREATOR_USER_ID,
|
||||
});
|
||||
|
||||
// Set up the room
|
||||
room.currentState.setStateEvents([invalidAliasEvent]);
|
||||
room.recalculate();
|
||||
|
||||
expect(room.getAltAliases()).toEqual(["#foo:bar"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("calculateRoomName()", () => {
|
||||
it("should ignore empty m.room.name 'name' field", async () => {
|
||||
const mockClient = createMockClient();
|
||||
const room = new Room("!room:example.org", mockClient, CREATOR_USER_ID);
|
||||
const event = new MatrixEvent({
|
||||
type: EventType.RoomName,
|
||||
content: {
|
||||
name: "",
|
||||
},
|
||||
state_key: "",
|
||||
event_id: "$123",
|
||||
room_id: room.roomId,
|
||||
sender: CREATOR_USER_ID,
|
||||
});
|
||||
|
||||
// Set up the room
|
||||
room.currentState.setStateEvents([event]);
|
||||
room.recalculate();
|
||||
|
||||
expect(room.name).not.toEqual("");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { MatrixClient, PendingEventOrdering } from "../../../src/client";
|
||||
import { Room, RoomEvent } from "../../../src/models/room";
|
||||
import { FeatureSupport, Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../../src/models/thread";
|
||||
@@ -43,12 +41,12 @@ describe("Thread", () => {
|
||||
const myUserId = "@bob:example.org";
|
||||
const testClient = new TestClient(myUserId, "DEVICE", "ACCESS_TOKEN", undefined, { timelineSupport: false });
|
||||
const client = testClient.client;
|
||||
client.supportsThreads = jest.fn().mockReturnValue(true);
|
||||
client.supportsThreads = vi.fn().mockReturnValue(true);
|
||||
const room = new Room("123", client, myUserId, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
vi.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
|
||||
const { thread } = mkThread({
|
||||
room,
|
||||
@@ -86,10 +84,10 @@ describe("Thread", () => {
|
||||
beforeEach(() => {
|
||||
client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(),
|
||||
isInitialSyncComplete: jest.fn().mockReturnValue(false),
|
||||
getRoom: jest.fn().mockImplementation(() => room),
|
||||
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
|
||||
supportsThreads: jest.fn().mockReturnValue(true),
|
||||
isInitialSyncComplete: vi.fn().mockReturnValue(false),
|
||||
getRoom: vi.fn().mockImplementation(() => room),
|
||||
decryptEventIfNeeded: vi.fn().mockResolvedValue(void 0),
|
||||
supportsThreads: vi.fn().mockReturnValue(true),
|
||||
});
|
||||
client.reEmitter = mock(ReEmitter, "ReEmitter");
|
||||
client.canSupport = new Map();
|
||||
@@ -128,11 +126,11 @@ describe("Thread", () => {
|
||||
});
|
||||
room.addReceipt(receipt);
|
||||
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
vi.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.resetAllMocks();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("considers own events with no RR as read", () => {
|
||||
@@ -216,10 +214,10 @@ describe("Thread", () => {
|
||||
beforeEach(() => {
|
||||
client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(),
|
||||
isInitialSyncComplete: jest.fn().mockReturnValue(false),
|
||||
getRoom: jest.fn().mockImplementation(() => room),
|
||||
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
|
||||
supportsThreads: jest.fn().mockReturnValue(true),
|
||||
isInitialSyncComplete: vi.fn().mockReturnValue(false),
|
||||
getRoom: vi.fn().mockImplementation(() => room),
|
||||
decryptEventIfNeeded: vi.fn().mockResolvedValue(void 0),
|
||||
supportsThreads: vi.fn().mockReturnValue(true),
|
||||
});
|
||||
client.reEmitter = mock(ReEmitter, "ReEmitter");
|
||||
client.canSupport = new Map();
|
||||
@@ -231,11 +229,11 @@ describe("Thread", () => {
|
||||
|
||||
room = new Room("123", client, myUserId);
|
||||
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
vi.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.resetAllMocks();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("uses unthreaded receipt to figure out read up to", () => {
|
||||
@@ -319,12 +317,12 @@ describe("Thread", () => {
|
||||
timelineSupport: false,
|
||||
});
|
||||
const client = testClient.client;
|
||||
client.supportsThreads = jest.fn().mockReturnValue(true);
|
||||
client.supportsThreads = vi.fn().mockReturnValue(true);
|
||||
const room = new Room("123", client, myUserId, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
vi.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
|
||||
const { thread } = mkThread({
|
||||
room,
|
||||
@@ -336,7 +334,7 @@ describe("Thread", () => {
|
||||
await emitPromise(thread, ThreadEvent.Update);
|
||||
expect(thread.length).toBe(2);
|
||||
|
||||
jest.spyOn(client, "createMessagesRequest").mockImplementation((_, token) =>
|
||||
vi.spyOn(client, "createMessagesRequest").mockImplementation((_, token) =>
|
||||
Promise.resolve({
|
||||
chunk: [],
|
||||
start: `${token}-new`,
|
||||
@@ -374,12 +372,12 @@ describe("Thread", () => {
|
||||
timelineSupport: false,
|
||||
});
|
||||
const client = testClient.client;
|
||||
client.supportsThreads = jest.fn().mockReturnValue(true);
|
||||
client.supportsThreads = vi.fn().mockReturnValue(true);
|
||||
const room = new Room("123", client, myUserId, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
vi.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
|
||||
const { thread } = mkThread({
|
||||
room,
|
||||
@@ -391,7 +389,7 @@ describe("Thread", () => {
|
||||
await emitPromise(thread, ThreadEvent.Update);
|
||||
expect(thread.length).toBe(2);
|
||||
|
||||
jest.spyOn(client, "createMessagesRequest").mockImplementation((_, token) =>
|
||||
vi.spyOn(client, "createMessagesRequest").mockImplementation((_, token) =>
|
||||
Promise.resolve({
|
||||
chunk: [],
|
||||
start: `${token}-new`,
|
||||
@@ -426,12 +424,12 @@ describe("Thread", () => {
|
||||
timelineSupport: false,
|
||||
});
|
||||
const client = testClient.client;
|
||||
client.supportsThreads = jest.fn().mockReturnValue(true);
|
||||
client.supportsThreads = vi.fn().mockReturnValue(true);
|
||||
const room = new Room("123", client, myUserId, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
vi.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
|
||||
const { thread } = mkThread({
|
||||
room,
|
||||
@@ -442,7 +440,7 @@ describe("Thread", () => {
|
||||
});
|
||||
await emitPromise(thread, ThreadEvent.Update);
|
||||
expect(thread.length).toBe(2);
|
||||
const mock = jest.spyOn(thread, "resetLiveTimeline");
|
||||
const mock = vi.spyOn(thread, "resetLiveTimeline");
|
||||
mock.mockReturnValue(Promise.resolve());
|
||||
|
||||
room.resetLiveTimeline("b1", "f1");
|
||||
@@ -637,8 +635,8 @@ describe("Thread", () => {
|
||||
const client = mock(MatrixClient, "MatrixClient");
|
||||
client.reEmitter = mock(ReEmitter, "ReEmitter");
|
||||
client.canSupport = canSupport;
|
||||
jest.spyOn(client, "getEventMapper").mockReturnValue(eventMapperFor(client, {}));
|
||||
mocked(client.supportsThreads).mockReturnValue(true);
|
||||
vi.spyOn(client, "getEventMapper").mockReturnValue(eventMapperFor(client, {}));
|
||||
vi.mocked(client.supportsThreads).mockReturnValue(true);
|
||||
return client;
|
||||
}
|
||||
});
|
||||
@@ -692,7 +690,7 @@ describe("Thread", () => {
|
||||
// (fetchEditsWhereNeeded only applies to encrypted messages for some reason)
|
||||
messageToEdit.event.type = EventType.RoomMessageEncrypted;
|
||||
const editEvent = mkEdit(messageToEdit, client, user, room, "edit");
|
||||
mocked(client.relations).mockImplementation(async (_roomId, eventId) => {
|
||||
vi.mocked(client.relations).mockImplementation(async (_roomId, eventId) => {
|
||||
if (eventId === messageToEdit.getId()) {
|
||||
return { events: [editEvent] };
|
||||
} else {
|
||||
@@ -785,12 +783,12 @@ describe("Thread", () => {
|
||||
timelineSupport: false,
|
||||
});
|
||||
const client = testClient.client;
|
||||
client.supportsThreads = jest.fn().mockReturnValue(true);
|
||||
client.supportsThreads = vi.fn().mockReturnValue(true);
|
||||
|
||||
const room = new Room("!room:z", client, myUserId, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
vi.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
|
||||
// Create a root event
|
||||
const rootEvent = mkMessage({
|
||||
@@ -903,7 +901,7 @@ describe("Thread", () => {
|
||||
timelineSupport: false,
|
||||
});
|
||||
const client = testClient.client;
|
||||
client.supportsThreads = jest.fn().mockReturnValue(true);
|
||||
client.supportsThreads = vi.fn().mockReturnValue(true);
|
||||
|
||||
// Force server-side support so threads start uninitialised
|
||||
const prevSupport = Thread.hasServerSideSupport;
|
||||
@@ -913,7 +911,7 @@ describe("Thread", () => {
|
||||
const room = new Room("!room:z", client, myUserId, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
vi.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
|
||||
// Create a root event and thread
|
||||
const rootEvent = mkMessage({ room: room.roomId, user: myUserId, msg: "Root", event: true });
|
||||
@@ -1028,13 +1026,13 @@ function createClient(canSupport = new Map()): MatrixClient {
|
||||
client.reEmitter = mock(ReEmitter, "ReEmitter");
|
||||
client.canSupport = canSupport;
|
||||
|
||||
jest.spyOn(client, "supportsThreads").mockReturnValue(true);
|
||||
jest.spyOn(client, "getEventMapper").mockReturnValue(eventMapperFor(client, {}));
|
||||
vi.spyOn(client, "supportsThreads").mockReturnValue(true);
|
||||
vi.spyOn(client, "getEventMapper").mockReturnValue(eventMapperFor(client, {}));
|
||||
|
||||
// Mock methods that call out to HTTP endpoints
|
||||
jest.spyOn(client, "paginateEventTimeline").mockResolvedValue(true);
|
||||
jest.spyOn(client, "relations").mockResolvedValue({ events: [] });
|
||||
jest.spyOn(client, "fetchRoomEvent").mockResolvedValue({});
|
||||
vi.spyOn(client, "paginateEventTimeline").mockResolvedValue(true);
|
||||
vi.spyOn(client, "relations").mockResolvedValue({ events: [] });
|
||||
vi.spyOn(client, "fetchRoomEvent").mockResolvedValue({});
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
@@ -53,11 +53,11 @@ describe("fixNotificationCountOnDecryption", () => {
|
||||
beforeEach(() => {
|
||||
mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(),
|
||||
isInitialSyncComplete: jest.fn().mockReturnValue(false),
|
||||
getPushActionsForEvent: jest.fn().mockReturnValue(mkPushAction(true, true)),
|
||||
getRoom: jest.fn().mockImplementation(() => room),
|
||||
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
|
||||
supportsThreads: jest.fn().mockReturnValue(true),
|
||||
isInitialSyncComplete: vi.fn().mockReturnValue(false),
|
||||
getPushActionsForEvent: vi.fn().mockReturnValue(mkPushAction(true, true)),
|
||||
getRoom: vi.fn().mockImplementation(() => room),
|
||||
decryptEventIfNeeded: vi.fn().mockResolvedValue(void 0),
|
||||
supportsThreads: vi.fn().mockReturnValue(true),
|
||||
});
|
||||
mockClient.reEmitter = mock(ReEmitter, "ReEmitter");
|
||||
mockClient.canSupport = new Map();
|
||||
@@ -127,8 +127,8 @@ describe("fixNotificationCountOnDecryption", () => {
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 1);
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
|
||||
|
||||
event.getPushActions = jest.fn().mockReturnValue(mkPushAction(false, false));
|
||||
threadEvent.getPushActions = jest.fn().mockReturnValue(mkPushAction(false, false));
|
||||
event.getPushActions = vi.fn().mockReturnValue(mkPushAction(false, false));
|
||||
threadEvent.getPushActions = vi.fn().mockReturnValue(mkPushAction(false, false));
|
||||
});
|
||||
|
||||
it("changes the room count to highlight on decryption", () => {
|
||||
@@ -200,8 +200,8 @@ describe("fixNotificationCountOnDecryption", () => {
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, 0);
|
||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, 0);
|
||||
|
||||
event.getPushActions = jest.fn().mockReturnValue(mkPushAction(true, false));
|
||||
mockClient.getPushActionsForEvent = jest.fn().mockReturnValue(mkPushAction(false, false));
|
||||
event.getPushActions = vi.fn().mockReturnValue(mkPushAction(true, false));
|
||||
mockClient.getPushActionsForEvent = vi.fn().mockReturnValue(mkPushAction(false, false));
|
||||
|
||||
fixNotificationCountOnDecryption(mockClient, event);
|
||||
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
|
||||
@@ -212,8 +212,8 @@ describe("fixNotificationCountOnDecryption", () => {
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0);
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
|
||||
|
||||
threadEvent.getPushActions = jest.fn().mockReturnValue(mkPushAction(true, false));
|
||||
mockClient.getPushActionsForEvent = jest.fn().mockReturnValue(mkPushAction(false, false));
|
||||
threadEvent.getPushActions = vi.fn().mockReturnValue(mkPushAction(true, false));
|
||||
mockClient.getPushActionsForEvent = vi.fn().mockReturnValue(mkPushAction(false, false));
|
||||
|
||||
fixNotificationCountOnDecryption(mockClient, event);
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(0);
|
||||
@@ -221,7 +221,7 @@ describe("fixNotificationCountOnDecryption", () => {
|
||||
});
|
||||
|
||||
it("emits events", () => {
|
||||
const cb = jest.fn();
|
||||
const cb = vi.fn();
|
||||
room.on(RoomEvent.UnreadNotifications, cb);
|
||||
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user