Compare commits
265 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 32c0b81332 | |||
| 634b8ebbb4 | |||
| 4ab8066e1f | |||
| 13dccb3d71 | |||
| f41fa84e72 | |||
| 3c4134537a | |||
| d4414341d6 | |||
| f173014fc0 | |||
| 81cb44db7f | |||
| 71f9b25db7 | |||
| 9c5c7ddb17 | |||
| feb424b0a9 | |||
| 648a1a09b1 | |||
| 56c5375bbd | |||
| b29e1e9d21 | |||
| 7ade461a4c | |||
| b5414ea914 | |||
| 711bf4710d | |||
| 48b60bb885 | |||
| 7ed14dc749 | |||
| 26736b6bb1 | |||
| 056aae823d | |||
| d1bfdca0c9 | |||
| c0577c29c4 | |||
| a2d1dee0a1 | |||
| d7bcdff29b | |||
| ed71cdeccd | |||
| 729f924de1 | |||
| 4732098731 | |||
| a7d503a8ac | |||
| 063d69eff1 | |||
| 614f446361 | |||
| b62fac9dad | |||
| ff07cb642f | |||
| fba3cf2756 | |||
| 5973c66726 | |||
| b22fc1f9d9 | |||
| 3f48a954d8 | |||
| b5d544df68 | |||
| cd26ba67d4 | |||
| 488390365a | |||
| b7b1129478 | |||
| 0fa9528a3f | |||
| a7b1dcaf95 | |||
| bb5bccbf78 | |||
| ece3ccb958 | |||
| a769cf88f7 | |||
| 8b0f1a0c59 | |||
| 978e748246 | |||
| 32a5cc4728 | |||
| d580f56c0b | |||
| 789458e0d4 | |||
| 8d14d45272 | |||
| 2ec1fa6605 | |||
| f15d682938 | |||
| 21a10a2d14 | |||
| fc02e550bd | |||
| 78637a0689 | |||
| 4ca882fcd4 | |||
| 13ee0eb7f5 | |||
| cb018dfc80 | |||
| 7574dacdb3 | |||
| 0c417b7c32 | |||
| daf845d7bd | |||
| 52792ec89b | |||
| 6dc4a62e8c | |||
| 1cd8ea61ea | |||
| 0c5eb277e4 | |||
| d459a91af3 | |||
| 18722d0031 | |||
| 5119934268 | |||
| 077da23d08 | |||
| 083b4cb17e | |||
| 4316009401 | |||
| 72f3c360b6 | |||
| fcbc195fbe | |||
| af38021d28 | |||
| 5e8cb9fa18 | |||
| 6ef9f6c55e | |||
| e6a3b0ebc0 | |||
| aaae55736f | |||
| 9e586ab634 | |||
| 7ff44d4a50 | |||
| 63abd00ca7 | |||
| 40f2579158 | |||
| ceb2a57feb | |||
| 90e8336797 | |||
| 73ca9c9ed2 | |||
| cc065c2772 | |||
| 028be0fee2 | |||
| d4500da59a | |||
| b6aef6772e | |||
| 49696cecbd | |||
| 83cb52c89c | |||
| e82bae2c4d | |||
| ee2b0204aa | |||
| 1ec7670f6a | |||
| 8ab2e10471 | |||
| 8ff8685ae5 | |||
| f3772cdf82 | |||
| ee2f1cdfd4 | |||
| 0de73a0b3e | |||
| be742e811c | |||
| ca9263fa64 | |||
| 9f619be08d | |||
| 7792254c12 | |||
| fff41f1f27 | |||
| 3e0f9f582e | |||
| 47ba8cfa24 | |||
| 54bb4c8011 | |||
| 71e763263e | |||
| 2ebcda2a55 | |||
| e10db6f7e9 | |||
| 1e041a2957 | |||
| 1631d6f3c0 | |||
| 3d86821258 | |||
| 261bc81554 | |||
| 8f701f43fb | |||
| 4bdb9111dd | |||
| 93e2135223 | |||
| 56cb05aac0 | |||
| 38d5b202c9 | |||
| cfffa9c518 | |||
| 73dbd709d8 | |||
| eef67e2c03 | |||
| e3498f0668 | |||
| f04c147faa | |||
| 33bbd45f1e | |||
| fd91a534c7 | |||
| c2afc357b9 | |||
| 1d3f67f2ce | |||
| 514e4f07f1 | |||
| fbb1c4b2bd | |||
| 63dea599b1 | |||
| 743ba5f050 | |||
| cb180b4195 | |||
| c805b7e29d | |||
| 8f6814450b | |||
| 37e8391cde | |||
| 90234402a7 | |||
| 8ecf603d73 | |||
| af243581ff | |||
| 01afed9ff9 | |||
| 2687bb37fb | |||
| 65b1a10803 | |||
| 9d230ef0d6 | |||
| a03438f2af | |||
| c622e9260f | |||
| 8bf53d6f90 | |||
| 8c30a3b0df | |||
| c61d53eed0 | |||
| 95f7d1d347 | |||
| 72d70bb929 | |||
| 4f67e59692 | |||
| d40d5c8a39 | |||
| 87398ac555 | |||
| de3d5ead42 | |||
| 1e1b571b28 | |||
| f400a7b1b2 | |||
| a0bcb5777f | |||
| f8a625eddb | |||
| 69a2a15b95 | |||
| b9d0596dd7 | |||
| 72af8c193c | |||
| ed8c326856 | |||
| f5bf6b1be6 | |||
| 0e19f8dc69 | |||
| 6049c0bf37 | |||
| a102253f30 | |||
| f71d86f005 | |||
| 70e34ffb76 | |||
| 91aa7b26e6 | |||
| 7d45947fb3 | |||
| 6e174328e8 | |||
| 5fb97fcce4 | |||
| 3d1a450129 | |||
| a58c5aacdf | |||
| d7e165a279 | |||
| a57ee803f1 | |||
| 170a52b09f | |||
| 2be5889d18 | |||
| ca6b574bee | |||
| 57b0172a2d | |||
| 53260ee25d | |||
| 964281322f | |||
| 7c3f483396 | |||
| 59784aa9fe | |||
| 72a2b6d571 | |||
| 5854af0eae | |||
| acd3d3a804 | |||
| 1b8c04a430 | |||
| 378b73f8b8 | |||
| c482a6ab15 | |||
| 2daa429b77 | |||
| 6ebbc15359 | |||
| 9a840d484c | |||
| e89467c9fb | |||
| 0b396c005c | |||
| d05313f95e | |||
| 3f97853011 | |||
| 2a5c4b1edf | |||
| 65a3c6707c | |||
| 942477f0bf | |||
| 1770b3131a | |||
| 41d3ffdab9 | |||
| 78aa6cb62b | |||
| b56b8040e6 | |||
| d1cf98b177 | |||
| 5fc6b3ed17 | |||
| 6e41533fed | |||
| bc76532bd5 | |||
| 62f1dd79bc | |||
| fd3f53e814 | |||
| 798ac7b94c | |||
| eb0c0f7b93 | |||
| f03293f53d | |||
| 7d062387b7 | |||
| 9acf3b18ca | |||
| b41d067c94 | |||
| c8503b3120 | |||
| da03c3b529 | |||
| d48b19e052 | |||
| 6861c67f56 | |||
| c87048bd9f | |||
| 02269f33b7 | |||
| 7f46ae7b97 | |||
| 2ab3566f95 | |||
| 9a504af18e | |||
| 8b50986906 | |||
| f9a222ecea | |||
| 5b5a3d8b5e | |||
| 037cbdd214 | |||
| 0349411e6d | |||
| d7b75e4b9e | |||
| 254f043ab0 | |||
| 5f3e115545 | |||
| fc55c4c72a | |||
| f795577e14 | |||
| f12cee984a | |||
| c3b4572841 | |||
| 40fe159c10 | |||
| ddecc87947 | |||
| 23837266fc | |||
| 3c9ca8c373 | |||
| 7f2a4c2568 | |||
| 2ad647a73c | |||
| 7faba5c2f0 | |||
| 1d9250b277 | |||
| 08054c1d6d | |||
| 333872e878 | |||
| 913cd257f4 | |||
| 69f7789c40 | |||
| e79ef1f33a | |||
| fb8f61a5ec | |||
| 9b1f2a1d11 | |||
| 686216fb75 | |||
| f4b83e1a27 | |||
| 97f21b6635 | |||
| 87641a6803 | |||
| 7e4331172a | |||
| a976080d1b | |||
| bcf3bba44e | |||
| 54ac36d424 | |||
| e84c90dbbc | |||
| 4424438658 |
@@ -69,9 +69,6 @@ module.exports = {
|
||||
// TODO: There are many tests with invalid expects that should be fixed,
|
||||
// https://github.com/matrix-org/matrix-js-sdk/issues/2976
|
||||
"jest/valid-expect": "off",
|
||||
// TODO: There are many cases to refactor away,
|
||||
// https://github.com/matrix-org/matrix-js-sdk/issues/2978
|
||||
"jest/no-conditional-expect": "off",
|
||||
// Also treat "oldBackendOnly" as a test function.
|
||||
// Used in some crypto tests.
|
||||
"jest/no-standalone-expect": [
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
)
|
||||
)
|
||||
steps:
|
||||
- uses: tibdex/backport@v2
|
||||
- uses: tibdex/backport@2e217641d82d02ba0603f46b1aeedefb258890ac # v2
|
||||
with:
|
||||
labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>"
|
||||
# We can't use GITHUB_TOKEN here or CI won't run on the new PR
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
|
||||
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
|
||||
- name: 📥 Download artifact
|
||||
uses: dawidd6/action-download-artifact@5e780fc7bbd0cac69fc73271ed86edf5dcb72d67 # v2.26.0
|
||||
uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2
|
||||
with:
|
||||
workflow: static_analysis.yml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
path: docs
|
||||
|
||||
- name: 📤 Deploy to Netlify
|
||||
uses: matrix-org/netlify-pr-preview@v1
|
||||
uses: matrix-org/netlify-pr-preview@v2
|
||||
with:
|
||||
path: docs
|
||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
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@v2
|
||||
uses: peter-evans/repository-dispatch@26b39ed245ab8f31526069329e112ab2fb224588 # v2
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
repository: ${{ matrix.repo }}
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref }}
|
||||
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref || github.head_ref || github.ref }}
|
||||
jobs:
|
||||
changelog:
|
||||
name: Preview Changelog
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
if: github.event.action == 'opened'
|
||||
steps:
|
||||
- name: Check membership
|
||||
uses: tspascoal/get-user-teams-membership@v2
|
||||
uses: tspascoal/get-user-teams-membership@37c08f7b52a72ca95d12af2e7ab2553ca9adf13b # v2
|
||||
id: teams
|
||||
with:
|
||||
username: ${{ github.event.pull_request.user.login }}
|
||||
|
||||
@@ -20,11 +20,11 @@ jobs:
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --pure-lockfile"
|
||||
run: "yarn install --frozen-lockfile"
|
||||
|
||||
- name: 🚀 Publish to npm
|
||||
id: npm-publish
|
||||
uses: JS-DevTools/npm-publish@v1
|
||||
uses: JS-DevTools/npm-publish@0f451a94170d1699fd50710966d48fb26194d939 # v1
|
||||
with:
|
||||
token: ${{ secrets.NPM_TOKEN }}
|
||||
access: public
|
||||
|
||||
@@ -11,46 +11,39 @@ jobs:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: 🧮 Checkout gh-pages
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: gh-pages
|
||||
path: _docs
|
||||
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --pure-lockfile"
|
||||
run: "yarn install --frozen-lockfile"
|
||||
|
||||
- name: 📖 Generate JSDoc
|
||||
run: "yarn gendoc"
|
||||
|
||||
- name: 📋 Copy to temp
|
||||
- name: 🔨 Install symlinks
|
||||
run: |
|
||||
cp -a "./_docs" "$RUNNER_TEMP/"
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y symlinks
|
||||
|
||||
- name: 🧮 Checkout gh-pages
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: gh-pages
|
||||
|
||||
- name: 🔪 Prepare
|
||||
env:
|
||||
GITHUB_REF_NAME: ${{ github.ref_name }}
|
||||
- name: 📖 Generate docs
|
||||
run: |
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
[ ! -e "$VERSION" ] || rm -r $VERSION
|
||||
cp -r $RUNNER_TEMP/_docs/ $VERSION
|
||||
|
||||
# Add the new directory to the index if it isn't there already
|
||||
if ! grep -q ">Version $VERSION</a>" index.html; then
|
||||
perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print
|
||||
"<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' "$VERSION" index.html
|
||||
fi
|
||||
yarn tpv purge --yes --out _docs --stale --major 10
|
||||
yarn gendoc
|
||||
symlinks -rc _docs
|
||||
|
||||
- name: 🚀 Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
keep_files: true
|
||||
publish_dir: .
|
||||
run: |
|
||||
git config --global user.email "releases@riot.im"
|
||||
git config --global user.name "RiotRobot"
|
||||
git add . --all
|
||||
git commit -m "Update docs"
|
||||
git push
|
||||
working-directory: _docs
|
||||
|
||||
npm:
|
||||
name: Publish
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
# We create the status here and then update it to success/failure in the `report` stage
|
||||
# This provides an easy link to this workflow_run from the PR before Cypress is done.
|
||||
- uses: Sibz/github-status-action@v1
|
||||
- uses: Sibz/github-status-action@faaa4d96fecf273bd762985e0e7f9f933c774918 # v1
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: pending
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
- name: "🩻 SonarCloud Scan"
|
||||
id: sonarcloud
|
||||
uses: matrix-org/sonarcloud-workflow-action@v2.3
|
||||
uses: matrix-org/sonarcloud-workflow-action@v2.5
|
||||
# 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:
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
coverage_extract_path: coverage
|
||||
extra_args: ${{ inputs.extra_args }}
|
||||
|
||||
- uses: Sibz/github-status-action@v1
|
||||
- uses: Sibz/github-status-action@faaa4d96fecf273bd762985e0e7f9f933c774918 # v1
|
||||
if: always()
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -11,6 +11,7 @@ jobs:
|
||||
# This is a workaround for https://github.com/SonarSource/SonarJS/issues/578
|
||||
prepare:
|
||||
name: Prepare
|
||||
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event != 'merge_group'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
reportPaths: ${{ steps.extra_args.outputs.reportPaths }}
|
||||
@@ -19,7 +20,7 @@ jobs:
|
||||
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
|
||||
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
|
||||
- name: 📥 Download artifact
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2
|
||||
with:
|
||||
workflow: tests.yaml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
@@ -35,6 +36,7 @@ jobs:
|
||||
|
||||
sonarqube:
|
||||
name: 🩻 SonarQube
|
||||
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event != 'merge_group'
|
||||
needs: prepare
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
|
||||
secrets:
|
||||
|
||||
@@ -65,7 +65,14 @@ jobs:
|
||||
run: "yarn install"
|
||||
|
||||
- name: Generate Docs
|
||||
run: "yarn run gendoc"
|
||||
run: "yarn run gendoc --treatWarningsAsErrors"
|
||||
|
||||
# Upload artifact duplicates symlink contents so we do this to save 75% space
|
||||
- name: Flatten symlink and write _redirects
|
||||
run: |
|
||||
find _docs -mindepth 1 -maxdepth 1 ! -type f ! -name stable -printf '/%f/* /stable/:splat\n' > _docs/_redirects
|
||||
find _docs -mindepth 1 -maxdepth 1 -type l -delete
|
||||
find _docs -mindepth 1 -maxdepth 1 -type d -execdir mv {} stable \; -quit
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
|
||||
+23
-11
@@ -8,6 +8,8 @@ on:
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
env:
|
||||
ENABLE_COVERAGE: ${{ github.event_name != 'merge_group' }}
|
||||
jobs:
|
||||
jest:
|
||||
name: "Jest [${{ matrix.specs }}] (Node ${{ matrix.node }})"
|
||||
@@ -36,26 +38,24 @@ jobs:
|
||||
|
||||
- name: Get number of CPU cores
|
||||
id: cpu-cores
|
||||
uses: SimenB/github-actions-cpu-cores@v1
|
||||
|
||||
- name: Load metrics reporter
|
||||
id: metrics
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
run: |
|
||||
echo "extra-reporter='--reporters=<rootDir>/spec/slowReporter.js'" >> $GITHUB_OUTPUT
|
||||
uses: SimenB/github-actions-cpu-cores@410541432439795d30db6501fb1d8178eb41e502 # v1
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
yarn coverage \
|
||||
yarn test \
|
||||
--coverage=${{ env.ENABLE_COVERAGE }} \
|
||||
--ci \
|
||||
--reporters github-actions ${{ steps.metrics.outputs.extra-reporter }} \
|
||||
--max-workers ${{ steps.cpu-cores.outputs.count }} \
|
||||
./spec/${{ matrix.specs }}
|
||||
mv coverage/lcov.info coverage/${{ matrix.node }}-${{ matrix.specs }}.lcov.info
|
||||
env:
|
||||
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
|
||||
|
||||
- name: Move coverage files into place
|
||||
if: env.ENABLE_COVERAGE == 'true'
|
||||
run: mv coverage/lcov.info coverage/${{ matrix.node }}-${{ matrix.specs }}.lcov.info
|
||||
|
||||
- name: Upload Artifact
|
||||
if: env.ENABLE_COVERAGE == 'true'
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: coverage
|
||||
@@ -71,7 +71,8 @@ jobs:
|
||||
disable_coverage: true
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
|
||||
# Hook for branch protection to work outside merge queues
|
||||
# Hook for branch protection to skip downstream testing outside of merge queues
|
||||
# and skip sonarcloud coverage within merge queues
|
||||
downstream:
|
||||
name: Downstream tests
|
||||
runs-on: ubuntu-latest
|
||||
@@ -79,5 +80,16 @@ jobs:
|
||||
needs:
|
||||
- matrix-react-sdk
|
||||
steps:
|
||||
- name: Skip SonarCloud on merge queues
|
||||
if: env.ENABLE_COVERAGE == 'false'
|
||||
uses: Sibz/github-status-action@faaa4d96fecf273bd762985e0e7f9f933c774918 # v1
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
description: SonarCloud skipped
|
||||
context: SonarCloud Code Analysis
|
||||
sha: ${{ github.sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- if: needs.matrix-react-sdk.result != 'skipped' && needs.matrix-react-sdk.result != 'success'
|
||||
run: exit 1
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
uses: peter-evans/create-pull-request@284f54f989303d2699d373481a0cfa13ad5a6666 # v5
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/upgrade-deps
|
||||
@@ -31,8 +31,8 @@ jobs:
|
||||
T-Task
|
||||
|
||||
- name: Enable automerge
|
||||
uses: peter-evans/enable-pull-request-automerge@v2
|
||||
run: gh pr merge --merge --auto "$PR_NUMBER"
|
||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
PR_NUMBER: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
|
||||
+101
@@ -1,3 +1,104 @@
|
||||
Changes in [26.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v26.0.1) (2023-06-09)
|
||||
==================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix: handle `baseUrl` with trailing slash in `fetch.getUrl` ([\#3455](https://github.com/matrix-org/matrix-js-sdk/pull/3455)). Fixes vector-im/element-web#25526. Contributed by @kerryarchibald.
|
||||
|
||||
Changes in [26.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v26.0.0) (2023-06-06)
|
||||
==================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Ensure we do not add relations to the wrong timeline ([\#3427](https://github.com/matrix-org/matrix-js-sdk/pull/3427)). Fixes vector-im/element-web#25450 and vector-im/element-web#25494.
|
||||
* Deprecate `QrCodeEvent`, `SasEvent` and `VerificationEvent` ([\#3386](https://github.com/matrix-org/matrix-js-sdk/pull/3386)).
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Move crypto classes into a separate namespace ([\#3385](https://github.com/matrix-org/matrix-js-sdk/pull/3385)).
|
||||
|
||||
## ✨ Features
|
||||
* Mention deno support in the README ([\#3417](https://github.com/matrix-org/matrix-js-sdk/pull/3417)). Contributed by @sigmaSd.
|
||||
* Mark room version 10 as safe ([\#3425](https://github.com/matrix-org/matrix-js-sdk/pull/3425)).
|
||||
* Prioritise entirely supported flows for UIA ([\#3402](https://github.com/matrix-org/matrix-js-sdk/pull/3402)).
|
||||
* Add methods to terminate idb worker ([\#3362](https://github.com/matrix-org/matrix-js-sdk/pull/3362)).
|
||||
* Total summary count ([\#3351](https://github.com/matrix-org/matrix-js-sdk/pull/3351)). Contributed by @toger5.
|
||||
* Audio concealment ([\#3349](https://github.com/matrix-org/matrix-js-sdk/pull/3349)). Contributed by @toger5.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Correctly accumulate sync summaries. ([\#3366](https://github.com/matrix-org/matrix-js-sdk/pull/3366)). Fixes vector-im/element-web#23345.
|
||||
* Keep measuring a call feed's volume after a stream replacement ([\#3361](https://github.com/matrix-org/matrix-js-sdk/pull/3361)). Fixes vector-im/element-call#1051.
|
||||
* Element-R: Avoid uploading a new fallback key at every `/sync` ([\#3338](https://github.com/matrix-org/matrix-js-sdk/pull/3338)). Fixes vector-im/element-web#25215.
|
||||
* Accumulate receipts for the main thread and unthreaded separately ([\#3339](https://github.com/matrix-org/matrix-js-sdk/pull/3339)). Fixes vector-im/element-web#24629.
|
||||
* Remove spec non-compliant extended glob format ([\#3423](https://github.com/matrix-org/matrix-js-sdk/pull/3423)). Fixes vector-im/element-web#25474.
|
||||
* Fix bug where original event was inserted into timeline instead of the edit event ([\#3398](https://github.com/matrix-org/matrix-js-sdk/pull/3398)). Contributed by @andybalaam.
|
||||
* Only add a local receipt if it's after an existing receipt ([\#3399](https://github.com/matrix-org/matrix-js-sdk/pull/3399)). Contributed by @andybalaam.
|
||||
* Attempt a potential workaround for stuck notifs ([\#3384](https://github.com/matrix-org/matrix-js-sdk/pull/3384)). Fixes vector-im/element-web#25406. Contributed by @andybalaam.
|
||||
* Fix verification bug with `pendingEventOrdering: "chronological"` ([\#3382](https://github.com/matrix-org/matrix-js-sdk/pull/3382)).
|
||||
|
||||
Changes in [25.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v25.1.1) (2023-05-16)
|
||||
==================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Rebuild to fix packaging glitch in 25.1.0. Fixes #3363
|
||||
|
||||
Changes in [25.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v25.1.0) (2023-05-09)
|
||||
==================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Deprecate MatrixClient::resolveRoomAlias ([\#3316](https://github.com/matrix-org/matrix-js-sdk/pull/3316)).
|
||||
|
||||
## ✨ Features
|
||||
* add client method to remove pusher ([\#3324](https://github.com/matrix-org/matrix-js-sdk/pull/3324)). Contributed by @kerryarchibald.
|
||||
* Implement MSC 3981 ([\#3248](https://github.com/matrix-org/matrix-js-sdk/pull/3248)). Fixes vector-im/element-web#25021. Contributed by @justjanne.
|
||||
* Added `Room.getLastLiveEvent` and `Room.getLastThread`. Deprecated `Room.lastThread` in favour of `Room.getLastThread`. ([\#3321](https://github.com/matrix-org/matrix-js-sdk/pull/3321)).
|
||||
* Element-R: wire up device lists ([\#3272](https://github.com/matrix-org/matrix-js-sdk/pull/3272)). Contributed by @florianduros.
|
||||
* Node 20 support ([\#3302](https://github.com/matrix-org/matrix-js-sdk/pull/3302)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix racing between one-time-keys processing and sync ([\#3327](https://github.com/matrix-org/matrix-js-sdk/pull/3327)). Fixes vector-im/element-web#25214. Contributed by @florianduros.
|
||||
* Fix lack of media when a user reconnects ([\#3318](https://github.com/matrix-org/matrix-js-sdk/pull/3318)).
|
||||
* Fix TimelineWindow getEvents exploding if no neigbouring timeline ([\#3285](https://github.com/matrix-org/matrix-js-sdk/pull/3285)). Fixes vector-im/element-web#25104.
|
||||
|
||||
Changes in [25.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v25.0.0) (2023-04-25)
|
||||
==================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Change `Store.save()` to return a `Promise` ([\#3221](https://github.com/matrix-org/matrix-js-sdk/pull/3221)). Contributed by @texuf.
|
||||
|
||||
## ✨ Features
|
||||
* Add typedoc-plugin-mdn-links ([\#3292](https://github.com/matrix-org/matrix-js-sdk/pull/3292)).
|
||||
* Annotate events with executed push rule ([\#3284](https://github.com/matrix-org/matrix-js-sdk/pull/3284)). Contributed by @kerryarchibald.
|
||||
* Element-R: pass device list change notifications into rust crypto-sdk ([\#3254](https://github.com/matrix-org/matrix-js-sdk/pull/3254)). Fixes vector-im/element-web#24795. Contributed by @florianduros.
|
||||
* Support for MSC3882 revision 1 ([\#3228](https://github.com/matrix-org/matrix-js-sdk/pull/3228)). Contributed by @hughns.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix screen sharing on Firefox 113 ([\#3282](https://github.com/matrix-org/matrix-js-sdk/pull/3282)). Contributed by @tulir.
|
||||
* Retry processing potential poll events after decryption ([\#3246](https://github.com/matrix-org/matrix-js-sdk/pull/3246)). Fixes vector-im/element-web#24568.
|
||||
* Element-R: handle events which arrive before their keys ([\#3230](https://github.com/matrix-org/matrix-js-sdk/pull/3230)). Fixes vector-im/element-web#24489.
|
||||
|
||||
Changes in [24.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v24.1.0) (2023-04-11)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Allow via_servers property in findPredecessor (update to MSC3946) ([\#3240](https://github.com/matrix-org/matrix-js-sdk/pull/3240)). Contributed by @andybalaam.
|
||||
* Fire `closed` event when IndexedDB closes unexpectedly ([\#3218](https://github.com/matrix-org/matrix-js-sdk/pull/3218)).
|
||||
* Implement MSC3952: intentional mentions ([\#3092](https://github.com/matrix-org/matrix-js-sdk/pull/3092)). Fixes vector-im/element-web#24376.
|
||||
* Send one time key count and unused fallback keys for rust-crypto ([\#3215](https://github.com/matrix-org/matrix-js-sdk/pull/3215)). Fixes vector-im/element-web#24795. Contributed by @florianduros.
|
||||
* Improve `processBeaconEvents` hotpath ([\#3200](https://github.com/matrix-org/matrix-js-sdk/pull/3200)).
|
||||
* Implement MSC3966: a push rule condition to check if an array contains a value ([\#3180](https://github.com/matrix-org/matrix-js-sdk/pull/3180)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* indexddb-local-backend - return the current sync to database promise … ([\#3222](https://github.com/matrix-org/matrix-js-sdk/pull/3222)). Contributed by @texuf.
|
||||
* Revert "Add the call object to Call events" ([\#3236](https://github.com/matrix-org/matrix-js-sdk/pull/3236)).
|
||||
* Handle group call redaction ([\#3231](https://github.com/matrix-org/matrix-js-sdk/pull/3231)). Fixes vector-im/voip-internal#128.
|
||||
* Stop doing O(n^2) work to find event's home (`eventShouldLiveIn`) ([\#3227](https://github.com/matrix-org/matrix-js-sdk/pull/3227)). Contributed by @jryans.
|
||||
* Fix bug where video would not unmute if it started muted ([\#3213](https://github.com/matrix-org/matrix-js-sdk/pull/3213)). Fixes vector-im/element-call#925.
|
||||
* Fixes to event encryption in the Rust Crypto implementation ([\#3202](https://github.com/matrix-org/matrix-js-sdk/pull/3202)).
|
||||
|
||||
Changes in [24.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v24.0.0) (2023-03-28)
|
||||
==================================================================================================
|
||||
|
||||
## 🔒 Security
|
||||
* Fixes for [CVE-2023-28427](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE-2023-28427) / GHSA-mwq8-fjpf-c2gr
|
||||
|
||||
Changes in [23.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.5.0) (2023-03-15)
|
||||
==================================================================================================
|
||||
|
||||
|
||||
@@ -55,6 +55,8 @@ client.publicRooms(function (err, data) {
|
||||
See below for how to include libolm to enable end-to-end-encryption. Please check
|
||||
[the Node.js terminal app](examples/node) for a more complex example.
|
||||
|
||||
You can also use the sdk with [Deno](https://deno.land/) (`import npm:matrix-js-sdk`) but its not officialy supported.
|
||||
|
||||
To start the client:
|
||||
|
||||
```javascript
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/* 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",
|
||||
};
|
||||
|
||||
// if we're running under GHA, enable the GHA reporter
|
||||
if (env["GITHUB_ACTIONS"] !== undefined) {
|
||||
const reporters: Config["reporters"] = [["github-actions", { silent: false }], "summary"];
|
||||
|
||||
// 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.js");
|
||||
}
|
||||
config.reporters = reporters;
|
||||
}
|
||||
|
||||
export default config;
|
||||
+16
-28
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "23.5.0",
|
||||
"version": "26.0.1",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
@@ -55,13 +55,13 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.3",
|
||||
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.9",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^5.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
"loglevel": "^1.7.1",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-widget-api": "^1.0.0",
|
||||
"matrix-widget-api": "^1.3.1",
|
||||
"p-retry": "4",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"unhomoglyph": "^1.0.6",
|
||||
@@ -101,16 +101,16 @@
|
||||
"debug": "^4.3.4",
|
||||
"docdash": "^2.0.0",
|
||||
"domexception": "^4.0.0",
|
||||
"eslint": "8.34.0",
|
||||
"eslint": "8.40.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jest": "^27.1.6",
|
||||
"eslint-plugin-jsdoc": "^40.0.0",
|
||||
"eslint-plugin-jsdoc": "^44.0.0",
|
||||
"eslint-plugin-matrix-org": "^1.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.2.17",
|
||||
"eslint-plugin-unicorn": "^45.0.0",
|
||||
"eslint-plugin-unicorn": "^47.0.0",
|
||||
"exorcist": "^2.0.0",
|
||||
"fake-indexeddb": "^4.0.0",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
@@ -119,30 +119,18 @@
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jest-mock": "^29.0.0",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"prettier": "2.8.4",
|
||||
"rimraf": "^4.0.0",
|
||||
"prettier": "2.8.8",
|
||||
"rimraf": "^5.0.0",
|
||||
"terser": "^5.5.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsify": "^5.0.2",
|
||||
"typedoc": "^0.23.20",
|
||||
"typedoc-plugin-missing-exports": "^1.0.0",
|
||||
"typescript": "^4.5.3"
|
||||
},
|
||||
"jest": {
|
||||
"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"
|
||||
"typedoc": "^0.24.0",
|
||||
"typedoc-plugin-coverage": "^2.1.0",
|
||||
"typedoc-plugin-mdn-links": "^3.0.3",
|
||||
"typedoc-plugin-missing-exports": "^2.0.0",
|
||||
"typedoc-plugin-versions": "^0.2.3",
|
||||
"typedoc-plugin-versions-cli": "^0.1.12",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"@casualbot/jest-sonar-reporter": {
|
||||
"outputDirectory": "coverage",
|
||||
|
||||
+2
-2
@@ -130,7 +130,7 @@ fi
|
||||
# global cache here to ensure we get the right thing.
|
||||
yarn cache clean
|
||||
# Ensure all dependencies are updated
|
||||
yarn install --ignore-scripts --pure-lockfile
|
||||
yarn install --ignore-scripts --frozen-lockfile
|
||||
|
||||
# ignore leading v on release
|
||||
release="${1#v}"
|
||||
@@ -225,7 +225,7 @@ if [ $dodist -eq 0 ]; then
|
||||
pushd "$builddir"
|
||||
git clone "$projdir" .
|
||||
git checkout "$rel_branch"
|
||||
yarn install --pure-lockfile
|
||||
yarn install --frozen-lockfile
|
||||
# We haven't tagged yet, so tell the dist script what version
|
||||
# it's building
|
||||
DIST_VERSION="$tag" yarn dist
|
||||
|
||||
@@ -15,4 +15,4 @@ for line in sys.stdin:
|
||||
break
|
||||
found_first_header = True
|
||||
elif not re.match(r"^=+$", line) and len(line) > 0:
|
||||
print line
|
||||
print(line)
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
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 fetchMock from "fetch-mock-jest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
|
||||
import { CRYPTO_BACKENDS, InitCrypto } from "../test-utils/test-utils";
|
||||
import { createClient, MatrixClient, UIAuthCallback } from "../../src";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = new IDBFactory();
|
||||
});
|
||||
|
||||
const TEST_USER_ID = "@alice:localhost";
|
||||
const TEST_DEVICE_ID = "xzcvb";
|
||||
|
||||
/**
|
||||
* Integration tests for cross-signing functionality.
|
||||
*
|
||||
* These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as
|
||||
* to provide the most effective integration tests possible.
|
||||
*/
|
||||
describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: string, initCrypto: InitCrypto) => {
|
||||
let aliceClient: MatrixClient;
|
||||
|
||||
beforeEach(async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
|
||||
const homeserverUrl = "https://alice-server.com";
|
||||
aliceClient = createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
userId: TEST_USER_ID,
|
||||
accessToken: "akjgkrgjs",
|
||||
deviceId: TEST_DEVICE_ID,
|
||||
});
|
||||
|
||||
await initCrypto(aliceClient);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await aliceClient.stopClient();
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
describe("bootstrapCrossSigning (before initialsync completes)", () => {
|
||||
it("publishes keys if none were yet published", async () => {
|
||||
// have account_data requests return an empty object
|
||||
fetchMock.get("express:/_matrix/client/r0/user/:userId/account_data/:type", {});
|
||||
|
||||
// we expect a request to upload signatures for our device ...
|
||||
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});
|
||||
|
||||
// ... and one to upload the cross-signing keys (with UIA)
|
||||
fetchMock.post(
|
||||
// legacy crypto uses /unstable/; /v3/ is correct
|
||||
{
|
||||
url: new RegExp("/_matrix/client/(unstable|v3)/keys/device_signing/upload"),
|
||||
name: "upload-keys",
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
// provide a UIA callback, so that the cross-signing keys are uploaded
|
||||
const authDict = { type: "test" };
|
||||
const uiaCallback: UIAuthCallback<void> = async (makeRequest) => {
|
||||
await makeRequest(authDict);
|
||||
};
|
||||
|
||||
// now bootstrap cross signing, and check it resolves successfully
|
||||
await aliceClient.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: uiaCallback,
|
||||
});
|
||||
|
||||
// check the cross-signing keys upload
|
||||
expect(fetchMock.called("upload-keys")).toBeTruthy();
|
||||
const [, keysOpts] = fetchMock.lastCall("upload-keys")!;
|
||||
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}]`);
|
||||
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}]`);
|
||||
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")!;
|
||||
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}]`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+371
-47
@@ -19,7 +19,7 @@ import anotherjson from "another-json";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import { MockResponse } from "fetch-mock";
|
||||
import { MockResponse, MockResponseFunction } from "fetch-mock";
|
||||
|
||||
import type { IDeviceKeys } from "../../src/@types/crypto";
|
||||
import * as testUtils from "../test-utils/test-utils";
|
||||
@@ -47,6 +47,9 @@ import {
|
||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||
import { E2EKeyReceiver, IE2EKeyReceiver } from "../test-utils/E2EKeyReceiver";
|
||||
import { ISyncResponder, SyncResponder } from "../test-utils/SyncResponder";
|
||||
import { escapeRegExp } from "../../src/utils";
|
||||
import { downloadDeviceToJsDevice } from "../../src/rust-crypto/device-converter";
|
||||
import { flushPromises } from "../test-utils/flushPromises";
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
|
||||
@@ -341,6 +344,11 @@ async function expectSendRoomKey(
|
||||
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,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -359,12 +367,20 @@ async function expectSendMegolmMessage(
|
||||
inboundGroupSessionPromise: Promise<Olm.InboundGroupSession>,
|
||||
): Promise<Partial<IEvent>> {
|
||||
const encryptedMessageContent = await new Promise<IContent>((resolve) => {
|
||||
fetchMock.putOnce(new RegExp("/send/m.room.encrypted/"), (url: string, opts: RequestInit): MockResponse => {
|
||||
resolve(JSON.parse(opts.body as string));
|
||||
return {
|
||||
event_id: "$event_id",
|
||||
};
|
||||
});
|
||||
fetchMock.putOnce(
|
||||
new RegExp("/send/m.room.encrypted/"),
|
||||
(url: string, opts: RequestInit): MockResponse => {
|
||||
resolve(JSON.parse(opts.body as string));
|
||||
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,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// In some of the tests, the room key is sent *after* the actual event, so we may need to wait for it now.
|
||||
@@ -438,8 +454,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
const rootRegexp = escapeRegExp(new URL("/_matrix/client/", aliceClient.getHomeserverUrl()).toString());
|
||||
fetchMock.postOnce(
|
||||
new URL("/_matrix/client/r0/keys/query", aliceClient.getHomeserverUrl()).toString(),
|
||||
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
|
||||
@@ -448,6 +465,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an expectation for a /keys/claim request for the MatrixClient under test
|
||||
*
|
||||
* @param response - the response to return from the request. Normally an {@link IClaimOTKsResult}
|
||||
* (or a function that returns one).
|
||||
*/
|
||||
function expectAliceKeyClaim(response: MockResponse | MockResponseFunction) {
|
||||
const rootRegexp = escapeRegExp(new URL("/_matrix/client/", aliceClient.getHomeserverUrl()).toString());
|
||||
fetchMock.postOnce(new RegExp(rootRegexp + "(r0|v3)/keys/claim"), response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the device keys for testOlmAccount in a format suitable for a
|
||||
* response to /keys/query
|
||||
@@ -536,6 +564,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
it("MatrixClient.getCrypto returns a CryptoApi", () => {
|
||||
expect(aliceClient.getCrypto()).toHaveProperty("globalBlacklistUnverifiedDevices");
|
||||
});
|
||||
|
||||
it("Alice receives a megolm message", async () => {
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
@@ -543,7 +575,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
// if we're using the old crypto impl, stub out some methods in the device manager.
|
||||
// TODO: replace this with intercepts of the /keys/query endpoint to make it impl agnostic.
|
||||
if (aliceClient.crypto) {
|
||||
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
aliceClient.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
}
|
||||
|
||||
@@ -594,16 +626,14 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
expect(decryptedEvent.getContent().body).toEqual("42");
|
||||
});
|
||||
|
||||
oldBackendOnly("Alice receives a megolm message before the session keys", async () => {
|
||||
it("Alice receives a megolm message before the session keys", async () => {
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
|
||||
// https://github.com/vector-im/element-web/issues/2273
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
// if we're using the old crypto impl, stub out some methods in the device manager.
|
||||
// TODO: replace this with intercepts of the /keys/query endpoint to make it impl agnostic.
|
||||
if (aliceClient.crypto) {
|
||||
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
aliceClient.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
}
|
||||
|
||||
@@ -637,7 +667,11 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const room = aliceClient.getRoom(ROOM_ID)!;
|
||||
expect(room.getLiveTimeline().getEvents()[0].getContent().msgtype).toEqual("m.bad.encrypted");
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
|
||||
// wait for a first attempt at decryption: should fail
|
||||
await testUtils.awaitDecryption(event);
|
||||
expect(event.getContent().msgtype).toEqual("m.bad.encrypted");
|
||||
|
||||
// now she gets the room_key event
|
||||
syncResponder.sendOrQueueSyncResponse({
|
||||
@@ -648,20 +682,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
});
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
|
||||
let decryptedEvent: MatrixEvent;
|
||||
if (event.getContent().msgtype != "m.bad.encrypted") {
|
||||
decryptedEvent = event;
|
||||
} else {
|
||||
decryptedEvent = await new Promise<MatrixEvent>((resolve) => {
|
||||
event.once(MatrixEventEvent.Decrypted, (ev) => {
|
||||
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
||||
resolve(ev);
|
||||
});
|
||||
});
|
||||
}
|
||||
expect(decryptedEvent.getContent().body).toEqual("42");
|
||||
await testUtils.awaitDecryption(event, { waitOnDecryptionFailure: true });
|
||||
expect(event.getContent().body).toEqual("42");
|
||||
});
|
||||
|
||||
it("Alice gets a second room_key message", async () => {
|
||||
@@ -671,7 +693,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
// if we're using the old crypto impl, stub out some methods in the device manager.
|
||||
// TODO: replace this with intercepts of the /keys/query endpoint to make it impl agnostic.
|
||||
if (aliceClient.crypto) {
|
||||
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
aliceClient.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
}
|
||||
|
||||
@@ -738,7 +760,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
expect(event.getContent().body).toEqual("42");
|
||||
});
|
||||
|
||||
oldBackendOnly("prepareToEncrypt", async () => {
|
||||
it("prepareToEncrypt", async () => {
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
aliceClient.setGlobalErrorOnUnknownDevices(false);
|
||||
@@ -751,10 +773,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
expectAliceKeyQuery(getTestKeysQueryResponse("@bob:xyz"));
|
||||
|
||||
// ... and then claim one of his OTKs
|
||||
fetchMock.postOnce(
|
||||
new URL("/_matrix/client/r0/keys/claim", aliceClient.getHomeserverUrl()).toString(),
|
||||
getTestKeysClaimResponse("@bob:xyz"),
|
||||
);
|
||||
expectAliceKeyClaim(getTestKeysClaimResponse("@bob:xyz"));
|
||||
|
||||
// fire off the prepare request
|
||||
const room = aliceClient.getRoom(ROOM_ID);
|
||||
@@ -768,7 +787,71 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
await p;
|
||||
});
|
||||
|
||||
it("Alice sends a megolm message with GlobalErrorOnUnknownDevices=false", async () => {
|
||||
aliceClient.setGlobalErrorOnUnknownDevices(false);
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
// Alice shares a room with Bob
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"]));
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// Once we send the message, Alice will check Bob's device list (twice, because reasons) ...
|
||||
expectAliceKeyQuery(getTestKeysQueryResponse("@bob:xyz"));
|
||||
expectAliceKeyQuery(getTestKeysQueryResponse("@bob:xyz"));
|
||||
|
||||
// ... and claim one of his OTKs ...
|
||||
expectAliceKeyClaim(getTestKeysClaimResponse("@bob:xyz"));
|
||||
|
||||
// ... and send an m.room_key message
|
||||
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount);
|
||||
|
||||
// Finally, send the message, and expect to get an `m.room.encrypted` event that we can decrypt.
|
||||
await Promise.all([
|
||||
aliceClient.sendTextMessage(ROOM_ID, "test"),
|
||||
expectSendMegolmMessage(inboundGroupSessionPromise),
|
||||
]);
|
||||
});
|
||||
|
||||
it("We should start a new megolm session after forceDiscardSession", async () => {
|
||||
aliceClient.setGlobalErrorOnUnknownDevices(false);
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
// Alice shares a room with Bob
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"]));
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// Once we send the message, Alice will check Bob's device list (twice, because reasons) ...
|
||||
expectAliceKeyQuery(getTestKeysQueryResponse("@bob:xyz"));
|
||||
expectAliceKeyQuery(getTestKeysQueryResponse("@bob:xyz"));
|
||||
|
||||
// ... and claim one of his OTKs ...
|
||||
expectAliceKeyClaim(getTestKeysClaimResponse("@bob:xyz"));
|
||||
|
||||
// ... and send an m.room_key message
|
||||
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount);
|
||||
|
||||
// Send the first message, and check we can decrypt it.
|
||||
await Promise.all([
|
||||
aliceClient.sendTextMessage(ROOM_ID, "test"),
|
||||
expectSendMegolmMessage(inboundGroupSessionPromise),
|
||||
]);
|
||||
|
||||
// Finally the interesting part: discard the session.
|
||||
aliceClient.forceDiscardSession(ROOM_ID);
|
||||
|
||||
// Now when we send the next message, we should get a *new* megolm session.
|
||||
const inboundGroupSessionPromise2 = expectSendRoomKey("@bob:xyz", testOlmAccount);
|
||||
const p2 = expectSendMegolmMessage(inboundGroupSessionPromise2);
|
||||
await Promise.all([aliceClient.sendTextMessage(ROOM_ID, "test2"), p2]);
|
||||
});
|
||||
|
||||
oldBackendOnly("Alice sends a megolm message", async () => {
|
||||
// TODO: do something about this for the rust backend.
|
||||
// Currently it fails because we don't respect the default GlobalErrorOnUnknownDevices and
|
||||
// send messages to unknown devices.
|
||||
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
const p2pSession = await establishOlmSession(aliceClient, keyReceiver, syncResponder, testOlmAccount);
|
||||
@@ -1027,20 +1110,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
throw new Error("sendTextMessage succeeded on an unknown device");
|
||||
} catch (e) {
|
||||
expect((e as any).name).toEqual("UnknownDeviceError");
|
||||
expect(Object.keys((e as any).devices)).toEqual([aliceClient.getUserId()!]);
|
||||
expect(Object.keys((e as any)?.devices[aliceClient.getUserId()!])).toEqual(["DEVICE_ID"]);
|
||||
expect([...(e as any).devices.keys()]).toEqual([aliceClient.getUserId()!]);
|
||||
expect((e as any).devices.get(aliceClient.getUserId()!).has("DEVICE_ID")).toBeTruthy();
|
||||
}
|
||||
|
||||
// mark the device as known, and resend.
|
||||
aliceClient.setDeviceKnown(aliceClient.getUserId()!, "DEVICE_ID");
|
||||
fetchMock.postOnce(
|
||||
new URL("/_matrix/client/r0/keys/claim", aliceClient.getHomeserverUrl()).toString(),
|
||||
(url: string, opts: RequestInit): MockResponse => {
|
||||
const content = JSON.parse(opts.body as string);
|
||||
expect(content.one_time_keys[aliceClient.getUserId()!].DEVICE_ID).toEqual("signed_curve25519");
|
||||
return getTestKeysClaimResponse(aliceClient.getUserId()!);
|
||||
},
|
||||
);
|
||||
expectAliceKeyClaim((url: string, opts: RequestInit): MockResponse => {
|
||||
const content = JSON.parse(opts.body as string);
|
||||
expect(content.one_time_keys[aliceClient.getUserId()!].DEVICE_ID).toEqual("signed_curve25519");
|
||||
return getTestKeysClaimResponse(aliceClient.getUserId()!);
|
||||
});
|
||||
|
||||
const inboundGroupSessionPromise = expectSendRoomKey(aliceClient.getUserId()!, testOlmAccount);
|
||||
|
||||
@@ -1099,7 +1179,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
// if we're using the old crypto impl, stub out some methods in the device manager.
|
||||
// TODO: replace this with intercepts of the /keys/query endpoint to make it impl agnostic.
|
||||
if (aliceClient.crypto) {
|
||||
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
aliceClient.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
}
|
||||
|
||||
@@ -1255,7 +1335,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
// if we're using the old crypto impl, stub out some methods in the device manager.
|
||||
// TODO: replace this with intercepts of the /keys/query endpoint to make it impl agnostic.
|
||||
if (aliceClient.crypto) {
|
||||
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
aliceClient.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
}
|
||||
|
||||
@@ -1322,7 +1402,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
// if we're using the old crypto impl, stub out some methods in the device manager.
|
||||
// TODO: replace this with intercepts of the /keys/query endpoint to make it impl agnostic.
|
||||
if (aliceClient.crypto) {
|
||||
aliceClient.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
aliceClient.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
aliceClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
|
||||
aliceClient.crypto!.deviceList.getUserByIdentityKey = () => beccaTestClient.client.getUserId()!;
|
||||
}
|
||||
@@ -1849,4 +1929,248 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
expect(event.getContent().body).not.toContain("withheld");
|
||||
});
|
||||
});
|
||||
|
||||
describe("key upload request", () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
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/r0/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,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
it("should make key upload request after sync", async () => {
|
||||
let uploadPromise = awaitKeyUploadRequest();
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse([]));
|
||||
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// Verify that `/upload` is called on Alice's homesever
|
||||
const { keysCount, fallbackKeysCount } = await uploadPromise;
|
||||
expect(keysCount).toBeGreaterThan(0);
|
||||
expect(fallbackKeysCount).toBe(0);
|
||||
|
||||
uploadPromise = awaitKeyUploadRequest();
|
||||
syncResponder.sendOrQueueSyncResponse({
|
||||
next_batch: 2,
|
||||
device_one_time_keys_count: { signed_curve25519: 0 },
|
||||
device_unused_fallback_key_types: [],
|
||||
});
|
||||
|
||||
// Advance local date to 2 minutes
|
||||
// The old crypto only runs the upload every 60 seconds
|
||||
jest.setSystemTime(Date.now() + 2 * 60 * 1000);
|
||||
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// After we set device_one_time_keys_count to 0
|
||||
// a `/upload` is expected
|
||||
const res = await uploadPromise;
|
||||
expect(res.keysCount).toBeGreaterThan(0);
|
||||
expect(res.fallbackKeysCount).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUserDeviceInfo", () => {
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
// From https://spec.matrix.org/v1.6/client-server-api/#post_matrixclientv3keysquery
|
||||
// Using extracted response from matrix.org, it needs to have real keys etc to pass old crypto verification
|
||||
const queryResponseBody = {
|
||||
device_keys: {
|
||||
"@testing_florian1:matrix.org": {
|
||||
EBMMPAFOPU: {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "EBMMPAFOPU",
|
||||
keys: {
|
||||
"curve25519:EBMMPAFOPU": "HyhQD4mXwNViqns0noABW9NxHbCAOkriQ4QKGGndk3w",
|
||||
"ed25519:EBMMPAFOPU": "xSQaxrFOTXH+7Zjo+iwb445hlNPFjnx1O3KaV3Am55k",
|
||||
},
|
||||
signatures: {
|
||||
"@testing_florian1:matrix.org": {
|
||||
"ed25519:EBMMPAFOPU":
|
||||
"XFJVq9HmO5lfJN7l6muaUt887aUHg0/poR3p9XHGXBrLUqzfG7Qllq7jjtUjtcTc5CMD7/mpsXfuC2eV+X1uAw",
|
||||
},
|
||||
},
|
||||
user_id: "@testing_florian1:matrix.org",
|
||||
unsigned: {
|
||||
device_display_name: "display name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
failures: {},
|
||||
master_keys: {
|
||||
"@testing_florian1:matrix.org": {
|
||||
user_id: "@testing_florian1:matrix.org",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
"ed25519:O5s5RoLaz93Bjf/pg55oJeCVeYYoruQhqEd0Mda6lq0":
|
||||
"O5s5RoLaz93Bjf/pg55oJeCVeYYoruQhqEd0Mda6lq0",
|
||||
},
|
||||
signatures: {
|
||||
"@testing_florian1:matrix.org": {
|
||||
"ed25519:UKAQMJSJZC":
|
||||
"q4GuzzuhZfTpwrlqnJ9+AEUtEfEQ0um1PO3puwp/+vidzFicw0xEPjedpJoASYQIJ8XJAAWX8Q235EKeCzEXCA",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
self_signing_keys: {
|
||||
"@testing_florian1:matrix.org": {
|
||||
user_id: "@testing_florian1:matrix.org",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
"ed25519:YYWIHBCuKGEy9CXiVrfBVR0N1I60JtiJTNCWjiLAFzo":
|
||||
"YYWIHBCuKGEy9CXiVrfBVR0N1I60JtiJTNCWjiLAFzo",
|
||||
},
|
||||
signatures: {
|
||||
"@testing_florian1:matrix.org": {
|
||||
"ed25519:O5s5RoLaz93Bjf/pg55oJeCVeYYoruQhqEd0Mda6lq0":
|
||||
"yckmxgQ3JA5bb205/RunJipnpZ37ycGNf4OFzDwAad++chd71aGHqAMQ1f6D2GVfl8XdHmiRaohZf4mGnDL0AA",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
user_signing_keys: {
|
||||
"@testing_florian1:matrix.org": {
|
||||
user_id: "@testing_florian1:matrix.org",
|
||||
usage: ["user_signing"],
|
||||
keys: {
|
||||
"ed25519:Maa77okgZxnABGqaiChEUnV4rVsAI61WXWeL5TSEUhs":
|
||||
"Maa77okgZxnABGqaiChEUnV4rVsAI61WXWeL5TSEUhs",
|
||||
},
|
||||
signatures: {
|
||||
"@testing_florian1:matrix.org": {
|
||||
"ed25519:O5s5RoLaz93Bjf/pg55oJeCVeYYoruQhqEd0Mda6lq0":
|
||||
"WxNNXb13yCrBwXUQzdDWDvWSQ/qWCfwpvssOudlAgbtMzRESMbCTDkeA8sS1awaAtUmu7FrPtDb5LYfK/EE2CQ",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
for (const path of ["/_matrix/client/r0/keys/query", "/_matrix/client/v3/keys/query"]) {
|
||||
fetchMock.post(new URL(path, aliceClient.getHomeserverUrl()).toString(), listener);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
it("Download uncached keys for known user", async () => {
|
||||
const queryPromise = awaitKeyQueryRequest();
|
||||
|
||||
const user = "@testing_florian1:matrix.org";
|
||||
const devicesInfo = await aliceClient.getCrypto()!.getUserDeviceInfo([user], true);
|
||||
|
||||
// Wait for `/keys/query` to be called
|
||||
const deviceKeysPayload = await queryPromise;
|
||||
|
||||
expect(deviceKeysPayload).toStrictEqual({ [user]: [] });
|
||||
expect(devicesInfo.get(user)?.size).toBe(1);
|
||||
|
||||
// Convert the expected device to IDevice and check
|
||||
expect(devicesInfo.get(user)?.get("EBMMPAFOPU")).toStrictEqual(
|
||||
downloadDeviceToJsDevice(queryResponseBody.device_keys[user]?.EBMMPAFOPU),
|
||||
);
|
||||
});
|
||||
|
||||
it("Download uncached keys for unknown user", async () => {
|
||||
const queryPromise = awaitKeyQueryRequest();
|
||||
|
||||
const user = "@bob:xyz";
|
||||
const devicesInfo = await aliceClient.getCrypto()!.getUserDeviceInfo([user], true);
|
||||
|
||||
// Wait for `/keys/query` to be called
|
||||
const deviceKeysPayload = await queryPromise;
|
||||
|
||||
expect(deviceKeysPayload).toStrictEqual({ [user]: [] });
|
||||
// The old crypto has an empty map for `@bob:xyz`
|
||||
// The new crypto does not have the `@bob:xyz` entry in `devicesInfo`
|
||||
expect(devicesInfo.get(user)?.size).toBeFalsy();
|
||||
});
|
||||
|
||||
it("Get devices from tacked users", async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
const queryPromise = awaitKeyQueryRequest();
|
||||
|
||||
const user = "@testing_florian1:matrix.org";
|
||||
// `user` will be added to the room
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse([user, "@bob:xyz"]));
|
||||
|
||||
// Advance local date to 2 minutes
|
||||
// The old crypto only runs the upload every 60 seconds
|
||||
jest.setSystemTime(Date.now() + 2 * 60 * 1000);
|
||||
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// Old crypto: for alice: run over the `sleep(5)` in `doQueuedQueries` of `DeviceList`
|
||||
jest.runAllTimers();
|
||||
// Old crypto: for alice: run the `processQueryResponseForUser` in `doQueuedQueries` of `DeviceList`
|
||||
await flushPromises();
|
||||
|
||||
// Wait for alice to query `user` keys
|
||||
await queryPromise;
|
||||
|
||||
// Old crypto: for `user`: run over the `sleep(5)` in `doQueuedQueries` of `DeviceList`
|
||||
jest.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();
|
||||
|
||||
const devicesInfo = await aliceClient.getCrypto()!.getUserDeviceInfo([user]);
|
||||
|
||||
// We should only have the `user` in it
|
||||
expect(devicesInfo.size).toBe(1);
|
||||
// We are expecting only the EBMMPAFOPU device
|
||||
expect(devicesInfo.get(user)!.size).toBe(1);
|
||||
expect(devicesInfo.get(user)!.get("EBMMPAFOPU")).toEqual(
|
||||
downloadDeviceToJsDevice(queryResponseBody.device_keys[user]["EBMMPAFOPU"]),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,11 +21,13 @@ import {
|
||||
EventStatus,
|
||||
EventTimeline,
|
||||
EventTimelineSet,
|
||||
EventType,
|
||||
Filter,
|
||||
IEvent,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
PendingEventOrdering,
|
||||
RelationType,
|
||||
Room,
|
||||
} from "../../src/matrix";
|
||||
import { logger } from "../../src/logger";
|
||||
@@ -33,6 +35,7 @@ import { encodeParams, encodeUri, QueryDict, replaceParam } from "../../src/util
|
||||
import { TestClient } from "../TestClient";
|
||||
import { FeatureSupport, Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../src/models/thread";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
import { Feature, ServerSupport } from "../../src/feature";
|
||||
|
||||
const userId = "@alice:localhost";
|
||||
const userName = "Alice";
|
||||
@@ -1139,7 +1142,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
const prom = emitPromise(room, ThreadEvent.Update);
|
||||
// Assume we're seeing the reply while loading backlog
|
||||
room.addLiveEvents([THREAD_REPLY2]);
|
||||
await room.addLiveEvents([THREAD_REPLY2]);
|
||||
httpBackend
|
||||
.when(
|
||||
"GET",
|
||||
@@ -1153,7 +1156,118 @@ describe("MatrixClient event timelines", function () {
|
||||
});
|
||||
await flushHttp(prom);
|
||||
// but while loading the metadata, a new reply has arrived
|
||||
room.addLiveEvents([THREAD_REPLY3]);
|
||||
await room.addLiveEvents([THREAD_REPLY3]);
|
||||
const thread = room.getThread(THREAD_ROOT_UPDATED.event_id!)!;
|
||||
// then the events should still be all in the right order
|
||||
expect(thread.events.map((it) => it.getId())).toEqual([
|
||||
THREAD_ROOT.event_id,
|
||||
THREAD_REPLY.event_id,
|
||||
THREAD_REPLY2.getId(),
|
||||
THREAD_REPLY3.getId(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should ensure thread events don't get reordered with recursive relations", async () => {
|
||||
// Test data for a second reply to the first thread
|
||||
const THREAD_REPLY2 = utils.mkEvent({
|
||||
room: roomId,
|
||||
user: userId,
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"body": "thread reply 2",
|
||||
"msgtype": "m.text",
|
||||
"m.relates_to": {
|
||||
// We can't use the const here because we change server support mode for test
|
||||
rel_type: "io.element.thread",
|
||||
event_id: THREAD_ROOT.event_id,
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
THREAD_REPLY2.localTimestamp += 1000;
|
||||
const THREAD_ROOT_REACTION = utils.mkEvent({
|
||||
event: true,
|
||||
type: EventType.Reaction,
|
||||
user: userId,
|
||||
room: roomId,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Annotation,
|
||||
event_id: THREAD_ROOT.event_id!,
|
||||
key: Math.random().toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
THREAD_ROOT_REACTION.localTimestamp += 2000;
|
||||
|
||||
// Test data for a second reply to the first thread
|
||||
const THREAD_REPLY3 = utils.mkEvent({
|
||||
room: roomId,
|
||||
user: userId,
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"body": "thread reply 3",
|
||||
"msgtype": "m.text",
|
||||
"m.relates_to": {
|
||||
// We can't use the const here because we change server support mode for test
|
||||
rel_type: "io.element.thread",
|
||||
event_id: THREAD_ROOT.event_id,
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
THREAD_REPLY3.localTimestamp += 3000;
|
||||
|
||||
// Test data for the first thread, with the second reply
|
||||
const THREAD_ROOT_UPDATED = {
|
||||
...THREAD_ROOT,
|
||||
unsigned: {
|
||||
...THREAD_ROOT.unsigned,
|
||||
"m.relations": {
|
||||
...THREAD_ROOT.unsigned!["m.relations"],
|
||||
"io.element.thread": {
|
||||
...THREAD_ROOT.unsigned!["m.relations"]!["io.element.thread"],
|
||||
count: 3,
|
||||
latest_event: THREAD_REPLY3.event,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
client.clientOpts.threadSupport = true;
|
||||
client.canSupport.set(Feature.RelationsRecursion, ServerSupport.Stable);
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
|
||||
|
||||
client.fetchRoomEvent = () => Promise.resolve(THREAD_ROOT_UPDATED);
|
||||
|
||||
await client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId)!;
|
||||
|
||||
const prom = emitPromise(room, ThreadEvent.Update);
|
||||
// Assume we're seeing the reply while loading backlog
|
||||
await room.addLiveEvents([THREAD_REPLY2]);
|
||||
httpBackend
|
||||
.when(
|
||||
"GET",
|
||||
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
|
||||
encodeURIComponent(THREAD_ROOT_UPDATED.event_id!) +
|
||||
"/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) +
|
||||
buildRelationPaginationQuery({
|
||||
dir: Direction.Backward,
|
||||
limit: 3,
|
||||
recurse: true,
|
||||
}),
|
||||
)
|
||||
.respond(200, {
|
||||
chunk: [THREAD_REPLY3.event, THREAD_ROOT_REACTION, THREAD_REPLY2.event, THREAD_REPLY],
|
||||
});
|
||||
await flushHttp(prom);
|
||||
// but while loading the metadata, a new reply has arrived
|
||||
await room.addLiveEvents([THREAD_REPLY3]);
|
||||
const thread = room.getThread(THREAD_ROOT_UPDATED.event_id!)!;
|
||||
// then the events should still be all in the right order
|
||||
expect(thread.events.map((it) => it.getId())).toEqual([
|
||||
@@ -1458,7 +1572,7 @@ describe("MatrixClient event timelines", function () {
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD2_ROOT);
|
||||
room.addLiveEvents([THREAD_REPLY2]);
|
||||
await room.addLiveEvents([THREAD_REPLY2]);
|
||||
await httpBackend.flushAllExpected();
|
||||
await prom;
|
||||
expect(thread.length).toBe(2);
|
||||
@@ -1823,11 +1937,6 @@ describe("MatrixClient event timelines", function () {
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
@@ -1847,7 +1956,10 @@ describe("MatrixClient event timelines", function () {
|
||||
encodeURIComponent(THREAD_ROOT.event_id!) +
|
||||
"/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) +
|
||||
buildRelationPaginationQuery({ dir: Direction.Backward, from: "start_token" }),
|
||||
buildRelationPaginationQuery({
|
||||
dir: Direction.Backward,
|
||||
from: "start_token",
|
||||
}),
|
||||
)
|
||||
.respond(200, function () {
|
||||
return {
|
||||
|
||||
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
import { Mocked } from "jest-mock";
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { CRYPTO_ENABLED, IStoredClientOpts, MatrixClient } from "../../src/client";
|
||||
@@ -24,6 +25,7 @@ import { THREAD_RELATION_TYPE } from "../../src/models/thread";
|
||||
import { IFilterDefinition } from "../../src/filter";
|
||||
import { ISearchResults } from "../../src/@types/search";
|
||||
import { IStore } from "../../src/store";
|
||||
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
|
||||
|
||||
describe("MatrixClient", function () {
|
||||
const userId = "@alice:localhost";
|
||||
@@ -603,14 +605,14 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
|
||||
const prom = client!.downloadKeys(["boris", "chaz"]).then(function (res) {
|
||||
assertObjectContains(res.boris.dev1, {
|
||||
assertObjectContains(res.get("boris")!.get("dev1")!, {
|
||||
verified: 0, // DeviceVerification.UNVERIFIED
|
||||
keys: { "ed25519:dev1": ed25519key },
|
||||
algorithms: ["1"],
|
||||
unsigned: { abc: "def" },
|
||||
});
|
||||
|
||||
assertObjectContains(res.chaz.dev2, {
|
||||
assertObjectContains(res.get("chaz")!.get("dev2")!, {
|
||||
verified: 0, // DeviceVerification.UNVERIFIED
|
||||
keys: { "ed25519:dev2": ed25519key },
|
||||
algorithms: ["2"],
|
||||
@@ -1127,22 +1129,51 @@ describe("MatrixClient", function () {
|
||||
|
||||
describe("requestLoginToken", () => {
|
||||
it("should hit the expected API endpoint with UIA", async () => {
|
||||
httpBackend!
|
||||
.when("GET", "/capabilities")
|
||||
.respond(200, { capabilities: { "org.matrix.msc3882.get_login_token": { enabled: true } } });
|
||||
const response = {};
|
||||
const uiaData = {};
|
||||
const prom = client!.requestLoginToken(uiaData);
|
||||
httpBackend!
|
||||
.when("POST", "/unstable/org.matrix.msc3882/login/token", { auth: uiaData })
|
||||
.when("POST", "/unstable/org.matrix.msc3882/login/get_token", { auth: uiaData })
|
||||
.respond(200, response);
|
||||
await httpBackend!.flush("");
|
||||
expect(await prom).toStrictEqual(response);
|
||||
});
|
||||
|
||||
it("should hit the expected API endpoint without UIA", async () => {
|
||||
const response = {};
|
||||
httpBackend!
|
||||
.when("GET", "/capabilities")
|
||||
.respond(200, { capabilities: { "org.matrix.msc3882.get_login_token": { enabled: true } } });
|
||||
const response = { login_token: "xyz", expires_in_ms: 5000 };
|
||||
const prom = client!.requestLoginToken();
|
||||
httpBackend!.when("POST", "/unstable/org.matrix.msc3882/login/get_token", {}).respond(200, response);
|
||||
await httpBackend!.flush("");
|
||||
// check that expires_in has been populated for compatibility with r0
|
||||
expect(await prom).toStrictEqual({ ...response, expires_in: 5 });
|
||||
});
|
||||
|
||||
it("should hit the r1 endpoint when capability is disabled", async () => {
|
||||
httpBackend!
|
||||
.when("GET", "/capabilities")
|
||||
.respond(200, { capabilities: { "org.matrix.msc3882.get_login_token": { enabled: false } } });
|
||||
const response = { login_token: "xyz", expires_in_ms: 5000 };
|
||||
const prom = client!.requestLoginToken();
|
||||
httpBackend!.when("POST", "/unstable/org.matrix.msc3882/login/get_token", {}).respond(200, response);
|
||||
await httpBackend!.flush("");
|
||||
// check that expires_in has been populated for compatibility with r0
|
||||
expect(await prom).toStrictEqual({ ...response, expires_in: 5 });
|
||||
});
|
||||
|
||||
it("should hit the r0 endpoint for fallback", async () => {
|
||||
httpBackend!.when("GET", "/capabilities").respond(200, {});
|
||||
const response = { login_token: "xyz", expires_in: 5 };
|
||||
const prom = client!.requestLoginToken();
|
||||
httpBackend!.when("POST", "/unstable/org.matrix.msc3882/login/token", {}).respond(200, response);
|
||||
await httpBackend!.flush("");
|
||||
expect(await prom).toStrictEqual(response);
|
||||
// check that expires_in has been populated for compatibility with r1
|
||||
expect(await prom).toStrictEqual({ ...response, expires_in_ms: 5000 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1383,6 +1414,42 @@ describe("MatrixClient", function () {
|
||||
await client!.uploadKeys();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCryptoTrustCrossSignedDevices", () => {
|
||||
it("should throw if e2e is disabled", () => {
|
||||
expect(() => client!.getCryptoTrustCrossSignedDevices()).toThrow("End-to-end encryption disabled");
|
||||
});
|
||||
|
||||
it("should proxy to the crypto backend", async () => {
|
||||
const mockBackend = {
|
||||
getTrustCrossSignedDevices: jest.fn().mockReturnValue(true),
|
||||
} as unknown as Mocked<CryptoBackend>;
|
||||
client!["cryptoBackend"] = mockBackend;
|
||||
|
||||
expect(client!.getCryptoTrustCrossSignedDevices()).toBe(true);
|
||||
mockBackend.getTrustCrossSignedDevices.mockReturnValue(false);
|
||||
expect(client!.getCryptoTrustCrossSignedDevices()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setCryptoTrustCrossSignedDevices", () => {
|
||||
it("should throw if e2e is disabled", () => {
|
||||
expect(() => client!.setCryptoTrustCrossSignedDevices(false)).toThrow("End-to-end encryption disabled");
|
||||
});
|
||||
|
||||
it("should proxy to the crypto backend", async () => {
|
||||
const mockBackend = {
|
||||
setTrustCrossSignedDevices: jest.fn(),
|
||||
} as unknown as Mocked<CryptoBackend>;
|
||||
client!["cryptoBackend"] = mockBackend;
|
||||
|
||||
client!.setCryptoTrustCrossSignedDevices(true);
|
||||
expect(mockBackend.setTrustCrossSignedDevices).toHaveBeenLastCalledWith(true);
|
||||
|
||||
client!.setCryptoTrustCrossSignedDevices(false);
|
||||
expect(mockBackend.setTrustCrossSignedDevices).toHaveBeenLastCalledWith(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function withThreadId(event: MatrixEvent, newThreadId: string): MatrixEvent {
|
||||
|
||||
@@ -157,14 +157,10 @@ describe("MatrixClient opts", function () {
|
||||
error: "Ruh roh",
|
||||
}),
|
||||
);
|
||||
try {
|
||||
await Promise.all([
|
||||
expect(client.sendTextMessage("!foo:bar", "a body", "txn1")).rejects.toThrow(),
|
||||
httpBackend.flush("/txn1", 1),
|
||||
]);
|
||||
} catch (err) {
|
||||
expect((<MatrixError>err).errcode).toEqual("M_SOMETHING");
|
||||
}
|
||||
|
||||
await expect(
|
||||
Promise.all([client.sendTextMessage("!foo:bar", "a body", "txn1"), httpBackend.flush("/txn1", 1)]),
|
||||
).rejects.toThrow("MatrixError: [500] Unknown message");
|
||||
});
|
||||
|
||||
it("shouldn't queue events", async () => {
|
||||
|
||||
@@ -36,11 +36,15 @@ import {
|
||||
NotificationCountType,
|
||||
IEphemeral,
|
||||
Room,
|
||||
IndexedDBStore,
|
||||
RelationType,
|
||||
} from "../../src";
|
||||
import { ReceiptType } from "../../src/@types/read_receipts";
|
||||
import { UNREAD_THREAD_NOTIFICATIONS } from "../../src/@types/sync";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { emitPromise, mkEvent, mkMessage } from "../test-utils/test-utils";
|
||||
import { THREAD_RELATION_TYPE } from "../../src/models/thread";
|
||||
|
||||
describe("MatrixClient syncing", () => {
|
||||
const selfUserId = "@alice:localhost";
|
||||
@@ -1867,4 +1871,124 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
|
||||
idbClient.stopClient();
|
||||
idbHttpBackend.stop();
|
||||
});
|
||||
|
||||
it("should query server for which thread a 2nd order relation belongs to and stash in sync accumulator", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
|
||||
async function startClient(client: MatrixClient): Promise<void> {
|
||||
await Promise.all([
|
||||
idbClient.startClient({
|
||||
// Without this all events just go into the main timeline
|
||||
threadSupport: true,
|
||||
}),
|
||||
idbHttpBackend.flushAllExpected(),
|
||||
emitPromise(idbClient, ClientEvent.Room),
|
||||
]);
|
||||
}
|
||||
|
||||
function assertEventsExpected(client: MatrixClient): void {
|
||||
const room = client.getRoom(roomId);
|
||||
const mainTimelineEvents = room!.getLiveTimeline().getEvents();
|
||||
expect(mainTimelineEvents).toHaveLength(1);
|
||||
expect(mainTimelineEvents[0].getContent().body).toEqual("Test");
|
||||
|
||||
const thread = room!.getThread("$someThreadId")!;
|
||||
expect(thread.replayEvents).toHaveLength(1);
|
||||
expect(thread.replayEvents![0].getRelation()!.key).toEqual("🪿");
|
||||
}
|
||||
|
||||
let idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken, undefined, {
|
||||
store: new IndexedDBStore({
|
||||
indexedDB: global.indexedDB,
|
||||
dbName: "test",
|
||||
}),
|
||||
});
|
||||
let idbHttpBackend = idbTestClient.httpBackend;
|
||||
let idbClient = idbTestClient.client;
|
||||
await idbClient.store.startup();
|
||||
|
||||
idbHttpBackend.when("GET", "/versions").respond(200, { versions: ["v1.4"] });
|
||||
idbHttpBackend.when("GET", "/pushrules/").respond(200, {});
|
||||
idbHttpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
|
||||
const syncRoomSection = {
|
||||
join: {
|
||||
[roomId]: {
|
||||
timeline: {
|
||||
prev_batch: "foo",
|
||||
events: [
|
||||
mkMessage({
|
||||
room: roomId,
|
||||
user: selfUserId,
|
||||
msg: "Test",
|
||||
}),
|
||||
mkEvent({
|
||||
room: roomId,
|
||||
user: selfUserId,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Annotation,
|
||||
event_id: "$someUnknownEvent",
|
||||
key: "🪿",
|
||||
},
|
||||
},
|
||||
type: "m.reaction",
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
idbHttpBackend.when("GET", "/sync").respond(200, {
|
||||
...syncData,
|
||||
rooms: syncRoomSection,
|
||||
});
|
||||
idbHttpBackend.when("GET", `/rooms/${encodeURIComponent(roomId)}/event/%24someUnknownEvent`).respond(
|
||||
200,
|
||||
mkEvent({
|
||||
room: roomId,
|
||||
user: selfUserId,
|
||||
content: {
|
||||
"body": "Thread response",
|
||||
"m.relates_to": {
|
||||
rel_type: THREAD_RELATION_TYPE.name,
|
||||
event_id: "$someThreadId",
|
||||
},
|
||||
},
|
||||
type: "m.room.message",
|
||||
}),
|
||||
);
|
||||
|
||||
await startClient(idbClient);
|
||||
assertEventsExpected(idbClient);
|
||||
|
||||
idbHttpBackend.verifyNoOutstandingExpectation();
|
||||
// Force sync accumulator to persist, reset client, assert it doesn't re-fetch event on next start-up
|
||||
await idbClient.store.save(true);
|
||||
await idbClient.stopClient();
|
||||
await idbClient.store.destroy();
|
||||
await idbHttpBackend.stop();
|
||||
|
||||
idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken, undefined, {
|
||||
store: new IndexedDBStore({
|
||||
indexedDB: global.indexedDB,
|
||||
dbName: "test",
|
||||
}),
|
||||
});
|
||||
idbHttpBackend = idbTestClient.httpBackend;
|
||||
idbClient = idbTestClient.client;
|
||||
await idbClient.store.startup();
|
||||
|
||||
idbHttpBackend.when("GET", "/versions").respond(200, { versions: ["v1.4"] });
|
||||
idbHttpBackend.when("GET", "/pushrules/").respond(200, {});
|
||||
idbHttpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
idbHttpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
await startClient(idbClient);
|
||||
assertEventsExpected(idbClient);
|
||||
|
||||
idbHttpBackend.verifyNoOutstandingExpectation();
|
||||
await idbClient.stopClient();
|
||||
await idbHttpBackend.stop();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,7 +89,7 @@ describe("MatrixClient syncing", () => {
|
||||
|
||||
const thread = mkThread({ room, client: client!, authorId: selfUserId, participantUserIds: [selfUserId] });
|
||||
const threadReply = thread.events.at(-1)!;
|
||||
room.addLiveEvents([thread.rootEvent]);
|
||||
await room.addLiveEvents([thread.rootEvent]);
|
||||
|
||||
// Initialize read receipt datastructure before testing the reaction
|
||||
room.addReceiptToStructure(thread.rootEvent.getId()!, ReceiptType.Read, selfUserId, { ts: 1 }, false);
|
||||
|
||||
@@ -472,7 +472,7 @@ describe("MatrixClient crypto", () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
@@ -483,7 +483,7 @@ describe("MatrixClient crypto", () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
@@ -545,7 +545,7 @@ describe("MatrixClient crypto", () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
|
||||
@@ -42,6 +42,7 @@ import { SyncApiOptions, SyncState } from "../../src/sync";
|
||||
import { IStoredClientOpts } from "../../src/client";
|
||||
import { logger } from "../../src/logger";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
import { defer } from "../../src/utils";
|
||||
|
||||
describe("SlidingSyncSdk", () => {
|
||||
let client: MatrixClient | undefined;
|
||||
@@ -301,67 +302,57 @@ describe("SlidingSyncSdk", () => {
|
||||
},
|
||||
};
|
||||
|
||||
it("can be created with required_state and timeline", () => {
|
||||
it("can be created with required_state and timeline", async () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]);
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const gotRoom = client!.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.name).toEqual(data[roomA].name);
|
||||
expect(gotRoom.getMyMembership()).toEqual("join");
|
||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline);
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.name).toEqual(data[roomA].name);
|
||||
expect(gotRoom!.getMyMembership()).toEqual("join");
|
||||
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline);
|
||||
});
|
||||
|
||||
it("can be created with timeline only", () => {
|
||||
it("can be created with timeline only", async () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]);
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const gotRoom = client!.getRoom(roomB);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.name).toEqual(data[roomB].name);
|
||||
expect(gotRoom.getMyMembership()).toEqual("join");
|
||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline);
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.name).toEqual(data[roomB].name);
|
||||
expect(gotRoom!.getMyMembership()).toEqual("join");
|
||||
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline);
|
||||
});
|
||||
|
||||
it("can be created with a highlight_count", () => {
|
||||
it("can be created with a highlight_count", async () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]);
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const gotRoom = client!.getRoom(roomC);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(
|
||||
data[roomC].highlight_count,
|
||||
);
|
||||
});
|
||||
|
||||
it("can be created with a notification_count", () => {
|
||||
it("can be created with a notification_count", async () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]);
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const gotRoom = client!.getRoom(roomD);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(
|
||||
data[roomD].notification_count,
|
||||
);
|
||||
});
|
||||
|
||||
it("can be created with an invited/joined_count", () => {
|
||||
it("can be created with an invited/joined_count", async () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomG, data[roomG]);
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const gotRoom = client!.getRoom(roomG);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getInvitedMemberCount()).toEqual(data[roomG].invited_count);
|
||||
expect(gotRoom.getJoinedMemberCount()).toEqual(data[roomG].joined_count);
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.getInvitedMemberCount()).toEqual(data[roomG].invited_count);
|
||||
expect(gotRoom!.getJoinedMemberCount()).toEqual(data[roomG].joined_count);
|
||||
});
|
||||
|
||||
it("can be created with live events", () => {
|
||||
let seenLiveEvent = false;
|
||||
it("can be created with live events", async () => {
|
||||
const seenLiveEventDeferred = defer<boolean>();
|
||||
const listener = (
|
||||
ev: MatrixEvent,
|
||||
room?: Room,
|
||||
@@ -371,43 +362,37 @@ describe("SlidingSyncSdk", () => {
|
||||
) => {
|
||||
if (timelineData?.liveEvent) {
|
||||
assertTimelineEvents([ev], data[roomH].timeline.slice(-1));
|
||||
seenLiveEvent = true;
|
||||
seenLiveEventDeferred.resolve(true);
|
||||
}
|
||||
};
|
||||
client!.on(RoomEvent.Timeline, listener);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomH, data[roomH]);
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
client!.off(RoomEvent.Timeline, listener);
|
||||
const gotRoom = client!.getRoom(roomH);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.name).toEqual(data[roomH].name);
|
||||
expect(gotRoom.getMyMembership()).toEqual("join");
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.name).toEqual(data[roomH].name);
|
||||
expect(gotRoom!.getMyMembership()).toEqual("join");
|
||||
// check the entire timeline is correct
|
||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents(), data[roomH].timeline);
|
||||
expect(seenLiveEvent).toBe(true);
|
||||
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents(), data[roomH].timeline);
|
||||
await expect(seenLiveEventDeferred.promise).resolves.toBeTruthy();
|
||||
});
|
||||
|
||||
it("can be created with invite_state", () => {
|
||||
it("can be created with invite_state", async () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]);
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const gotRoom = client!.getRoom(roomE);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getMyMembership()).toEqual("invite");
|
||||
expect(gotRoom.currentState.getJoinRule()).toEqual(JoinRule.Invite);
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.getMyMembership()).toEqual("invite");
|
||||
expect(gotRoom!.currentState.getJoinRule()).toEqual(JoinRule.Invite);
|
||||
});
|
||||
|
||||
it("uses the 'name' field to caluclate the room name", () => {
|
||||
it("uses the 'name' field to caluclate the room name", async () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]);
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const gotRoom = client!.getRoom(roomF);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.name).toEqual(data[roomF].name);
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.name).toEqual(data[roomF].name);
|
||||
});
|
||||
|
||||
describe("updating", () => {
|
||||
@@ -419,33 +404,33 @@ describe("SlidingSyncSdk", () => {
|
||||
name: data[roomA].name,
|
||||
});
|
||||
const gotRoom = client!.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
expect(gotRoom).toBeTruthy();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
const newTimeline = data[roomA].timeline;
|
||||
newTimeline.push(newEvent);
|
||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-3), newTimeline);
|
||||
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents().slice(-3), newTimeline);
|
||||
});
|
||||
|
||||
it("can update with a new required_state event", async () => {
|
||||
let gotRoom = client!.getRoom(roomB);
|
||||
expect(gotRoom).toBeDefined();
|
||||
expect(gotRoom).toBeTruthy();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Invite); // default
|
||||
expect(gotRoom!.getJoinRule()).toEqual(JoinRule.Invite); // default
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, {
|
||||
required_state: [mkOwnStateEvent("m.room.join_rules", { join_rule: "restricted" }, "")],
|
||||
timeline: [],
|
||||
name: data[roomB].name,
|
||||
});
|
||||
gotRoom = client!.getRoom(roomB);
|
||||
expect(gotRoom).toBeDefined();
|
||||
expect(gotRoom).toBeTruthy();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Restricted);
|
||||
expect(gotRoom!.getJoinRule()).toEqual(JoinRule.Restricted);
|
||||
});
|
||||
|
||||
it("can update with a new highlight_count", async () => {
|
||||
@@ -456,11 +441,11 @@ describe("SlidingSyncSdk", () => {
|
||||
highlight_count: 1,
|
||||
});
|
||||
const gotRoom = client!.getRoom(roomC);
|
||||
expect(gotRoom).toBeDefined();
|
||||
expect(gotRoom).toBeTruthy();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(1);
|
||||
expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(1);
|
||||
});
|
||||
|
||||
it("can update with a new notification_count", async () => {
|
||||
@@ -471,11 +456,11 @@ describe("SlidingSyncSdk", () => {
|
||||
notification_count: 1,
|
||||
});
|
||||
const gotRoom = client!.getRoom(roomD);
|
||||
expect(gotRoom).toBeDefined();
|
||||
expect(gotRoom).toBeTruthy();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(1);
|
||||
expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(1);
|
||||
});
|
||||
|
||||
it("can update with a new joined_count", () => {
|
||||
@@ -486,11 +471,11 @@ describe("SlidingSyncSdk", () => {
|
||||
joined_count: 1,
|
||||
});
|
||||
const gotRoom = client!.getRoom(roomG);
|
||||
expect(gotRoom).toBeDefined();
|
||||
expect(gotRoom).toBeTruthy();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getJoinedMemberCount()).toEqual(1);
|
||||
expect(gotRoom!.getJoinedMemberCount()).toEqual(1);
|
||||
});
|
||||
|
||||
// Regression test for a bug which caused the timeline entries to be out-of-order
|
||||
@@ -512,7 +497,7 @@ describe("SlidingSyncSdk", () => {
|
||||
initial: true, // e.g requested via room subscription
|
||||
});
|
||||
const gotRoom = client!.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
expect(gotRoom).toBeTruthy();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
@@ -530,7 +515,7 @@ describe("SlidingSyncSdk", () => {
|
||||
);
|
||||
|
||||
// we expect the timeline now to be oldTimeline (so the old events are in fact old)
|
||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents(), oldTimeline);
|
||||
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents(), oldTimeline);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -626,9 +611,9 @@ describe("SlidingSyncSdk", () => {
|
||||
await httpBackend!.flush("/profile", 1, 1000);
|
||||
await emitPromise(client!, RoomMemberEvent.Name);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeDefined();
|
||||
expect(room).toBeTruthy();
|
||||
const inviteeMember = room.getMember(invitee)!;
|
||||
expect(inviteeMember).toBeDefined();
|
||||
expect(inviteeMember).toBeTruthy();
|
||||
expect(inviteeMember.getMxcAvatarUrl()).toEqual(inviteeProfile.avatar_url);
|
||||
expect(inviteeMember.name).toEqual(inviteeProfile.displayname);
|
||||
});
|
||||
@@ -662,41 +647,30 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("can update device lists", () => {
|
||||
client!.crypto!.processDeviceLists = jest.fn();
|
||||
ext.onResponse({
|
||||
device_lists: {
|
||||
changed: ["@alice:localhost"],
|
||||
left: ["@bob:localhost"],
|
||||
},
|
||||
});
|
||||
// TODO: more assertions?
|
||||
expect(client!.crypto!.processDeviceLists).toHaveBeenCalledWith({
|
||||
changed: ["@alice:localhost"],
|
||||
left: ["@bob:localhost"],
|
||||
});
|
||||
});
|
||||
|
||||
it("can update OTK counts", () => {
|
||||
client!.crypto!.updateOneTimeKeyCount = jest.fn();
|
||||
it("can update OTK counts and unused fallback keys", () => {
|
||||
client!.crypto!.processKeyCounts = jest.fn();
|
||||
ext.onResponse({
|
||||
device_one_time_keys_count: {
|
||||
signed_curve25519: 42,
|
||||
},
|
||||
});
|
||||
expect(client!.crypto!.updateOneTimeKeyCount).toHaveBeenCalledWith(42);
|
||||
ext.onResponse({
|
||||
device_one_time_keys_count: {
|
||||
not_signed_curve25519: 42,
|
||||
// missing field -> default to 0
|
||||
},
|
||||
});
|
||||
expect(client!.crypto!.updateOneTimeKeyCount).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it("can update fallback keys", () => {
|
||||
ext.onResponse({
|
||||
device_unused_fallback_key_types: ["signed_curve25519"],
|
||||
});
|
||||
expect(client!.crypto!.getNeedsNewFallback()).toEqual(false);
|
||||
ext.onResponse({
|
||||
device_unused_fallback_key_types: ["not_signed_curve25519"],
|
||||
});
|
||||
expect(client!.crypto!.getNeedsNewFallback()).toEqual(true);
|
||||
expect(client!.crypto!.processKeyCounts).toHaveBeenCalledWith({ signed_curve25519: 42 }, [
|
||||
"signed_curve25519",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -734,7 +708,7 @@ describe("SlidingSyncSdk", () => {
|
||||
],
|
||||
});
|
||||
globalData = client!.getAccountData(globalType)!;
|
||||
expect(globalData).toBeDefined();
|
||||
expect(globalData).toBeTruthy();
|
||||
expect(globalData.getContent()).toEqual(globalContent);
|
||||
});
|
||||
|
||||
@@ -755,6 +729,7 @@ describe("SlidingSyncSdk", () => {
|
||||
foo: "bar",
|
||||
};
|
||||
const roomType = "test";
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
ext.onResponse({
|
||||
rooms: {
|
||||
[roomId]: [
|
||||
@@ -766,9 +741,9 @@ describe("SlidingSyncSdk", () => {
|
||||
},
|
||||
});
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeDefined();
|
||||
expect(room).toBeTruthy();
|
||||
const event = room.getAccountData(roomType)!;
|
||||
expect(event).toBeDefined();
|
||||
expect(event).toBeTruthy();
|
||||
expect(event.getContent()).toEqual(roomContent);
|
||||
});
|
||||
|
||||
@@ -891,11 +866,9 @@ describe("SlidingSyncSdk", () => {
|
||||
const evType = ev.getType();
|
||||
expect(seen[evType]).toBeFalsy();
|
||||
seen[evType] = true;
|
||||
if (evType === "m.key.verification.start" || evType === "m.key.verification.request") {
|
||||
expect(ev.isCancelled()).toEqual(true);
|
||||
} else {
|
||||
expect(ev.isCancelled()).toEqual(false);
|
||||
}
|
||||
expect(ev.isCancelled()).toEqual(
|
||||
evType === "m.key.verification.start" || evType === "m.key.verification.request",
|
||||
);
|
||||
});
|
||||
ext.onResponse({
|
||||
next_batch: "45678",
|
||||
@@ -956,8 +929,9 @@ describe("SlidingSyncSdk", () => {
|
||||
],
|
||||
initial: true,
|
||||
});
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeDefined();
|
||||
expect(room).toBeTruthy();
|
||||
expect(room.getMember(selfUserId)?.typing).toEqual(false);
|
||||
ext.onResponse({
|
||||
rooms: {
|
||||
@@ -997,7 +971,7 @@ describe("SlidingSyncSdk", () => {
|
||||
initial: true,
|
||||
});
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeDefined();
|
||||
expect(room).toBeTruthy();
|
||||
expect(room.getMember(selfUserId)?.typing).toEqual(false);
|
||||
ext.onResponse({
|
||||
rooms: {
|
||||
@@ -1090,12 +1064,13 @@ describe("SlidingSyncSdk", () => {
|
||||
],
|
||||
initial: true,
|
||||
});
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeDefined();
|
||||
expect(room).toBeTruthy();
|
||||
expect(room.getReadReceiptForUserId(alice, true)).toBeNull();
|
||||
ext.onResponse(generateReceiptResponse(alice, roomId, lastEvent.event_id, "m.read", 1234567));
|
||||
const receipt = room.getReadReceiptForUserId(alice);
|
||||
expect(receipt).toBeDefined();
|
||||
expect(receipt).toBeTruthy();
|
||||
expect(receipt?.eventId).toEqual(lastEvent.event_id);
|
||||
expect(receipt?.data.ts).toEqual(1234567);
|
||||
expect(receipt?.data.thread_id).toBeFalsy();
|
||||
|
||||
@@ -1698,7 +1698,7 @@ describe("SlidingSync", () => {
|
||||
});
|
||||
|
||||
function timeout(delayMs: number, reason: string): { promise: Promise<never>; cancel: () => void } {
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
return {
|
||||
promise: new Promise((resolve, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
|
||||
+1
-1
@@ -23,5 +23,5 @@ try {
|
||||
global.Olm = require("@matrix-org/olm");
|
||||
logger.log("loaded libolm");
|
||||
} catch (e) {
|
||||
logger.warn("unable to run crypto tests: libolm not available");
|
||||
logger.warn("unable to run crypto tests: libolm not available", e);
|
||||
}
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import DOMException from "domexception";
|
||||
|
||||
global.DOMException = DOMException;
|
||||
global.DOMException = DOMException as typeof global.DOMException;
|
||||
|
||||
jest.mock("../src/http-api/utils", () => ({
|
||||
...jest.requireActual("../src/http-api/utils"),
|
||||
|
||||
@@ -6,7 +6,7 @@ import "../olm-loader";
|
||||
|
||||
import { logger } from "../../src/logger";
|
||||
import { IContent, IEvent, IEventRelation, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { ClientEvent, EventType, IPusher, MatrixClient, MsgType } from "../../src";
|
||||
import { ClientEvent, EventType, IPusher, MatrixClient, MsgType, RelationType } from "../../src";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { eventMapperFor } from "../../src/event-mapper";
|
||||
|
||||
@@ -258,6 +258,9 @@ export interface IMessageOpts {
|
||||
* @param opts.user - The user ID for the event.
|
||||
* @param opts.msg - Optional. The content.body for the event.
|
||||
* @param opts.event - True to make a MatrixEvent.
|
||||
* @param opts.relatesTo - An IEventRelation relating this to another event.
|
||||
* @param opts.ts - The timestamp of the event.
|
||||
* @param opts.event - True to make a MatrixEvent.
|
||||
* @param client - If passed along with opts.event=true will be used to set up re-emitters.
|
||||
* @returns The event
|
||||
*/
|
||||
@@ -297,6 +300,7 @@ interface IReplyMessageOpts extends IMessageOpts {
|
||||
* @param opts.room - The room ID for the event.
|
||||
* @param opts.user - The user ID for the event.
|
||||
* @param opts.msg - Optional. The content.body for the event.
|
||||
* @param opts.ts - The timestamp of the event.
|
||||
* @param opts.replyToMessage - The replied message
|
||||
* @param opts.event - True to make a MatrixEvent.
|
||||
* @param client - If passed along with opts.event=true will be used to set up re-emitters.
|
||||
@@ -330,6 +334,73 @@ export function mkReplyMessage(
|
||||
return mkEvent(eventOpts, client);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a reaction event.
|
||||
*
|
||||
* @param target - the event we are reacting to.
|
||||
* @param client - the MatrixClient
|
||||
* @param userId - the userId of the sender
|
||||
* @param roomId - the id of the room we are in
|
||||
* @param ts - The timestamp of the event.
|
||||
* @returns The event
|
||||
*/
|
||||
export function mkReaction(
|
||||
target: MatrixEvent,
|
||||
client: MatrixClient,
|
||||
userId: string,
|
||||
roomId: string,
|
||||
ts?: number,
|
||||
): MatrixEvent {
|
||||
return mkEvent(
|
||||
{
|
||||
event: true,
|
||||
type: EventType.Reaction,
|
||||
user: userId,
|
||||
room: roomId,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Annotation,
|
||||
event_id: target.getId()!,
|
||||
key: Math.random().toString(),
|
||||
},
|
||||
},
|
||||
ts,
|
||||
},
|
||||
client,
|
||||
);
|
||||
}
|
||||
|
||||
export function mkEdit(
|
||||
target: MatrixEvent,
|
||||
client: MatrixClient,
|
||||
userId: string,
|
||||
roomId: string,
|
||||
msg?: string,
|
||||
ts?: number,
|
||||
) {
|
||||
msg = msg ?? `Edit of ${target.getId()}`;
|
||||
return mkEvent(
|
||||
{
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
user: userId,
|
||||
room: roomId,
|
||||
content: {
|
||||
"body": `* ${msg}`,
|
||||
"m.new_content": {
|
||||
body: msg,
|
||||
},
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Replace,
|
||||
event_id: target.getId()!,
|
||||
},
|
||||
},
|
||||
ts,
|
||||
},
|
||||
client,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A mock implementation of webstorage
|
||||
*/
|
||||
@@ -375,17 +446,17 @@ export async function awaitDecryption(
|
||||
// already
|
||||
if (event.getClearContent() !== null) {
|
||||
if (waitOnDecryptionFailure && event.isDecryptionFailure()) {
|
||||
logger.log(`${Date.now()} event ${event.getId()} got decryption error; waiting`);
|
||||
logger.log(`${Date.now()}: event ${event.getId()} got decryption error; waiting`);
|
||||
} else {
|
||||
return event;
|
||||
}
|
||||
} else {
|
||||
logger.log(`${Date.now()} event ${event.getId()} is not yet decrypted; waiting`);
|
||||
logger.log(`${Date.now()}: event ${event.getId()} is not yet decrypted; waiting`);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
event.once(MatrixEventEvent.Decrypted, (ev) => {
|
||||
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
||||
event.once(MatrixEventEvent.Decrypted, (ev, err) => {
|
||||
logger.log(`${Date.now()}: MatrixEventEvent.Decrypted for event ${event.getId()}: ${err ?? "success"}`);
|
||||
resolve(ev);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -115,6 +115,26 @@ type MakeThreadProps = {
|
||||
ts?: number;
|
||||
};
|
||||
|
||||
type MakeThreadResult = {
|
||||
/**
|
||||
* Thread model
|
||||
*/
|
||||
thread: Thread;
|
||||
/**
|
||||
* Thread root event
|
||||
*/
|
||||
rootEvent: MatrixEvent;
|
||||
/**
|
||||
* Events added to the thread
|
||||
*/
|
||||
events: MatrixEvent[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Starts a new thread in a room by creating a message as thread root.
|
||||
* Also creates a Thread model and adds it to the room.
|
||||
* Does not insert the messages into a timeline.
|
||||
*/
|
||||
export const mkThread = ({
|
||||
room,
|
||||
client,
|
||||
@@ -122,7 +142,7 @@ export const mkThread = ({
|
||||
participantUserIds,
|
||||
length = 2,
|
||||
ts = 1,
|
||||
}: MakeThreadProps): { thread: Thread; rootEvent: MatrixEvent; events: MatrixEvent[] } => {
|
||||
}: MakeThreadProps): MakeThreadResult => {
|
||||
const { rootEvent, events } = makeThreadEvents({
|
||||
roomId: room.roomId,
|
||||
authorId,
|
||||
|
||||
+147
-6
@@ -30,6 +30,7 @@ import {
|
||||
RoomState,
|
||||
RoomStateEvent,
|
||||
RoomStateEventHandlerMap,
|
||||
SendToDeviceContentMap,
|
||||
} from "../../src";
|
||||
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
|
||||
import { ReEmitter } from "../../src/ReEmitter";
|
||||
@@ -122,6 +123,7 @@ export class MockRTCPeerConnection {
|
||||
public iceCandidateListener?: (e: RTCPeerConnectionIceEvent) => void;
|
||||
public iceConnectionStateChangeListener?: () => void;
|
||||
public onTrackListener?: (e: RTCTrackEvent) => void;
|
||||
public onDataChannelListener?: (ev: RTCDataChannelEvent) => void;
|
||||
public needsNegotiation = false;
|
||||
public readyToNegotiate: Promise<void>;
|
||||
private onReadyToNegotiate?: () => void;
|
||||
@@ -167,6 +169,8 @@ export class MockRTCPeerConnection {
|
||||
this.iceConnectionStateChangeListener = listener;
|
||||
} else if (type == "track") {
|
||||
this.onTrackListener = listener;
|
||||
} else if (type == "datachannel") {
|
||||
this.onDataChannelListener = listener;
|
||||
}
|
||||
}
|
||||
public createDataChannel(label: string, opts: RTCDataChannelInit) {
|
||||
@@ -231,6 +235,12 @@ export class MockRTCPeerConnection {
|
||||
this.negotiationNeededListener();
|
||||
}
|
||||
}
|
||||
|
||||
public triggerIncomingDataChannel(): void {
|
||||
this.onDataChannelListener?.({ channel: {} } as RTCDataChannelEvent);
|
||||
}
|
||||
|
||||
public restartIce(): void {}
|
||||
}
|
||||
|
||||
export class MockRTCRtpSender {
|
||||
@@ -443,11 +453,7 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
|
||||
>();
|
||||
public sendToDevice = jest.fn<
|
||||
Promise<{}>,
|
||||
[
|
||||
eventType: string,
|
||||
contentMap: { [userId: string]: { [deviceId: string]: Record<string, any> } },
|
||||
txnId?: string,
|
||||
]
|
||||
[eventType: string, contentMap: SendToDeviceContentMap, txnId?: string]
|
||||
>();
|
||||
|
||||
public isInitialSyncComplete(): boolean {
|
||||
@@ -502,18 +508,22 @@ export class MockMatrixCall extends TypedEventEmitter<CallEvent, CallEventHandle
|
||||
public state = CallState.Ringing;
|
||||
public opponentUserId = FAKE_USER_ID_1;
|
||||
public opponentDeviceId = FAKE_DEVICE_ID_1;
|
||||
public opponentSessionId = FAKE_SESSION_ID_1;
|
||||
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),
|
||||
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 sendMetadataUpdate = jest.fn<void, []>();
|
||||
|
||||
@@ -525,6 +535,14 @@ export class MockMatrixCall extends TypedEventEmitter<CallEvent, CallEventHandle
|
||||
return this.opponentDeviceId;
|
||||
}
|
||||
|
||||
public getOpponentSessionId(): string | undefined {
|
||||
return this.opponentSessionId;
|
||||
}
|
||||
|
||||
public getLocalFeeds(): CallFeed[] {
|
||||
return [this.localUsermediaFeed];
|
||||
}
|
||||
|
||||
public typed(): MatrixCall {
|
||||
return this as unknown as MatrixCall;
|
||||
}
|
||||
@@ -585,6 +603,7 @@ export function makeMockGroupCallStateEvent(
|
||||
"m.type": GroupCallType.Video,
|
||||
"m.intent": GroupCallIntent.Prompt,
|
||||
},
|
||||
redacted?: boolean,
|
||||
): MatrixEvent {
|
||||
return {
|
||||
getType: jest.fn().mockReturnValue(EventType.GroupCallPrefix),
|
||||
@@ -592,6 +611,7 @@ export function makeMockGroupCallStateEvent(
|
||||
getTs: jest.fn().mockReturnValue(0),
|
||||
getContent: jest.fn().mockReturnValue(content),
|
||||
getStateKey: jest.fn().mockReturnValue(groupCallId),
|
||||
isRedacted: jest.fn().mockReturnValue(redacted ?? false),
|
||||
} as unknown as MatrixEvent;
|
||||
}
|
||||
|
||||
@@ -604,3 +624,124 @@ export function makeMockGroupCallMemberStateEvent(roomId: string, groupCallId: s
|
||||
getStateKey: jest.fn().mockReturnValue(groupCallId),
|
||||
} as unknown as MatrixEvent;
|
||||
}
|
||||
|
||||
export const REMOTE_SFU_DESCRIPTION =
|
||||
"v=0\n" +
|
||||
"o=- 3242942315779688438 1678878001 IN IP4 0.0.0.0\n" +
|
||||
"s=-\n" +
|
||||
"t=0 0\n" +
|
||||
"a=fingerprint:sha-256 EA:30:B2:7F:49:B5:46:D6:40:72:BF:79:95:C1:65:08:6E:35:09:FB:90:89:DA:EF:6B:82:D1:38:8C:25:39:B2\n" +
|
||||
"a=group:BUNDLE 0 1 2\n" +
|
||||
"m=audio 9 UDP/TLS/RTP/SAVPF 111 9 0 8\n" +
|
||||
"c=IN IP4 0.0.0.0\n" +
|
||||
"a=setup:actpass\n" +
|
||||
"a=mid:0\n" +
|
||||
"a=ice-ufrag:obZwzAcRtxwuozPZ\n" +
|
||||
"a=ice-pwd:TWXNaPeyKTTvRLyIQhWHfHlZHJjtcoKs\n" +
|
||||
"a=rtcp-mux\n" +
|
||||
"a=rtcp-rsize\n" +
|
||||
"a=rtpmap:111 opus/48000/2\n" +
|
||||
"a=fmtp:111 minptime=10;usedtx=1;useinbandfec=1\n" +
|
||||
"a=rtcp-fb:111 transport-cc \n" +
|
||||
"a=rtpmap:9 G722/8000\n" +
|
||||
"a=rtpmap:0 PCMU/8000\n" +
|
||||
"a=rtpmap:8 PCMA/8000\n" +
|
||||
"a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\n" +
|
||||
"a=ssrc:2963372119 cname:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90\n" +
|
||||
"a=ssrc:2963372119 msid:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90 4b811ab6-6926-473d-8ca5-ac45f268c507\n" +
|
||||
"a=ssrc:2963372119 mslabel:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90\n" +
|
||||
"a=ssrc:2963372119 label:4b811ab6-6926-473d-8ca5-ac45f268c507\n" +
|
||||
"a=msid:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90 4b811ab6-6926-473d-8ca5-ac45f268c507\n" +
|
||||
"a=sendrecv\n" +
|
||||
"a=candidate:1155505470 1 udp 2130706431 13.41.173.213 41385 typ host\n" +
|
||||
"a=candidate:1155505470 2 udp 2130706431 13.41.173.213 41385 typ host\n" +
|
||||
"a=candidate:1155505470 1 udp 2130706431 13.41.173.213 40026 typ host\n" +
|
||||
"a=candidate:1155505470 2 udp 2130706431 13.41.173.213 40026 typ host\n" +
|
||||
"a=end-of-candidates\n" +
|
||||
"m=video 9 UDP/TLS/RTP/SAVPF 96 97 102 103 104 106 108 109 98 99 112 116\n" +
|
||||
"c=IN IP4 0.0.0.0\n" +
|
||||
"a=setup:actpass\n" +
|
||||
"a=mid:1\n" +
|
||||
"a=ice-ufrag:obZwzAcRtxwuozPZ\n" +
|
||||
"a=ice-pwd:TWXNaPeyKTTvRLyIQhWHfHlZHJjtcoKs\n" +
|
||||
"a=rtcp-mux\n" +
|
||||
"a=rtcp-rsize\n" +
|
||||
"a=rtpmap:96 VP8/90000\n" +
|
||||
"a=rtcp-fb:96 goog-remb \n" +
|
||||
"a=rtcp-fb:96 transport-cc \n" +
|
||||
"a=rtcp-fb:96 ccm fir\n" +
|
||||
"a=rtcp-fb:96 nack \n" +
|
||||
"a=rtcp-fb:96 nack pli\n" +
|
||||
"a=rtpmap:97 rtx/90000\n" +
|
||||
"a=fmtp:97 apt=96\n" +
|
||||
"a=rtpmap:102 H264/90000\n" +
|
||||
"a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\n" +
|
||||
"a=rtcp-fb:102 goog-remb \n" +
|
||||
"a=rtcp-fb:102 transport-cc \n" +
|
||||
"a=rtcp-fb:102 ccm fir\n" +
|
||||
"a=rtcp-fb:102 nack \n" +
|
||||
"a=rtcp-fb:102 nack pli\n" +
|
||||
"a=rtpmap:103 rtx/90000\n" +
|
||||
"a=fmtp:103 apt=102\n" +
|
||||
"a=rtpmap:104 H264/90000\n" +
|
||||
"a=fmtp:104 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f\n" +
|
||||
"a=rtcp-fb:104 goog-remb \n" +
|
||||
"a=rtcp-fb:104 transport-cc \n" +
|
||||
"a=rtcp-fb:104 ccm fir\n" +
|
||||
"a=rtcp-fb:104 nack \n" +
|
||||
"a=rtcp-fb:104 nack pli\n" +
|
||||
"a=rtpmap:106 H264/90000\n" +
|
||||
"a=fmtp:106 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\n" +
|
||||
"a=rtcp-fb:106 goog-remb \n" +
|
||||
"a=rtcp-fb:106 transport-cc \n" +
|
||||
"a=rtcp-fb:106 ccm fir\n" +
|
||||
"a=rtcp-fb:106 nack \n" +
|
||||
"a=rtcp-fb:106 nack pli\n" +
|
||||
"a=rtpmap:108 H264/90000\n" +
|
||||
"a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f\n" +
|
||||
"a=rtcp-fb:108 goog-remb \n" +
|
||||
"a=rtcp-fb:108 transport-cc \n" +
|
||||
"a=rtcp-fb:108 ccm fir\n" +
|
||||
"a=rtcp-fb:108 nack \n" +
|
||||
"a=rtcp-fb:108 nack pli\n" +
|
||||
"a=rtpmap:109 rtx/90000\n" +
|
||||
"a=fmtp:109 apt=108\n" +
|
||||
"a=rtpmap:98 VP9/90000\n" +
|
||||
"a=fmtp:98 profile-id=0\n" +
|
||||
"a=rtcp-fb:98 goog-remb \n" +
|
||||
"a=rtcp-fb:98 transport-cc \n" +
|
||||
"a=rtcp-fb:98 ccm fir\n" +
|
||||
"a=rtcp-fb:98 nack \n" +
|
||||
"a=rtcp-fb:98 nack pli\n" +
|
||||
"a=rtpmap:99 rtx/90000\n" +
|
||||
"a=fmtp:99 apt=98\n" +
|
||||
"a=rtpmap:112 H264/90000\n" +
|
||||
"a=fmtp:112 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f\n" +
|
||||
"a=rtcp-fb:112 goog-remb \n" +
|
||||
"a=rtcp-fb:112 transport-cc \n" +
|
||||
"a=rtcp-fb:112 ccm fir\n" +
|
||||
"a=rtcp-fb:112 nack \n" +
|
||||
"a=rtcp-fb:112 nack pli\n" +
|
||||
"a=rtpmap:116 ulpfec/90000\n" +
|
||||
"a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\n" +
|
||||
"a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\n" +
|
||||
"a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\n" +
|
||||
"a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\n" +
|
||||
"a=rid:f recv\n" +
|
||||
"a=rid:h recv\n" +
|
||||
"a=rid:q recv\n" +
|
||||
"a=simulcast:recv f;h;q\n" +
|
||||
"a=ssrc:1212931603 cname:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90\n" +
|
||||
"a=ssrc:1212931603 msid:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90 12905f48-75b9-499f-ba50-fc00f56a86c6\n" +
|
||||
"a=ssrc:1212931603 mslabel:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90\n" +
|
||||
"a=ssrc:1212931603 label:12905f48-75b9-499f-ba50-fc00f56a86c6\n" +
|
||||
"a=msid:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90 12905f48-75b9-499f-ba50-fc00f56a86c6\n" +
|
||||
"a=sendrecv\n" +
|
||||
"m=application 9 UDP/DTLS/SCTP webrtc-datachannel\n" +
|
||||
"c=IN IP4 0.0.0.0\n" +
|
||||
"a=setup:actpass\n" +
|
||||
"a=mid:2\n" +
|
||||
"a=sendrecv\n" +
|
||||
"a=sctp-port:5000\n" +
|
||||
"a=ice-ufrag:obZwzAcRtxwuozPZ\n" +
|
||||
"a=ice-pwd:TWXNaPeyKTTvRLyIQhWHfHlZHJjtcoKs";
|
||||
|
||||
@@ -46,13 +46,7 @@ describe("NamespacedValue", () => {
|
||||
});
|
||||
|
||||
it("should not permit falsey values for both parts", () => {
|
||||
try {
|
||||
new UnstableValue(null!, null!);
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Failed to fail");
|
||||
} catch (e) {
|
||||
expect((<Error>e).message).toBe("One of stable or unstable values must be supplied");
|
||||
}
|
||||
expect(() => new UnstableValue(null!, null!)).toThrow("One of stable or unstable values must be supplied");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,12 +66,6 @@ describe("UnstableValue", () => {
|
||||
});
|
||||
|
||||
it("should not permit falsey unstable values", () => {
|
||||
try {
|
||||
new UnstableValue("stable", null!);
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Failed to fail");
|
||||
} catch (e) {
|
||||
expect((<Error>e).message).toBe("Unstable value must be supplied");
|
||||
}
|
||||
expect(() => new UnstableValue("stable", null!)).toThrow("Unstable value must be supplied");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -381,12 +381,7 @@ describe("Crypto", function () {
|
||||
event.senderCurve25519Key = null;
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto!.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
await expect(bobClient.crypto!.decryptEvent(event)).rejects.toBeTruthy();
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -405,7 +400,7 @@ describe("Crypto", function () {
|
||||
// the first message can't be decrypted yet, but the second one
|
||||
// can
|
||||
let ksEvent = await keyshareEventForEvent(aliceClient, events[1], 1);
|
||||
bobClient.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
bobClient.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
await decryptEventsPromise;
|
||||
@@ -617,12 +612,7 @@ describe("Crypto", function () {
|
||||
event.senderCurve25519Key = null;
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await secondAliceClient.crypto!.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
await expect(secondAliceClient.crypto!.decryptEvent(event)).rejects.toBeTruthy();
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -725,12 +715,7 @@ describe("Crypto", function () {
|
||||
event.senderCurve25519Key = null;
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto!.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
await expect(bobClient.crypto!.decryptEvent(event)).rejects.toBeTruthy();
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -805,12 +790,7 @@ describe("Crypto", function () {
|
||||
event.senderCurve25519Key = null;
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto!.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
await expect(bobClient.crypto!.decryptEvent(event)).rejects.toBeTruthy();
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -897,12 +877,7 @@ describe("Crypto", function () {
|
||||
event.senderCurve25519Key = null;
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto!.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
await expect(bobClient.crypto!.decryptEvent(event)).rejects.toBeTruthy();
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1011,7 +986,6 @@ describe("Crypto", function () {
|
||||
jest.setTimeout(10000);
|
||||
const client = new TestClient("@a:example.com", "dev").client;
|
||||
await client.initCrypto();
|
||||
client.crypto!.getSecretStorageKey = jest.fn().mockResolvedValue(null);
|
||||
client.crypto!.isCrossSigningReady = async () => false;
|
||||
client.crypto!.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null);
|
||||
client.crypto!.baseApis.setAccountData = jest.fn().mockResolvedValue(null);
|
||||
@@ -1039,7 +1013,7 @@ describe("Crypto", function () {
|
||||
|
||||
beforeEach(async () => {
|
||||
ensureOlmSessionsForDevices = jest.spyOn(olmlib, "ensureOlmSessionsForDevices");
|
||||
ensureOlmSessionsForDevices.mockResolvedValue({});
|
||||
ensureOlmSessionsForDevices.mockResolvedValue(new Map());
|
||||
encryptMessageForDevice = jest.spyOn(olmlib, "encryptMessageForDevice");
|
||||
encryptMessageForDevice.mockImplementation(async (...[result, , , , , , payload]) => {
|
||||
result.plaintext = { type: 0, body: JSON.stringify(payload) };
|
||||
|
||||
@@ -102,9 +102,10 @@ describe("CrossSigningInfo.getCrossSigningKey", function () {
|
||||
const info = new CrossSigningInfo(userId, { getCrossSigningKey }, { getCrossSigningKeyCache });
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
|
||||
expect(getCrossSigningKeyCache).toHaveBeenCalledTimes(shouldCache ? 1 : 0);
|
||||
if (shouldCache) {
|
||||
expect(getCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(getCrossSigningKeyCache).toHaveBeenLastCalledWith(type, expect.any(String));
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -115,10 +116,10 @@ describe("CrossSigningInfo.getCrossSigningKey", function () {
|
||||
const info = new CrossSigningInfo(userId, { getCrossSigningKey }, { storeCrossSigningKeyCache });
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(shouldCache ? 1 : 0);
|
||||
expect(storeCrossSigningKeyCache).toHaveBeenCalledTimes(shouldCache ? 1 : 0);
|
||||
if (shouldCache) {
|
||||
expect(storeCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
|
||||
expect(storeCrossSigningKeyCache.mock.calls[0][1]).toBe(testKey);
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(storeCrossSigningKeyCache).toHaveBeenLastCalledWith(type, testKey);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import { ClientEvent, MatrixClient, RoomMember } from "../../../../src";
|
||||
import { DeviceInfo, IDevice } from "../../../../src/crypto/deviceinfo";
|
||||
import { DeviceTrustLevel } from "../../../../src/crypto/CrossSigning";
|
||||
import { MegolmEncryption as MegolmEncryptionClass } from "../../../../src/crypto/algorithms/megolm";
|
||||
import { recursiveMapToObject } from "../../../../src/utils";
|
||||
import { sleep } from "../../../../src/utils";
|
||||
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!;
|
||||
@@ -183,14 +184,22 @@ describe("MegolmDecryption", function () {
|
||||
const deviceInfo = {} as DeviceInfo;
|
||||
mockCrypto.getStoredDevice.mockReturnValue(deviceInfo);
|
||||
|
||||
mockOlmLib.ensureOlmSessionsForDevices.mockResolvedValue({
|
||||
"@alice:foo": {
|
||||
alidevice: {
|
||||
sessionId: "alisession",
|
||||
device: new DeviceInfo("alidevice"),
|
||||
},
|
||||
},
|
||||
});
|
||||
mockOlmLib.ensureOlmSessionsForDevices.mockResolvedValue(
|
||||
new Map([
|
||||
[
|
||||
"@alice:foo",
|
||||
new Map([
|
||||
[
|
||||
"alidevice",
|
||||
{
|
||||
sessionId: "alisession",
|
||||
device: new DeviceInfo("alidevice"),
|
||||
},
|
||||
],
|
||||
]),
|
||||
],
|
||||
]),
|
||||
);
|
||||
|
||||
const awaitEncryptForDevice = new Promise<void>((res, rej) => {
|
||||
mockOlmLib.encryptMessageForDevice.mockImplementation(() => {
|
||||
@@ -357,11 +366,7 @@ describe("MegolmDecryption", function () {
|
||||
} as unknown as DeviceInfo;
|
||||
|
||||
mockCrypto.downloadKeys.mockReturnValue(
|
||||
Promise.resolve({
|
||||
"@alice:home.server": {
|
||||
aliceDevice: aliceDeviceInfo,
|
||||
},
|
||||
}),
|
||||
Promise.resolve(new Map([["@alice:home.server", new Map([["aliceDevice", aliceDeviceInfo]])]])),
|
||||
);
|
||||
|
||||
mockCrypto.checkDeviceTrust.mockReturnValue({
|
||||
@@ -523,23 +528,32 @@ describe("MegolmDecryption", function () {
|
||||
let megolm: MegolmEncryptionClass;
|
||||
let room: jest.Mocked<Room>;
|
||||
|
||||
const deviceMap: DeviceInfoMap = {
|
||||
"user-a": {
|
||||
"device-a": new DeviceInfo("device-a"),
|
||||
"device-b": new DeviceInfo("device-b"),
|
||||
"device-c": new DeviceInfo("device-c"),
|
||||
},
|
||||
"user-b": {
|
||||
"device-d": new DeviceInfo("device-d"),
|
||||
"device-e": new DeviceInfo("device-e"),
|
||||
"device-f": new DeviceInfo("device-f"),
|
||||
},
|
||||
"user-c": {
|
||||
"device-g": new DeviceInfo("device-g"),
|
||||
"device-h": new DeviceInfo("device-h"),
|
||||
"device-i": new DeviceInfo("device-i"),
|
||||
},
|
||||
};
|
||||
const deviceMap: DeviceInfoMap = new Map([
|
||||
[
|
||||
"user-a",
|
||||
new Map([
|
||||
["device-a", new DeviceInfo("device-a")],
|
||||
["device-b", new DeviceInfo("device-b")],
|
||||
["device-c", new DeviceInfo("device-c")],
|
||||
]),
|
||||
],
|
||||
[
|
||||
"user-b",
|
||||
new Map([
|
||||
["device-d", new DeviceInfo("device-d")],
|
||||
["device-e", new DeviceInfo("device-e")],
|
||||
["device-f", new DeviceInfo("device-f")],
|
||||
]),
|
||||
],
|
||||
[
|
||||
"user-c",
|
||||
new Map([
|
||||
["device-g", new DeviceInfo("device-g")],
|
||||
["device-h", new DeviceInfo("device-h")],
|
||||
["device-i", new DeviceInfo("device-i")],
|
||||
]),
|
||||
],
|
||||
]);
|
||||
|
||||
beforeEach(() => {
|
||||
room = testUtils.mock(Room, "Room") as jest.Mocked<Room>;
|
||||
@@ -572,8 +586,8 @@ describe("MegolmDecryption", function () {
|
||||
//@ts-ignore private member access, gross
|
||||
await megolm.encryptionPreparation?.promise;
|
||||
|
||||
for (const userId in deviceMap) {
|
||||
for (const deviceId in deviceMap[userId]) {
|
||||
for (const [userId, devices] of deviceMap) {
|
||||
for (const deviceId of devices.keys()) {
|
||||
expect(mockCrypto.checkDeviceTrust).toHaveBeenCalledWith(userId, deviceId);
|
||||
}
|
||||
}
|
||||
@@ -658,20 +672,20 @@ describe("MegolmDecryption", function () {
|
||||
expect(aliceClient.sendToDevice).toHaveBeenCalled();
|
||||
const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0];
|
||||
expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/);
|
||||
delete contentMap["@bob:example.com"].bobdevice1.session_id;
|
||||
delete contentMap["@bob:example.com"].bobdevice1["org.matrix.msgid"];
|
||||
delete contentMap["@bob:example.com"].bobdevice2.session_id;
|
||||
delete contentMap["@bob:example.com"].bobdevice2["org.matrix.msgid"];
|
||||
expect(contentMap).toStrictEqual({
|
||||
"@bob:example.com": {
|
||||
bobdevice1: {
|
||||
delete contentMap.get("@bob:example.com")?.get("bobdevice1")?.["session_id"];
|
||||
delete contentMap.get("@bob:example.com")?.get("bobdevice1")?.["org.matrix.msgid"];
|
||||
delete contentMap.get("@bob:example.com")?.get("bobdevice2")?.["session_id"];
|
||||
delete contentMap.get("@bob:example.com")?.get("bobdevice2")?.["org.matrix.msgid"];
|
||||
expect(recursiveMapToObject(contentMap)).toStrictEqual({
|
||||
["@bob:example.com"]: {
|
||||
["bobdevice1"]: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: roomId,
|
||||
code: "m.unverified",
|
||||
reason: "The sender has disabled encrypting to unverified devices.",
|
||||
sender_key: aliceDevice.deviceCurve25519Key,
|
||||
},
|
||||
bobdevice2: {
|
||||
["bobdevice2"]: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: roomId,
|
||||
code: "m.blacklisted",
|
||||
@@ -839,10 +853,10 @@ describe("MegolmDecryption", function () {
|
||||
expect(aliceClient.sendToDevice).toHaveBeenCalled();
|
||||
const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0];
|
||||
expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/);
|
||||
delete contentMap["@bob:example.com"]["bobdevice"]["org.matrix.msgid"];
|
||||
expect(contentMap).toStrictEqual({
|
||||
"@bob:example.com": {
|
||||
bobdevice: {
|
||||
delete contentMap.get("@bob:example.com")?.get("bobdevice")?.["org.matrix.msgid"];
|
||||
expect(recursiveMapToObject(contentMap)).toStrictEqual({
|
||||
["@bob:example.com"]: {
|
||||
["bobdevice"]: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
code: "m.no_olm",
|
||||
reason: "Unable to establish a secure channel.",
|
||||
|
||||
@@ -146,18 +146,21 @@ describe("OlmDevice", function () {
|
||||
});
|
||||
},
|
||||
} as unknown as MockedObject<MatrixClient>;
|
||||
const devicesByUser = {
|
||||
"@bob:example.com": [
|
||||
DeviceInfo.fromStorage(
|
||||
{
|
||||
keys: {
|
||||
"curve25519:ABCDEFG": "akey",
|
||||
const devicesByUser = new Map([
|
||||
[
|
||||
"@bob:example.com",
|
||||
[
|
||||
DeviceInfo.fromStorage(
|
||||
{
|
||||
keys: {
|
||||
"curve25519:ABCDEFG": "akey",
|
||||
},
|
||||
},
|
||||
},
|
||||
"ABCDEFG",
|
||||
),
|
||||
"ABCDEFG",
|
||||
),
|
||||
],
|
||||
],
|
||||
};
|
||||
]);
|
||||
|
||||
// start two tasks that try to ensure that there's an olm session
|
||||
const promises = Promise.all([
|
||||
@@ -218,12 +221,8 @@ describe("OlmDevice", function () {
|
||||
// There's no required ordering of devices per user, so here we
|
||||
// create two different orderings so that each task reserves a
|
||||
// device the other task needs before continuing.
|
||||
const devicesByUserAB = {
|
||||
"@bob:example.com": [deviceBobA, deviceBobB],
|
||||
};
|
||||
const devicesByUserBA = {
|
||||
"@bob:example.com": [deviceBobB, deviceBobA],
|
||||
};
|
||||
const devicesByUserAB = new Map([["@bob:example.com", [deviceBobA, deviceBobB]]]);
|
||||
const devicesByUserBA = new Map([["@bob:example.com", [deviceBobB, deviceBobA]]]);
|
||||
|
||||
const task1 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(aliceOlmDevice, baseApis, devicesByUserAB));
|
||||
|
||||
|
||||
@@ -456,6 +456,7 @@ describe("MegolmBackup", function () {
|
||||
client.http.authedRequest = function (method, path, queryParams, data, opts): any {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqual(2);
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
if (numCalls === 1) {
|
||||
expect(method).toBe("POST");
|
||||
expect(path).toBe("/room_keys/version");
|
||||
@@ -482,6 +483,7 @@ describe("MegolmBackup", function () {
|
||||
reject(new Error("authedRequest called too many times"));
|
||||
return Promise.resolve({});
|
||||
}
|
||||
/* eslint-enable jest/no-conditional-expect */
|
||||
};
|
||||
}),
|
||||
client.createKeyBackupVersion({
|
||||
|
||||
@@ -24,10 +24,11 @@ import * as olmlib from "../../../src/crypto/olmlib";
|
||||
import { MatrixError } from "../../../src/http-api";
|
||||
import { logger } from "../../../src/logger";
|
||||
import { ICrossSigningKey, ICreateClientOpts, ISignedKey, MatrixClient } from "../../../src/client";
|
||||
import { CryptoEvent, IBootstrapCrossSigningOpts } from "../../../src/crypto";
|
||||
import { CryptoEvent } from "../../../src/crypto";
|
||||
import { IDevice } from "../../../src/crypto/deviceinfo";
|
||||
import { TestClient } from "../../TestClient";
|
||||
import { resetCrossSigningKeys } from "./crypto-utils";
|
||||
import { BootstrapCrossSigningOpts } from "../../../src/crypto-api";
|
||||
|
||||
const PUSH_RULES_RESPONSE: Response = {
|
||||
method: "GET",
|
||||
@@ -146,7 +147,7 @@ describe("Cross Signing", function () {
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
alice.setAccountData = async () => ({});
|
||||
alice.getAccountDataFromServer = async <T extends { [k: string]: any }>(): Promise<T | null> => ({} as T);
|
||||
const authUploadDeviceSigningKeys: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"] = async (func) => {
|
||||
const authUploadDeviceSigningKeys: BootstrapCrossSigningOpts["authUploadDeviceSigningKeys"] = async (func) => {
|
||||
await func({});
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
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 { DeviceInfo } from "../../../src/crypto/deviceinfo";
|
||||
import { DeviceVerification } from "../../../src";
|
||||
import { deviceInfoToDevice } from "../../../src/crypto/device-converter";
|
||||
|
||||
describe("device-converter", () => {
|
||||
const userId = "@alice:example.com";
|
||||
const deviceId = "xcvf";
|
||||
|
||||
// All parameters for DeviceInfo initialization
|
||||
const keys = {
|
||||
[`ed25519:${deviceId}`]: "key1",
|
||||
[`curve25519:${deviceId}`]: "key2",
|
||||
};
|
||||
const algorithms = ["algo1", "algo2"];
|
||||
const verified = DeviceVerification.Verified;
|
||||
const signatures = { [userId]: { [deviceId]: "sign1" } };
|
||||
const displayName = "display name";
|
||||
const unsigned = {
|
||||
device_display_name: displayName,
|
||||
};
|
||||
|
||||
describe("deviceInfoToDevice", () => {
|
||||
it("should convert a DeviceInfo to a Device", () => {
|
||||
const deviceInfo = DeviceInfo.fromStorage({ keys, algorithms, verified, signatures, unsigned }, deviceId);
|
||||
const device = deviceInfoToDevice(deviceInfo, userId);
|
||||
|
||||
expect(device.deviceId).toBe(deviceId);
|
||||
expect(device.userId).toBe(userId);
|
||||
expect(device.verified).toBe(verified);
|
||||
expect(device.getIdentityKey()).toBe(keys[`curve25519:${deviceId}`]);
|
||||
expect(device.getFingerprint()).toBe(keys[`ed25519:${deviceId}`]);
|
||||
expect(device.displayName).toBe(displayName);
|
||||
});
|
||||
|
||||
it("should add empty signatures", () => {
|
||||
const deviceInfo = DeviceInfo.fromStorage({ keys, algorithms, verified }, deviceId);
|
||||
const device = deviceInfoToDevice(deviceInfo, userId);
|
||||
|
||||
expect(device.signatures.size).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,6 @@ limitations under the License.
|
||||
import "../../olm-loader";
|
||||
import * as olmlib from "../../../src/crypto/olmlib";
|
||||
import { IObject } from "../../../src/crypto/olmlib";
|
||||
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/crypto/SecretStorage";
|
||||
import { MatrixEvent } from "../../../src/models/event";
|
||||
import { TestClient } from "../../TestClient";
|
||||
import { makeTestClients } from "./verification/util";
|
||||
@@ -25,10 +24,10 @@ import { encryptAES } from "../../../src/crypto/aes";
|
||||
import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils";
|
||||
import { logger } from "../../../src/logger";
|
||||
import { ClientEvent, ICreateClientOpts, ICrossSigningKey, MatrixClient } from "../../../src/client";
|
||||
import { ISecretStorageKeyInfo } from "../../../src/crypto/api";
|
||||
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
|
||||
import { ISignatures } from "../../../src/@types/signed";
|
||||
import { ICurve25519AuthData } from "../../../src/crypto/keybackup";
|
||||
import { SecretStorageKeyDescription, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
|
||||
|
||||
async function makeTestClient(
|
||||
userInfo: { userId: string; deviceId: string },
|
||||
@@ -45,7 +44,7 @@ async function makeTestClient(
|
||||
await client.initCrypto();
|
||||
|
||||
// No need to download keys for these tests
|
||||
jest.spyOn(client.crypto!, "downloadKeys").mockResolvedValue({});
|
||||
jest.spyOn(client.crypto!, "downloadKeys").mockResolvedValue(new Map());
|
||||
|
||||
return client;
|
||||
}
|
||||
@@ -149,22 +148,14 @@ describe("Secrets", function () {
|
||||
it("should throw if given a key that doesn't exist", async function () {
|
||||
const alice = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
|
||||
|
||||
try {
|
||||
await alice.storeSecret("foo", "bar", ["this secret does not exist"]);
|
||||
// should be able to use expect(...).toThrow() but mocha still fails
|
||||
// the test even when it throws for reasons I have no inclination to debug
|
||||
expect(true).toBeFalsy();
|
||||
} catch (e) {}
|
||||
await expect(alice.storeSecret("foo", "bar", ["this secret does not exist"])).rejects.toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should refuse to encrypt with zero keys", async function () {
|
||||
const alice = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
|
||||
|
||||
try {
|
||||
await alice.storeSecret("foo", "bar", []);
|
||||
expect(true).toBeFalsy();
|
||||
} catch (e) {}
|
||||
await expect(alice.storeSecret("foo", "bar", [])).rejects.toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
@@ -215,10 +206,7 @@ describe("Secrets", function () {
|
||||
it("should refuse to encrypt if no keys given and no default key", async function () {
|
||||
const alice = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
|
||||
|
||||
try {
|
||||
await alice.storeSecret("foo", "bar");
|
||||
expect(true).toBeFalsy();
|
||||
} catch (e) {}
|
||||
await expect(alice.storeSecret("foo", "bar")).rejects.toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
@@ -274,7 +262,7 @@ describe("Secrets", function () {
|
||||
Object.values(otks)[0],
|
||||
);
|
||||
|
||||
osborne2.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
osborne2.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
osborne2.client.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
|
||||
const request = await secretStorage.request("foo", ["VAX"]);
|
||||
@@ -541,7 +529,9 @@ describe("Secrets", function () {
|
||||
await alice.bootstrapSecretStorage({});
|
||||
|
||||
expect(alice.getAccountData("m.secret_storage.default_key")!.getContent()).toEqual({ key: "key_id" });
|
||||
const keyInfo = alice.getAccountData("m.secret_storage.key.key_id")!.getContent<ISecretStorageKeyInfo>();
|
||||
const keyInfo = alice
|
||||
.getAccountData("m.secret_storage.key.key_id")!
|
||||
.getContent<SecretStorageKeyDescription>();
|
||||
expect(keyInfo.algorithm).toEqual("m.secret_storage.v1.aes-hmac-sha2");
|
||||
expect(keyInfo.passphrase).toEqual({
|
||||
algorithm: "m.pbkdf2",
|
||||
|
||||
@@ -121,12 +121,12 @@ describe("SAS verification", function () {
|
||||
|
||||
alice.client.crypto!.deviceList.storeDevicesForUser("@bob:example.com", BOB_DEVICES);
|
||||
alice.client.downloadKeys = () => {
|
||||
return Promise.resolve({});
|
||||
return Promise.resolve(new Map());
|
||||
};
|
||||
|
||||
bob.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", ALICE_DEVICES);
|
||||
bob.client.downloadKeys = () => {
|
||||
return Promise.resolve({});
|
||||
return Promise.resolve(new Map());
|
||||
};
|
||||
|
||||
aliceSasEvent = null;
|
||||
@@ -144,7 +144,7 @@ describe("SAS verification", function () {
|
||||
expect(e.sas).toEqual(aliceSasEvent.sas);
|
||||
e.confirm();
|
||||
aliceSasEvent.confirm();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
e.mismatch();
|
||||
aliceSasEvent.mismatch();
|
||||
}
|
||||
@@ -169,13 +169,14 @@ describe("SAS verification", function () {
|
||||
expect(e.sas).toEqual(bobSasEvent.sas);
|
||||
e.confirm();
|
||||
bobSasEvent.confirm();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
e.mismatch();
|
||||
bobSasEvent.mismatch();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all([alice.stop(), bob.stop()]);
|
||||
|
||||
@@ -186,10 +187,14 @@ describe("SAS verification", function () {
|
||||
let macMethod;
|
||||
let keyAgreement;
|
||||
const origSendToDevice = bob.client.sendToDevice.bind(bob.client);
|
||||
bob.client.sendToDevice = function (type, map) {
|
||||
bob.client.sendToDevice = async (type, map) => {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!].message_authentication_code;
|
||||
keyAgreement = map[alice.client.getUserId()!][alice.client.deviceId!].key_agreement_protocol;
|
||||
macMethod = map
|
||||
.get(alice.client.getUserId()!)
|
||||
?.get(alice.client.deviceId!)?.message_authentication_code;
|
||||
keyAgreement = map
|
||||
.get(alice.client.getUserId()!)
|
||||
?.get(alice.client.deviceId!)?.key_agreement_protocol;
|
||||
}
|
||||
return origSendToDevice(type, map);
|
||||
};
|
||||
@@ -237,7 +242,7 @@ describe("SAS verification", function () {
|
||||
// has, since it is the same object. If this does not
|
||||
// happen, the verification will fail due to a hash
|
||||
// commitment mismatch.
|
||||
map[bob.client.getUserId()!][bob.client.deviceId!].message_authentication_codes = [
|
||||
map.get(bob.client.getUserId()!)!.get(bob.client.deviceId!)!.message_authentication_codes = [
|
||||
"hkdf-hmac-sha256",
|
||||
];
|
||||
}
|
||||
@@ -246,7 +251,9 @@ describe("SAS verification", function () {
|
||||
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
|
||||
bob.client.sendToDevice = (type, map) => {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!].message_authentication_code;
|
||||
macMethod = map
|
||||
.get(alice.client.getUserId()!)!
|
||||
.get(alice.client.deviceId!)!.message_authentication_code;
|
||||
}
|
||||
return bobOrigSendToDevice(type, map);
|
||||
};
|
||||
@@ -291,14 +298,18 @@ describe("SAS verification", function () {
|
||||
// has, since it is the same object. If this does not
|
||||
// happen, the verification will fail due to a hash
|
||||
// commitment mismatch.
|
||||
map[bob.client.getUserId()!][bob.client.deviceId!].message_authentication_codes = ["hmac-sha256"];
|
||||
map.get(bob.client.getUserId()!)!.get(bob.client.deviceId!)!.message_authentication_codes = [
|
||||
"hmac-sha256",
|
||||
];
|
||||
}
|
||||
return aliceOrigSendToDevice(type, map);
|
||||
};
|
||||
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
|
||||
bob.client.sendToDevice = (type, map) => {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!].message_authentication_code;
|
||||
macMethod = map
|
||||
.get(alice.client.getUserId()!)!
|
||||
.get(alice.client.deviceId!)!.message_authentication_code;
|
||||
}
|
||||
return bobOrigSendToDevice(type, map);
|
||||
};
|
||||
@@ -363,6 +374,12 @@ describe("SAS verification", function () {
|
||||
expect(bobDeviceTrust.isLocallyVerified()).toBeTruthy();
|
||||
expect(bobDeviceTrust.isCrossSigningVerified()).toBeFalsy();
|
||||
|
||||
const bobDeviceVerificationStatus = (await alice.client
|
||||
.getCrypto()!
|
||||
.getDeviceVerificationStatus("@bob:example.com", "Dynabook"))!;
|
||||
expect(bobDeviceVerificationStatus.localVerified).toBe(true);
|
||||
expect(bobDeviceVerificationStatus.crossSigningVerified).toBe(false);
|
||||
|
||||
const aliceTrust = bob.client.checkUserTrust("@alice:example.com");
|
||||
expect(aliceTrust.isCrossSigningVerified()).toBeTruthy();
|
||||
expect(aliceTrust.isTofu()).toBeTruthy();
|
||||
@@ -370,6 +387,17 @@ describe("SAS verification", function () {
|
||||
const aliceDeviceTrust = bob.client.checkDeviceTrust("@alice:example.com", "Osborne2");
|
||||
expect(aliceDeviceTrust.isLocallyVerified()).toBeTruthy();
|
||||
expect(aliceDeviceTrust.isCrossSigningVerified()).toBeFalsy();
|
||||
|
||||
const aliceDeviceVerificationStatus = (await bob.client
|
||||
.getCrypto()!
|
||||
.getDeviceVerificationStatus("@alice:example.com", "Osborne2"))!;
|
||||
expect(aliceDeviceVerificationStatus.localVerified).toBe(true);
|
||||
expect(aliceDeviceVerificationStatus.crossSigningVerified).toBe(false);
|
||||
|
||||
const unknownDeviceVerificationStatus = await bob.client
|
||||
.getCrypto()!
|
||||
.getDeviceVerificationStatus("@alice:example.com", "xyz");
|
||||
expect(unknownDeviceVerificationStatus).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -454,7 +482,7 @@ describe("SAS verification", function () {
|
||||
);
|
||||
};
|
||||
alice.client.downloadKeys = () => {
|
||||
return Promise.resolve({});
|
||||
return Promise.resolve(new Map());
|
||||
};
|
||||
|
||||
bob.client.crypto!.setDeviceVerification = jest.fn();
|
||||
@@ -472,7 +500,7 @@ describe("SAS verification", function () {
|
||||
return "bob+base64+ed25519+key";
|
||||
};
|
||||
bob.client.downloadKeys = () => {
|
||||
return Promise.resolve({});
|
||||
return Promise.resolve(new Map());
|
||||
};
|
||||
|
||||
aliceSasEvent = null;
|
||||
@@ -491,7 +519,7 @@ describe("SAS verification", function () {
|
||||
expect(e.sas).toEqual(aliceSasEvent.sas);
|
||||
e.confirm();
|
||||
aliceSasEvent.confirm();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
e.mismatch();
|
||||
aliceSasEvent.mismatch();
|
||||
}
|
||||
@@ -515,7 +543,7 @@ describe("SAS verification", function () {
|
||||
expect(e.sas).toEqual(bobSasEvent.sas);
|
||||
e.confirm();
|
||||
bobSasEvent.confirm();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
e.mismatch();
|
||||
bobSasEvent.mismatch();
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import { IContent, MatrixEvent } from "../../../../src/models/event";
|
||||
import { IRoomTimelineData } from "../../../../src/models/event-timeline-set";
|
||||
import { Room, RoomEvent } from "../../../../src/models/room";
|
||||
import { logger } from "../../../../src/logger";
|
||||
import { MatrixClient, ClientEvent, ICreateClientOpts } from "../../../../src/client";
|
||||
import { MatrixClient, ClientEvent, ICreateClientOpts, SendToDeviceContentMap } from "../../../../src/client";
|
||||
|
||||
interface UserInfo {
|
||||
userId: string;
|
||||
@@ -36,16 +36,16 @@ export async function makeTestClients(
|
||||
const clientMap: Record<string, Record<string, MatrixClient>> = {};
|
||||
const makeSendToDevice =
|
||||
(matrixClient: MatrixClient): MatrixClient["sendToDevice"] =>
|
||||
async (type, map) => {
|
||||
async (type: string, contentMap: SendToDeviceContentMap) => {
|
||||
// logger.log(this.getUserId(), "sends", type, map);
|
||||
for (const [userId, devMap] of Object.entries(map)) {
|
||||
for (const [userId, deviceMessages] of contentMap) {
|
||||
if (userId in clientMap) {
|
||||
for (const [deviceId, msg] of Object.entries(devMap)) {
|
||||
for (const [deviceId, message] of deviceMessages) {
|
||||
if (deviceId in clientMap[userId]) {
|
||||
const event = new MatrixEvent({
|
||||
sender: matrixClient.getUserId()!,
|
||||
type: type,
|
||||
content: msg,
|
||||
content: message,
|
||||
});
|
||||
const client = clientMap[userId][deviceId];
|
||||
const decryptionPromise = event.isEncrypted()
|
||||
|
||||
@@ -25,6 +25,7 @@ import { IContent, MatrixEvent } from "../../../../src/models/event";
|
||||
import { MatrixClient } from "../../../../src/client";
|
||||
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
|
||||
import { VerificationBase } from "../../../../src/crypto/verification/Base";
|
||||
import { MapWithDefault } from "../../../../src/utils";
|
||||
|
||||
type MockClient = MatrixClient & {
|
||||
popEvents: () => MatrixEvent[];
|
||||
@@ -33,7 +34,9 @@ type MockClient = MatrixClient & {
|
||||
function makeMockClient(userId: string, deviceId: string): MockClient {
|
||||
let counter = 1;
|
||||
let events: MatrixEvent[] = [];
|
||||
const deviceEvents: Record<string, Record<string, MatrixEvent[]>> = {};
|
||||
const deviceEvents: MapWithDefault<string, MapWithDefault<string, MatrixEvent[]>> = new MapWithDefault(
|
||||
() => new MapWithDefault(() => []),
|
||||
);
|
||||
return {
|
||||
getUserId() {
|
||||
return userId;
|
||||
@@ -58,15 +61,11 @@ function makeMockClient(userId: string, deviceId: string): MockClient {
|
||||
return Promise.resolve({ event_id: eventId });
|
||||
},
|
||||
|
||||
sendToDevice(type: string, msgMap: Record<string, Record<string, IContent>>) {
|
||||
for (const userId of Object.keys(msgMap)) {
|
||||
const deviceMap = msgMap[userId];
|
||||
for (const deviceId of Object.keys(deviceMap)) {
|
||||
const content = deviceMap[deviceId];
|
||||
sendToDevice(type: string, msgMap: Map<string, Map<string, IContent>>) {
|
||||
for (const [userId, deviceMessages] of msgMap) {
|
||||
for (const [deviceId, content] of deviceMessages) {
|
||||
const event = new MatrixEvent({ content, type });
|
||||
deviceEvents[userId] = deviceEvents[userId] || {};
|
||||
deviceEvents[userId][deviceId] = deviceEvents[userId][deviceId] || [];
|
||||
deviceEvents[userId][deviceId].push(event);
|
||||
deviceEvents.getOrCreate(userId).getOrCreate(deviceId).push(event);
|
||||
}
|
||||
}
|
||||
return Promise.resolve({});
|
||||
@@ -79,14 +78,9 @@ function makeMockClient(userId: string, deviceId: string): MockClient {
|
||||
return e;
|
||||
},
|
||||
|
||||
// @ts-ignore special testing fn
|
||||
popDeviceEvents(userId: string, deviceId: string): MatrixEvent[] {
|
||||
const forDevice = deviceEvents[userId];
|
||||
const events = forDevice && forDevice[deviceId];
|
||||
const result = events || [];
|
||||
if (events) {
|
||||
delete forDevice[deviceId];
|
||||
}
|
||||
const result = deviceEvents.get(userId)?.get(deviceId) || [];
|
||||
deviceEvents?.get(userId)?.delete(deviceId);
|
||||
return result;
|
||||
},
|
||||
} as unknown as MockClient;
|
||||
|
||||
@@ -204,9 +204,14 @@ describe("RoomWidgetClient", () => {
|
||||
});
|
||||
|
||||
describe("to-device messages", () => {
|
||||
const unencryptedContentMap = {
|
||||
"@alice:example.org": { "*": { hello: "alice!" } },
|
||||
"@bob:example.org": { bobDesktop: { hello: "bob!" } },
|
||||
const unencryptedContentMap = new Map([
|
||||
["@alice:example.org", new Map([["*", { hello: "alice!" }]])],
|
||||
["@bob:example.org", new Map([["bobDesktop", { hello: "bob!" }]])],
|
||||
]);
|
||||
|
||||
const expectedRequestData = {
|
||||
["@alice:example.org"]: { ["*"]: { hello: "alice!" } },
|
||||
["@bob:example.org"]: { ["bobDesktop"]: { hello: "bob!" } },
|
||||
};
|
||||
|
||||
it("sends unencrypted (sendToDevice)", async () => {
|
||||
@@ -214,7 +219,7 @@ describe("RoomWidgetClient", () => {
|
||||
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
|
||||
|
||||
await client.sendToDevice("org.example.foo", unencryptedContentMap);
|
||||
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, unencryptedContentMap);
|
||||
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, expectedRequestData);
|
||||
});
|
||||
|
||||
it("sends unencrypted (queueToDevice)", async () => {
|
||||
@@ -229,7 +234,7 @@ describe("RoomWidgetClient", () => {
|
||||
],
|
||||
};
|
||||
await client.queueToDevice(batch);
|
||||
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, unencryptedContentMap);
|
||||
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, expectedRequestData);
|
||||
});
|
||||
|
||||
it("sends encrypted (encryptAndSendToDevices)", async () => {
|
||||
|
||||
@@ -24,13 +24,10 @@ import {
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
RelationType,
|
||||
Room,
|
||||
RoomEvent,
|
||||
} from "../../src";
|
||||
import { FeatureSupport, Thread } from "../../src/models/thread";
|
||||
import { Thread } from "../../src/models/thread";
|
||||
import { ReEmitter } from "../../src/ReEmitter";
|
||||
import { eventMapperFor } from "../../src/event-mapper";
|
||||
|
||||
describe("EventTimelineSet", () => {
|
||||
const roomId = "!foo:bar";
|
||||
@@ -82,6 +79,7 @@ describe("EventTimelineSet", () => {
|
||||
beforeEach(() => {
|
||||
client = utils.mock(MatrixClient, "MatrixClient");
|
||||
client.reEmitter = utils.mock(ReEmitter, "ReEmitter");
|
||||
client.canSupport = new Map();
|
||||
room = new Room(roomId, client, userA);
|
||||
eventTimelineSet = new EventTimelineSet(room);
|
||||
eventTimeline = new EventTimeline(eventTimelineSet);
|
||||
@@ -205,88 +203,6 @@ describe("EventTimelineSet", () => {
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
});
|
||||
|
||||
it("should allow edits to be added to thread timeline", async () => {
|
||||
jest.spyOn(client, "supportsThreads").mockReturnValue(true);
|
||||
jest.spyOn(client, "getEventMapper").mockReturnValue(eventMapperFor(client, {}));
|
||||
Thread.hasServerSideSupport = FeatureSupport.Stable;
|
||||
|
||||
const sender = "@alice:matrix.org";
|
||||
|
||||
const root = utils.mkEvent({
|
||||
event: true,
|
||||
content: {
|
||||
body: "Thread root",
|
||||
},
|
||||
type: EventType.RoomMessage,
|
||||
sender,
|
||||
});
|
||||
room.addLiveEvents([root]);
|
||||
|
||||
const threadReply = utils.mkEvent({
|
||||
event: true,
|
||||
content: {
|
||||
"body": "Thread reply",
|
||||
"m.relates_to": {
|
||||
event_id: root.getId()!,
|
||||
rel_type: RelationType.Thread,
|
||||
},
|
||||
},
|
||||
type: EventType.RoomMessage,
|
||||
sender,
|
||||
});
|
||||
|
||||
root.setUnsigned({
|
||||
"m.relations": {
|
||||
[RelationType.Thread]: {
|
||||
count: 1,
|
||||
latest_event: {
|
||||
content: threadReply.getContent(),
|
||||
origin_server_ts: 5,
|
||||
room_id: room.roomId,
|
||||
sender,
|
||||
type: EventType.RoomMessage,
|
||||
event_id: threadReply.getId()!,
|
||||
user_id: sender,
|
||||
age: 1,
|
||||
},
|
||||
current_user_participated: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const editToThreadReply = utils.mkEvent({
|
||||
event: true,
|
||||
content: {
|
||||
"body": " * edit",
|
||||
"m.new_content": {
|
||||
"body": "edit",
|
||||
"msgtype": "m.text",
|
||||
"org.matrix.msc1767.text": "edit",
|
||||
},
|
||||
"m.relates_to": {
|
||||
event_id: threadReply.getId()!,
|
||||
rel_type: RelationType.Replace,
|
||||
},
|
||||
},
|
||||
type: EventType.RoomMessage,
|
||||
sender,
|
||||
});
|
||||
|
||||
jest.spyOn(client, "paginateEventTimeline").mockImplementation(async () => {
|
||||
thread.timelineSet.getLiveTimeline().addEvent(threadReply, { toStartOfTimeline: true });
|
||||
return true;
|
||||
});
|
||||
jest.spyOn(client, "relations").mockResolvedValue({
|
||||
events: [],
|
||||
});
|
||||
|
||||
const thread = room.createThread(root.getId()!, root, [threadReply, editToThreadReply], false);
|
||||
thread.once(RoomEvent.TimelineReset, () => {
|
||||
const lastEvent = thread.timeline.at(-1)!;
|
||||
expect(lastEvent.getContent().body).toBe(" * edit");
|
||||
});
|
||||
});
|
||||
|
||||
describe("non-room timeline", () => {
|
||||
it("Adds event to timeline", () => {
|
||||
const nonRoomEventTimelineSet = new EventTimelineSet(
|
||||
|
||||
@@ -18,6 +18,7 @@ import { FetchHttpApi } from "../../../src/http-api/fetch";
|
||||
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
|
||||
import { ClientPrefix, HttpApiEvent, HttpApiEventHandlerMap, IdentityPrefix, IHttpOpts, Method } from "../../../src";
|
||||
import { emitPromise } from "../../test-utils/test-utils";
|
||||
import { QueryDict } from "../../../src/utils";
|
||||
|
||||
describe("FetchHttpApi", () => {
|
||||
const baseUrl = "http://baseUrl";
|
||||
@@ -235,4 +236,58 @@ describe("FetchHttpApi", () => {
|
||||
expect(fetchFn.mock.calls[0][1].headers.Authorization).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUrl()", () => {
|
||||
const localBaseUrl = "http://baseurl";
|
||||
const baseUrlWithTrailingSlash = "http://baseurl/";
|
||||
const makeApi = (thisBaseUrl = baseUrl): FetchHttpApi<any> => {
|
||||
const fetchFn = jest.fn();
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
return new FetchHttpApi(emitter, { baseUrl: thisBaseUrl, prefix, fetchFn });
|
||||
};
|
||||
|
||||
type TestParams = {
|
||||
path: string;
|
||||
queryParams?: QueryDict;
|
||||
prefix?: string;
|
||||
baseUrl?: string;
|
||||
};
|
||||
type TestCase = [TestParams, string];
|
||||
const queryParams: QueryDict = {
|
||||
test1: 99,
|
||||
test2: ["a", "b"],
|
||||
};
|
||||
const testPrefix = "/just/testing";
|
||||
const testUrl = "http://justtesting.com";
|
||||
const testUrlWithTrailingSlash = "http://justtesting.com/";
|
||||
|
||||
const testCases: TestCase[] = [
|
||||
[{ path: "/terms" }, `${localBaseUrl}${prefix}/terms`],
|
||||
[{ path: "/terms", queryParams }, `${localBaseUrl}${prefix}/terms?test1=99&test2=a&test2=b`],
|
||||
[{ path: "/terms", prefix: testPrefix }, `${localBaseUrl}${testPrefix}/terms`],
|
||||
[{ path: "/terms", baseUrl: testUrl }, `${testUrl}${prefix}/terms`],
|
||||
[{ path: "/terms", baseUrl: testUrlWithTrailingSlash }, `${testUrl}${prefix}/terms`],
|
||||
[
|
||||
{ path: "/terms", queryParams, prefix: testPrefix, baseUrl: testUrl },
|
||||
`${testUrl}${testPrefix}/terms?test1=99&test2=a&test2=b`,
|
||||
],
|
||||
];
|
||||
const runTests = (fetchBaseUrl: string) => {
|
||||
it.each<TestCase>(testCases)(
|
||||
"creates url with params %s",
|
||||
({ path, queryParams, prefix, baseUrl }, result) => {
|
||||
const api = makeApi(fetchBaseUrl);
|
||||
|
||||
expect(api.getUrl(path, queryParams, prefix, baseUrl)).toEqual(new URL(result));
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
describe("when fetch.opts.baseUrl does not have a trailing slash", () => {
|
||||
runTests(localBaseUrl);
|
||||
});
|
||||
describe("when fetch.opts.baseUrl does have a trailing slash", () => {
|
||||
runTests(baseUrlWithTrailingSlash);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import DOMException from "domexception";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { ClientPrefix, MatrixHttpApi, Method, UploadResponse } from "../../../src";
|
||||
@@ -33,8 +32,6 @@ describe("MatrixHttpApi", () => {
|
||||
|
||||
const DONE = 0;
|
||||
|
||||
global.DOMException = DOMException;
|
||||
|
||||
beforeEach(() => {
|
||||
xhr = {
|
||||
upload: {} as XMLHttpRequestUpload,
|
||||
|
||||
@@ -517,4 +517,47 @@ describe("InteractiveAuth", () => {
|
||||
expect(ia.getEmailSid()).toEqual(sid);
|
||||
});
|
||||
});
|
||||
|
||||
it("should prioritise shorter flows", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
doRequest: doRequest,
|
||||
stateUpdated: stateUpdated,
|
||||
requestEmailToken: jest.fn(),
|
||||
authData: {
|
||||
session: "sessionId",
|
||||
flows: [{ stages: [AuthType.Recaptcha, AuthType.Password] }, { stages: [AuthType.Password] }],
|
||||
params: {},
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
ia.chooseStage();
|
||||
expect(ia.getChosenFlow()?.stages).toEqual([AuthType.Password]);
|
||||
});
|
||||
|
||||
it("should prioritise flows with entirely supported stages", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
doRequest: doRequest,
|
||||
stateUpdated: stateUpdated,
|
||||
requestEmailToken: jest.fn(),
|
||||
authData: {
|
||||
session: "sessionId",
|
||||
flows: [{ stages: ["com.devture.shared_secret_auth"] }, { stages: [AuthType.Password] }],
|
||||
params: {},
|
||||
},
|
||||
supportedStages: [AuthType.Password],
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
ia.chooseStage();
|
||||
expect(ia.getChosenFlow()?.stages).toEqual([AuthType.Password]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { Mocked, mocked } from "jest-mock";
|
||||
|
||||
import { logger } from "../../src/logger";
|
||||
import { ClientEvent, IMatrixClientCreateOpts, ITurnServerResponse, MatrixClient, Store } from "../../src/client";
|
||||
@@ -50,6 +50,12 @@ import {
|
||||
MatrixScheduler,
|
||||
Method,
|
||||
Room,
|
||||
EventTimelineSet,
|
||||
PushRuleActionName,
|
||||
TweakName,
|
||||
RuleId,
|
||||
IPushRule,
|
||||
ConditionKind,
|
||||
} from "../../src";
|
||||
import { supportsMatrixCall } from "../../src/webrtc/call";
|
||||
import { makeBeaconEvent } from "../test-utils/beacon";
|
||||
@@ -63,6 +69,8 @@ import { QueryDict } from "../../src/utils";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import * as featureUtils from "../../src/feature";
|
||||
import { StubStore } from "../../src/store/stub";
|
||||
import { SecretStorageKeyDescriptionAesV1, ServerSideSecretStorageImpl } from "../../src/secret-storage";
|
||||
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
@@ -725,6 +733,7 @@ describe("MatrixClient", function () {
|
||||
getMyMembership: () => "join",
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({
|
||||
@@ -743,6 +752,7 @@ describe("MatrixClient", function () {
|
||||
} else {
|
||||
throw new Error("Unexpected event type or state key");
|
||||
}
|
||||
/* eslint-enable jest/no-conditional-expect */
|
||||
},
|
||||
} as Room["currentState"],
|
||||
} as unknown as Room;
|
||||
@@ -785,6 +795,7 @@ describe("MatrixClient", function () {
|
||||
getMyMembership: () => "join",
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({
|
||||
@@ -803,6 +814,7 @@ describe("MatrixClient", function () {
|
||||
} else {
|
||||
throw new Error("Unexpected event type or state key");
|
||||
}
|
||||
/* eslint-enable jest/no-conditional-expect */
|
||||
},
|
||||
} as Room["currentState"],
|
||||
} as unknown as Room;
|
||||
@@ -820,6 +832,7 @@ describe("MatrixClient", function () {
|
||||
getMyMembership: () => "join",
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({
|
||||
@@ -837,6 +850,7 @@ describe("MatrixClient", function () {
|
||||
} else {
|
||||
throw new Error("Unexpected event type or state key");
|
||||
}
|
||||
/* eslint-enable jest/no-conditional-expect */
|
||||
},
|
||||
} as Room["currentState"],
|
||||
} as unknown as Room;
|
||||
@@ -858,6 +872,7 @@ describe("MatrixClient", function () {
|
||||
const syncPromise = new Promise<void>((resolve, reject) => {
|
||||
client.on(ClientEvent.Sync, function syncListener(state) {
|
||||
if (state === "SYNCING") {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(httpLookups.length).toEqual(0);
|
||||
client.removeListener(ClientEvent.Sync, syncListener);
|
||||
resolve();
|
||||
@@ -944,6 +959,7 @@ describe("MatrixClient", function () {
|
||||
|
||||
const wasPreparedPromise = new Promise((resolve) => {
|
||||
client.on(ClientEvent.Sync, function syncListener(state) {
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(2);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
@@ -955,6 +971,7 @@ describe("MatrixClient", function () {
|
||||
// unexpected state transition!
|
||||
expect(state).toEqual(null);
|
||||
}
|
||||
/* eslint-enable jest/no-conditional-expect */
|
||||
});
|
||||
});
|
||||
await client.startClient();
|
||||
@@ -976,8 +993,10 @@ describe("MatrixClient", function () {
|
||||
const isSyncingPromise = new Promise((resolve) => {
|
||||
client.on(ClientEvent.Sync, function syncListener(state) {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
expect(httpLookups.length).toEqual(1);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
/* eslint-enable jest/no-conditional-expect */
|
||||
jest.advanceTimersByTime(1);
|
||||
} else if (state === "RECONNECTING" && httpLookups.length > 0) {
|
||||
jest.advanceTimersByTime(10000);
|
||||
@@ -1004,6 +1023,7 @@ describe("MatrixClient", function () {
|
||||
|
||||
const wasPreparedPromise = new Promise((resolve) => {
|
||||
client.on(ClientEvent.Sync, function syncListener(state) {
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(3);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
@@ -1015,6 +1035,7 @@ describe("MatrixClient", function () {
|
||||
// unexpected state transition!
|
||||
expect(state).toEqual(null);
|
||||
}
|
||||
/* eslint-enable jest/no-conditional-expect */
|
||||
});
|
||||
});
|
||||
await client.startClient();
|
||||
@@ -2691,4 +2712,295 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// these wrappers are deprecated, but we need coverage of them to pass the quality gate
|
||||
describe("SecretStorage wrappers", () => {
|
||||
let mockSecretStorage: Mocked<ServerSideSecretStorageImpl>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockSecretStorage = {
|
||||
getDefaultKeyId: jest.fn(),
|
||||
hasKey: jest.fn(),
|
||||
isStored: jest.fn(),
|
||||
} as unknown as Mocked<ServerSideSecretStorageImpl>;
|
||||
client["_secretStorage"] = mockSecretStorage;
|
||||
});
|
||||
|
||||
it("hasSecretStorageKey", async () => {
|
||||
mockSecretStorage.hasKey.mockResolvedValue(false);
|
||||
expect(await client.hasSecretStorageKey("mykey")).toBe(false);
|
||||
expect(mockSecretStorage.hasKey).toHaveBeenCalledWith("mykey");
|
||||
});
|
||||
|
||||
it("isSecretStored", async () => {
|
||||
const mockResult = { key: {} as SecretStorageKeyDescriptionAesV1 };
|
||||
mockSecretStorage.isStored.mockResolvedValue(mockResult);
|
||||
expect(await client.isSecretStored("mysecret")).toBe(mockResult);
|
||||
expect(mockSecretStorage.isStored).toHaveBeenCalledWith("mysecret");
|
||||
});
|
||||
|
||||
it("getDefaultSecretStorageKeyId", async () => {
|
||||
mockSecretStorage.getDefaultKeyId.mockResolvedValue("bzz");
|
||||
expect(await client.getDefaultSecretStorageKeyId()).toEqual("bzz");
|
||||
});
|
||||
|
||||
it("isKeyBackupKeyStored", async () => {
|
||||
mockSecretStorage.isStored.mockResolvedValue(null);
|
||||
expect(await client.isKeyBackupKeyStored()).toBe(null);
|
||||
expect(mockSecretStorage.isStored).toHaveBeenCalledWith("m.megolm_backup.v1");
|
||||
});
|
||||
});
|
||||
|
||||
// these wrappers are deprecated, but we need coverage of them to pass the quality gate
|
||||
describe("Crypto wrappers", () => {
|
||||
describe("exception if no crypto", () => {
|
||||
it("isCrossSigningReady", () => {
|
||||
expect(() => client.isCrossSigningReady()).toThrow("End-to-end encryption disabled");
|
||||
});
|
||||
|
||||
it("bootstrapCrossSigning", () => {
|
||||
expect(() => client.bootstrapCrossSigning({})).toThrow("End-to-end encryption disabled");
|
||||
});
|
||||
|
||||
it("isSecretStorageReady", () => {
|
||||
expect(() => client.isSecretStorageReady()).toThrow("End-to-end encryption disabled");
|
||||
});
|
||||
});
|
||||
|
||||
describe("defer to crypto backend", () => {
|
||||
let mockCryptoBackend: Mocked<CryptoBackend>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockCryptoBackend = {
|
||||
isCrossSigningReady: jest.fn(),
|
||||
bootstrapCrossSigning: jest.fn(),
|
||||
isSecretStorageReady: jest.fn(),
|
||||
stop: jest.fn().mockResolvedValue(undefined),
|
||||
} as unknown as Mocked<CryptoBackend>;
|
||||
client["cryptoBackend"] = mockCryptoBackend;
|
||||
});
|
||||
|
||||
it("isCrossSigningReady", async () => {
|
||||
const testResult = "test";
|
||||
mockCryptoBackend.isCrossSigningReady.mockResolvedValue(testResult as unknown as boolean);
|
||||
expect(await client.isCrossSigningReady()).toBe(testResult);
|
||||
expect(mockCryptoBackend.isCrossSigningReady).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("bootstrapCrossSigning", async () => {
|
||||
const testOpts = {};
|
||||
mockCryptoBackend.bootstrapCrossSigning.mockResolvedValue(undefined);
|
||||
await client.bootstrapCrossSigning(testOpts);
|
||||
expect(mockCryptoBackend.bootstrapCrossSigning).toHaveBeenCalledTimes(1);
|
||||
expect(mockCryptoBackend.bootstrapCrossSigning).toHaveBeenCalledWith(testOpts);
|
||||
});
|
||||
|
||||
it("isSecretStorageReady", async () => {
|
||||
client["cryptoBackend"] = mockCryptoBackend;
|
||||
const testResult = "test";
|
||||
mockCryptoBackend.isSecretStorageReady.mockResolvedValue(testResult as unknown as boolean);
|
||||
expect(await client.isSecretStorageReady()).toBe(testResult);
|
||||
expect(mockCryptoBackend.isSecretStorageReady).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("paginateEventTimeline()", () => {
|
||||
describe("notifications timeline", () => {
|
||||
const unsafeNotification = {
|
||||
actions: ["notify"],
|
||||
room_id: "__proto__",
|
||||
event: testUtils.mkMessage({
|
||||
user: "@villain:server.org",
|
||||
room: "!roomId:server.org",
|
||||
msg: "I am nefarious",
|
||||
}),
|
||||
profile_tag: null,
|
||||
read: true,
|
||||
ts: 12345,
|
||||
};
|
||||
|
||||
const goodNotification = {
|
||||
actions: ["notify"],
|
||||
room_id: "!favouriteRoom:server.org",
|
||||
event: new MatrixEvent({
|
||||
sender: "@bob:server.org",
|
||||
room_id: "!roomId:server.org",
|
||||
type: "m.call.invite",
|
||||
content: {},
|
||||
}),
|
||||
profile_tag: null,
|
||||
read: true,
|
||||
ts: 12345,
|
||||
};
|
||||
|
||||
const highlightNotification = {
|
||||
actions: ["notify", { set_tweak: "highlight", value: true }],
|
||||
room_id: "!roomId:server.org",
|
||||
event: testUtils.mkMessage({
|
||||
user: "@bob:server.org",
|
||||
room: "!roomId:server.org",
|
||||
msg: "I am highlighted banana",
|
||||
}),
|
||||
profile_tag: null,
|
||||
read: true,
|
||||
ts: 12345,
|
||||
};
|
||||
|
||||
const setNotifsResponse = (notifications: any[] = []): void => {
|
||||
const response: HttpLookup = {
|
||||
method: "GET",
|
||||
path: "/notifications",
|
||||
data: { notifications: JSON.parse(JSON.stringify(notifications)) },
|
||||
};
|
||||
httpLookups = [response];
|
||||
};
|
||||
|
||||
const callRule: IPushRule = {
|
||||
actions: [PushRuleActionName.Notify],
|
||||
conditions: [
|
||||
{
|
||||
kind: ConditionKind.EventMatch,
|
||||
key: "type",
|
||||
pattern: "m.call.invite",
|
||||
},
|
||||
],
|
||||
default: true,
|
||||
enabled: true,
|
||||
rule_id: ".m.rule.call",
|
||||
};
|
||||
const masterRule: IPushRule = {
|
||||
actions: [PushRuleActionName.DontNotify],
|
||||
conditions: [],
|
||||
default: true,
|
||||
enabled: false,
|
||||
rule_id: RuleId.Master,
|
||||
};
|
||||
const bananaRule = {
|
||||
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: true }],
|
||||
pattern: "banana",
|
||||
rule_id: "banana",
|
||||
default: false,
|
||||
enabled: true,
|
||||
} as IPushRule;
|
||||
const pushRules = {
|
||||
global: {
|
||||
underride: [callRule],
|
||||
override: [masterRule],
|
||||
content: [bananaRule],
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
makeClient();
|
||||
|
||||
// this is how notif timeline is set up in react-sdk
|
||||
const notifTimelineSet = new EventTimelineSet(undefined, {
|
||||
timelineSupport: true,
|
||||
pendingEvents: false,
|
||||
});
|
||||
notifTimelineSet.getLiveTimeline().setPaginationToken("", EventTimeline.BACKWARDS);
|
||||
client.setNotifTimelineSet(notifTimelineSet);
|
||||
|
||||
setNotifsResponse();
|
||||
|
||||
client.setPushRules(pushRules);
|
||||
});
|
||||
|
||||
it("should throw when trying to paginate forwards", async () => {
|
||||
const timeline = client.getNotifTimelineSet()!.getLiveTimeline();
|
||||
await expect(
|
||||
async () => await client.paginateEventTimeline(timeline, { backwards: false }),
|
||||
).rejects.toThrow("paginateNotifTimeline can only paginate backwards");
|
||||
});
|
||||
|
||||
it("defaults limit to 30 events", async () => {
|
||||
jest.spyOn(client.http, "authedRequest");
|
||||
const timeline = client.getNotifTimelineSet()!.getLiveTimeline();
|
||||
await client.paginateEventTimeline(timeline, { backwards: true });
|
||||
|
||||
expect(client.http.authedRequest).toHaveBeenCalledWith(Method.Get, "/notifications", {
|
||||
limit: "30",
|
||||
only: "highlight",
|
||||
});
|
||||
});
|
||||
|
||||
it("filters out unsafe notifications", async () => {
|
||||
setNotifsResponse([unsafeNotification, goodNotification, highlightNotification]);
|
||||
|
||||
const timelineSet = client.getNotifTimelineSet()!;
|
||||
const timeline = timelineSet.getLiveTimeline();
|
||||
await client.paginateEventTimeline(timeline, { backwards: true });
|
||||
|
||||
// badNotification not added to timeline
|
||||
const timelineEvents = timeline.getEvents();
|
||||
expect(timelineEvents.length).toEqual(2);
|
||||
});
|
||||
|
||||
it("sets push details on events and add to timeline", async () => {
|
||||
setNotifsResponse([goodNotification, highlightNotification]);
|
||||
|
||||
const timelineSet = client.getNotifTimelineSet()!;
|
||||
const timeline = timelineSet.getLiveTimeline();
|
||||
await client.paginateEventTimeline(timeline, { backwards: true });
|
||||
|
||||
const [highlightEvent, goodEvent] = timeline.getEvents();
|
||||
expect(highlightEvent.getPushActions()).toEqual({
|
||||
notify: true,
|
||||
tweaks: {
|
||||
highlight: true,
|
||||
},
|
||||
});
|
||||
expect(highlightEvent.getPushDetails().rule).toEqual({
|
||||
...bananaRule,
|
||||
kind: "content",
|
||||
});
|
||||
expect(goodEvent.getPushActions()).toEqual({
|
||||
notify: true,
|
||||
tweaks: {
|
||||
highlight: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("pushers", () => {
|
||||
const pusher = {
|
||||
app_id: "test",
|
||||
app_display_name: "Test App",
|
||||
data: {},
|
||||
device_display_name: "test device",
|
||||
kind: "http",
|
||||
lang: "en-NZ",
|
||||
pushkey: "1234",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
makeClient();
|
||||
const response: HttpLookup = {
|
||||
method: Method.Post,
|
||||
path: "/pushers/set",
|
||||
data: {},
|
||||
};
|
||||
httpLookups = [response];
|
||||
jest.spyOn(client.http, "authedRequest").mockClear();
|
||||
});
|
||||
|
||||
it("should make correct request to set pusher", async () => {
|
||||
const result = await client.setPusher(pusher);
|
||||
expect(client.http.authedRequest).toHaveBeenCalledWith(Method.Post, "/pushers/set", undefined, pusher);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it("should make correct request to remove pusher", async () => {
|
||||
const result = await client.removePusher(pusher.pushkey, pusher.app_id);
|
||||
expect(client.http.authedRequest).toHaveBeenCalledWith(Method.Post, "/pushers/set", undefined, {
|
||||
pushkey: pusher.pushkey,
|
||||
app_id: pusher.app_id,
|
||||
kind: null,
|
||||
});
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -265,11 +265,13 @@ describe("MSC3089Branch", () => {
|
||||
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
|
||||
expect(content).toMatchObject({
|
||||
retained: true, // canary for copying state
|
||||
active: false,
|
||||
});
|
||||
} else if (stateKey === fileEventId2) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(content).toMatchObject({
|
||||
active: true,
|
||||
version: 2,
|
||||
|
||||
@@ -130,14 +130,8 @@ describe("MSC3089TreeSpace", () => {
|
||||
return Promise.reject(new MatrixError({ errcode: "M_FORBIDDEN", error: "Sample Failure" }));
|
||||
});
|
||||
client.invite = fn;
|
||||
try {
|
||||
await tree.invite(target, false, false);
|
||||
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Failed to fail");
|
||||
} catch (e) {
|
||||
expect((<MatrixError>e).errcode).toEqual("M_FORBIDDEN");
|
||||
}
|
||||
await expect(tree.invite(target, false, false)).rejects.toThrow("MatrixError: Sample Failure");
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -357,13 +351,18 @@ describe("MSC3089TreeSpace", () => {
|
||||
.fn()
|
||||
.mockImplementation(async (roomId: string, eventType: EventType, content: any, stateKey: string) => {
|
||||
expect([tree.roomId, subspaceId]).toContain(roomId);
|
||||
|
||||
let expectedType: string;
|
||||
let expectedStateKey: string;
|
||||
if (roomId === subspaceId) {
|
||||
expect(eventType).toEqual(EventType.SpaceParent);
|
||||
expect(stateKey).toEqual(tree.roomId);
|
||||
expectedType = EventType.SpaceParent;
|
||||
expectedStateKey = tree.roomId;
|
||||
} else {
|
||||
expect(eventType).toEqual(EventType.SpaceChild);
|
||||
expect(stateKey).toEqual(subspaceId);
|
||||
expectedType = EventType.SpaceChild;
|
||||
expectedStateKey = subspaceId;
|
||||
}
|
||||
expect(eventType).toEqual(expectedType);
|
||||
expect(stateKey).toEqual(expectedStateKey);
|
||||
expect(content).toMatchObject({ via: [domain] });
|
||||
|
||||
// return value not used
|
||||
@@ -629,15 +628,8 @@ describe("MSC3089TreeSpace", () => {
|
||||
});
|
||||
|
||||
it("should throw when setting an order at the top level space", async () => {
|
||||
try {
|
||||
// The tree is what we've defined as top level, so it should work
|
||||
await tree.setOrder(2);
|
||||
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Failed to fail");
|
||||
} catch (e) {
|
||||
expect((<Error>e).message).toEqual("Cannot set order of top level spaces currently");
|
||||
}
|
||||
// The tree is what we've defined as top level, so it should work
|
||||
await expect(tree.setOrder(2)).rejects.toThrow("Cannot set order of top level spaces currently");
|
||||
});
|
||||
|
||||
it("should return a stable order for unordered children", () => {
|
||||
|
||||
@@ -17,6 +17,7 @@ limitations under the License.
|
||||
import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event";
|
||||
import { emitPromise } from "../../test-utils/test-utils";
|
||||
import { Crypto, IEventDecryptionResult } from "../../../src/crypto";
|
||||
import { IAnnotatedPushRule, PushRuleActionName, TweakName } from "../../../src";
|
||||
|
||||
describe("MatrixEvent", () => {
|
||||
it("should create copies of itself", () => {
|
||||
@@ -216,4 +217,95 @@ describe("MatrixEvent", () => {
|
||||
expect(encryptedEvent.replyEventId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("push details", () => {
|
||||
const pushRule = {
|
||||
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: true }],
|
||||
pattern: "banana",
|
||||
rule_id: "banana",
|
||||
kind: "override",
|
||||
default: false,
|
||||
enabled: true,
|
||||
} as IAnnotatedPushRule;
|
||||
describe("setPushActions()", () => {
|
||||
it("sets actions on event", () => {
|
||||
const actions = { notify: false, tweaks: {} };
|
||||
const event = new MatrixEvent({
|
||||
type: "com.example.test",
|
||||
content: {
|
||||
isTest: true,
|
||||
},
|
||||
});
|
||||
event.setPushActions(actions);
|
||||
|
||||
expect(event.getPushActions()).toBe(actions);
|
||||
});
|
||||
|
||||
it("sets actions to undefined", () => {
|
||||
const event = new MatrixEvent({
|
||||
type: "com.example.test",
|
||||
content: {
|
||||
isTest: true,
|
||||
},
|
||||
});
|
||||
event.setPushActions(null);
|
||||
|
||||
// undefined is set on state
|
||||
expect(event.getPushDetails().actions).toBe(undefined);
|
||||
// but pushActions getter returns null when falsy
|
||||
expect(event.getPushActions()).toBe(null);
|
||||
});
|
||||
|
||||
it("clears existing push rule", () => {
|
||||
const prevActions = { notify: true, tweaks: { highlight: true } };
|
||||
const actions = { notify: false, tweaks: {} };
|
||||
const event = new MatrixEvent({
|
||||
type: "com.example.test",
|
||||
content: {
|
||||
isTest: true,
|
||||
},
|
||||
});
|
||||
event.setPushDetails(prevActions, pushRule);
|
||||
|
||||
event.setPushActions(actions);
|
||||
|
||||
// rule is not in event push cache
|
||||
expect(event.getPushDetails()).toEqual({ actions });
|
||||
});
|
||||
});
|
||||
|
||||
describe("setPushDetails()", () => {
|
||||
it("sets actions and rule on event", () => {
|
||||
const actions = { notify: false, tweaks: {} };
|
||||
const event = new MatrixEvent({
|
||||
type: "com.example.test",
|
||||
content: {
|
||||
isTest: true,
|
||||
},
|
||||
});
|
||||
event.setPushDetails(actions, pushRule);
|
||||
|
||||
expect(event.getPushDetails()).toEqual({
|
||||
actions,
|
||||
rule: pushRule,
|
||||
});
|
||||
});
|
||||
it("clears existing push rule", () => {
|
||||
const prevActions = { notify: true, tweaks: { highlight: true } };
|
||||
const actions = { notify: false, tweaks: {} };
|
||||
const event = new MatrixEvent({
|
||||
type: "com.example.test",
|
||||
content: {
|
||||
isTest: true,
|
||||
},
|
||||
});
|
||||
event.setPushDetails(prevActions, pushRule);
|
||||
|
||||
event.setPushActions(actions);
|
||||
|
||||
// rule is not in event push cache
|
||||
expect(event.getPushDetails()).toEqual({ actions });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,13 +14,16 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { IEvent, MatrixEvent, PollEvent, Room } from "../../../src";
|
||||
import { M_POLL_START } from "matrix-events-sdk";
|
||||
|
||||
import { EventType, IEvent, MatrixEvent, PollEvent, Room } from "../../../src";
|
||||
import { REFERENCE_RELATION } from "../../../src/@types/extensible_events";
|
||||
import { M_POLL_END, M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE } from "../../../src/@types/polls";
|
||||
import { PollStartEvent } from "../../../src/extensible_events_v1/PollStartEvent";
|
||||
import { Poll } from "../../../src/models/poll";
|
||||
import { isPollEvent, Poll } from "../../../src/models/poll";
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../test-utils/client";
|
||||
import { flushPromises } from "../../test-utils/flushPromises";
|
||||
import { mkEvent } from "../../test-utils/test-utils";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
@@ -453,4 +456,31 @@ describe("Poll", () => {
|
||||
expect(responses.getRelations()).toEqual([responseEvent]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isPollEvent", () => {
|
||||
it("should return »false« for a non-poll event", () => {
|
||||
const messageEvent = mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
content: {},
|
||||
user: mockClient.getSafeUserId(),
|
||||
room: room.roomId,
|
||||
});
|
||||
expect(isPollEvent(messageEvent)).toBe(false);
|
||||
});
|
||||
|
||||
it.each([[M_POLL_START.name], [M_POLL_RESPONSE.name], [M_POLL_END.name]])(
|
||||
"should return »true« for a »%s« event",
|
||||
(type: string) => {
|
||||
const pollEvent = mkEvent({
|
||||
event: true,
|
||||
type,
|
||||
content: {},
|
||||
user: mockClient.getSafeUserId(),
|
||||
room: room.roomId,
|
||||
});
|
||||
expect(isPollEvent(pollEvent)).toBe(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,17 +14,20 @@ 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 } from "../../../src/models/room";
|
||||
import { Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../../src/models/thread";
|
||||
import { mkThread } from "../../test-utils/thread";
|
||||
import { Room, RoomEvent } from "../../../src/models/room";
|
||||
import { Thread, THREAD_RELATION_TYPE, ThreadEvent, FeatureSupport } from "../../../src/models/thread";
|
||||
import { makeThreadEvent, mkThread } from "../../test-utils/thread";
|
||||
import { TestClient } from "../../TestClient";
|
||||
import { emitPromise, mkMessage, mock } from "../../test-utils/test-utils";
|
||||
import { Direction, EventStatus, MatrixEvent } from "../../../src";
|
||||
import { emitPromise, mkEdit, mkMessage, mkReaction, mock } from "../../test-utils/test-utils";
|
||||
import { Direction, EventStatus, EventType, MatrixEvent } from "../../../src";
|
||||
import { ReceiptType } from "../../../src/@types/read_receipts";
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../test-utils/client";
|
||||
import { ReEmitter } from "../../../src/ReEmitter";
|
||||
import { Feature, ServerSupport } from "../../../src/feature";
|
||||
import { eventMapperFor } from "../../../src/event-mapper";
|
||||
|
||||
describe("Thread", () => {
|
||||
describe("constructor", () => {
|
||||
@@ -424,4 +427,301 @@ describe("Thread", () => {
|
||||
expect(mock).toHaveBeenCalledWith("b1", "f1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("insertEventIntoTimeline", () => {
|
||||
it("Inserts a reaction in timestamp order", () => {
|
||||
// Assumption: no server side support because if we have it, events
|
||||
// can only be added to the timeline after the thread has been
|
||||
// initialised, and we are not properly initialising it here.
|
||||
expect(Thread.hasServerSideSupport).toBe(FeatureSupport.None);
|
||||
|
||||
const client = createClientWithEventMapper();
|
||||
const userId = "user1";
|
||||
const room = new Room("room1", client, userId);
|
||||
|
||||
// Given a thread with a root plus 5 messages
|
||||
const { thread, events } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: userId,
|
||||
participantUserIds: ["@bob:hs", "@chia:hs", "@dv:hs"],
|
||||
length: 6,
|
||||
ts: 100, // Events will be at ts 100, 101, 102, 103, 104 and 105
|
||||
});
|
||||
|
||||
// When we insert a reaction to the second thread message
|
||||
const replyEvent = mkReaction(events[2], client, userId, room.roomId, 104);
|
||||
thread.insertEventIntoTimeline(replyEvent);
|
||||
|
||||
// Then the reaction is inserted based on its timestamp
|
||||
expect(thread.events.map((ev) => ev.getId())).toEqual([
|
||||
events[0].getId(),
|
||||
events[1].getId(),
|
||||
events[2].getId(),
|
||||
events[3].getId(),
|
||||
events[4].getId(),
|
||||
replyEvent.getId(),
|
||||
events[5].getId(),
|
||||
]);
|
||||
});
|
||||
|
||||
describe("Without relations recursion support", () => {
|
||||
it("Creates a local echo receipt for new events", async () => {
|
||||
// Assumption: no server side support because if we have it, events
|
||||
// can only be added to the timeline after the thread has been
|
||||
// initialised, and we are not properly initialising it here.
|
||||
expect(Thread.hasServerSideSupport).toBe(FeatureSupport.None);
|
||||
|
||||
// Given a client without relations recursion support
|
||||
const client = createClientWithEventMapper();
|
||||
|
||||
// And a thread with an added event (with later timestamp)
|
||||
const userId = "user1";
|
||||
const { thread, message } = await createThreadAndEvent(client, 1, 100, userId);
|
||||
|
||||
// Then a receipt was added to the thread
|
||||
const receipt = thread.getReadReceiptForUserId(userId);
|
||||
expect(receipt).toBeTruthy();
|
||||
expect(receipt?.eventId).toEqual(message.getId());
|
||||
expect(receipt?.data.ts).toEqual(100);
|
||||
expect(receipt?.data.thread_id).toEqual(thread.id);
|
||||
|
||||
// (And the receipt was synthetic)
|
||||
expect(thread.getReadReceiptForUserId(userId, true)).toBeNull();
|
||||
});
|
||||
|
||||
it("Doesn't create a local echo receipt for events before an existing receipt", async () => {
|
||||
// Assumption: no server side support because if we have it, events
|
||||
// can only be added to the timeline after the thread has been
|
||||
// initialised, and we are not properly initialising it here.
|
||||
expect(Thread.hasServerSideSupport).toBe(FeatureSupport.None);
|
||||
|
||||
// Given a client without relations recursion support
|
||||
const client = createClientWithEventMapper();
|
||||
|
||||
// And a thread with an added event with a lower timestamp than its other events
|
||||
const userId = "user1";
|
||||
const { thread } = await createThreadAndEvent(client, 200, 100, userId);
|
||||
|
||||
// Then no receipt was added to the thread (the receipt is still
|
||||
// for the thread root). This happens because since we have no
|
||||
// recursive relations support, we know that sometimes events
|
||||
// appear out of order, so we have to check their timestamps as
|
||||
// a guess of the correct order.
|
||||
expect(thread.getReadReceiptForUserId(userId)?.eventId).toEqual(thread.rootEvent?.getId());
|
||||
});
|
||||
});
|
||||
|
||||
describe("With relations recursion support", () => {
|
||||
it("Creates a local echo receipt for new events", async () => {
|
||||
// Assumption: no server side support because if we have it, events
|
||||
// can only be added to the timeline after the thread has been
|
||||
// initialised, and we are not properly initialising it here.
|
||||
expect(Thread.hasServerSideSupport).toBe(FeatureSupport.None);
|
||||
|
||||
// Given a client WITH relations recursion support
|
||||
const client = createClientWithEventMapper(
|
||||
new Map([[Feature.RelationsRecursion, ServerSupport.Stable]]),
|
||||
);
|
||||
|
||||
// And a thread with an added event (with later timestamp)
|
||||
const userId = "user1";
|
||||
const { thread, message } = await createThreadAndEvent(client, 1, 100, userId);
|
||||
|
||||
// Then a receipt was added to the thread
|
||||
const receipt = thread.getReadReceiptForUserId(userId);
|
||||
expect(receipt?.eventId).toEqual(message.getId());
|
||||
});
|
||||
|
||||
it("Creates a local echo receipt even for events BEFORE an existing receipt", async () => {
|
||||
// Assumption: no server side support because if we have it, events
|
||||
// can only be added to the timeline after the thread has been
|
||||
// initialised, and we are not properly initialising it here.
|
||||
expect(Thread.hasServerSideSupport).toBe(FeatureSupport.None);
|
||||
|
||||
// Given a client WITH relations recursion support
|
||||
const client = createClientWithEventMapper(
|
||||
new Map([[Feature.RelationsRecursion, ServerSupport.Stable]]),
|
||||
);
|
||||
|
||||
// And a thread with an added event with a lower timestamp than its other events
|
||||
const userId = "user1";
|
||||
const { thread, message } = await createThreadAndEvent(client, 200, 100, userId);
|
||||
|
||||
// Then a receipt was added to the thread, because relations
|
||||
// recursion is available, so we trust the server to have
|
||||
// provided us with events in the right order.
|
||||
const receipt = thread.getReadReceiptForUserId(userId);
|
||||
expect(receipt?.eventId).toEqual(message.getId());
|
||||
});
|
||||
});
|
||||
|
||||
async function createThreadAndEvent(
|
||||
client: MatrixClient,
|
||||
rootTs: number,
|
||||
eventTs: number,
|
||||
userId: string,
|
||||
): Promise<{ thread: Thread; message: MatrixEvent }> {
|
||||
const room = new Room("room1", client, userId);
|
||||
|
||||
// Given a thread
|
||||
const { thread } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: userId,
|
||||
participantUserIds: [],
|
||||
ts: rootTs,
|
||||
});
|
||||
// Sanity: the current receipt is for the thread root
|
||||
expect(thread.getReadReceiptForUserId(userId)?.eventId).toEqual(thread.rootEvent?.getId());
|
||||
|
||||
const awaitTimelineEvent = new Promise<void>((res) => thread.on(RoomEvent.Timeline, () => res()));
|
||||
|
||||
// When we add a message that is before the latest receipt
|
||||
const message = makeThreadEvent({
|
||||
event: true,
|
||||
rootEventId: thread.id,
|
||||
replyToEventId: thread.id,
|
||||
user: userId,
|
||||
room: room.roomId,
|
||||
ts: eventTs,
|
||||
});
|
||||
await thread.addEvent(message, false, true);
|
||||
await awaitTimelineEvent;
|
||||
|
||||
return { thread, message };
|
||||
}
|
||||
|
||||
function createClientWithEventMapper(canSupport: Map<Feature, ServerSupport> = new Map()): MatrixClient {
|
||||
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);
|
||||
return client;
|
||||
}
|
||||
});
|
||||
|
||||
describe("Editing events", () => {
|
||||
describe("Given server support for threads", () => {
|
||||
let previousThreadHasServerSideSupport: FeatureSupport;
|
||||
|
||||
beforeAll(() => {
|
||||
previousThreadHasServerSideSupport = Thread.hasServerSideSupport;
|
||||
Thread.hasServerSideSupport = FeatureSupport.Stable;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
Thread.hasServerSideSupport = previousThreadHasServerSideSupport;
|
||||
});
|
||||
|
||||
it("Adds edits from sync to the thread timeline and applies them", async () => {
|
||||
// Given a thread
|
||||
const client = createClient();
|
||||
const user = "@alice:matrix.org";
|
||||
const room = "!room:z";
|
||||
const thread = await createThread(client, user, room);
|
||||
|
||||
// When a message and an edit are added to the thread
|
||||
const messageToEdit = createThreadMessage(thread.id, user, room, "Thread reply");
|
||||
const editEvent = mkEdit(messageToEdit, client, user, room, "edit");
|
||||
await thread.addEvent(messageToEdit, false);
|
||||
await thread.addEvent(editEvent, false);
|
||||
|
||||
// Then both events end up in the timeline
|
||||
const lastEvent = thread.timeline.at(-1)!;
|
||||
const secondLastEvent = thread.timeline.at(-2)!;
|
||||
expect(lastEvent).toBe(editEvent);
|
||||
expect(secondLastEvent).toBe(messageToEdit);
|
||||
|
||||
// And the first message has been edited
|
||||
expect(secondLastEvent.getContent().body).toEqual("edit");
|
||||
});
|
||||
|
||||
it("Adds edits fetched on demand to the thread timeline and applies them", async () => {
|
||||
// Given we don't support recursive relations
|
||||
const client = createClient(new Map([[Feature.RelationsRecursion, ServerSupport.Unsupported]]));
|
||||
// And we have a thread
|
||||
const user = "@alice:matrix.org";
|
||||
const room = "!room:z";
|
||||
const thread = await createThread(client, user, room);
|
||||
|
||||
// When a message is added to the thread, and an edit to it is provided on demand
|
||||
const messageToEdit = createThreadMessage(thread.id, user, room, "Thread reply");
|
||||
// (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) => {
|
||||
if (eventId === messageToEdit.getId()) {
|
||||
return { events: [editEvent] };
|
||||
} else {
|
||||
return { events: [] };
|
||||
}
|
||||
});
|
||||
await thread.addEvent(messageToEdit, false);
|
||||
|
||||
// Then both events end up in the timeline
|
||||
const lastEvent = thread.timeline.at(-1)!;
|
||||
const secondLastEvent = thread.timeline.at(-2)!;
|
||||
expect(lastEvent).toBe(editEvent);
|
||||
expect(secondLastEvent).toBe(messageToEdit);
|
||||
|
||||
// And the first message has been edited
|
||||
expect(secondLastEvent.getContent().body).toEqual("edit");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a message event that lives in a thread
|
||||
*/
|
||||
function createThreadMessage(threadId: string, user: string, room: string, msg: string): MatrixEvent {
|
||||
return makeThreadEvent({
|
||||
event: true,
|
||||
user,
|
||||
room,
|
||||
msg,
|
||||
rootEventId: threadId,
|
||||
replyToEventId: threadId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a thread and wait for it to be properly initialised (so you can safely
|
||||
* add events to it and expect them to appear in the timeline.
|
||||
*/
|
||||
async function createThread(client: MatrixClient, user: string, roomId: string): Promise<Thread> {
|
||||
const root = mkMessage({ event: true, user, room: roomId, msg: "Thread root" });
|
||||
const room = new Room(roomId, client, "@roomcreator:x");
|
||||
|
||||
// Ensure the root is in the room timeline
|
||||
root.setThreadId(root.getId());
|
||||
await room.addLiveEvents([root]);
|
||||
|
||||
// Create the thread and wait for it to be initialised
|
||||
const thread = room.createThread(root.getId()!, root, [], false);
|
||||
await new Promise<void>((res) => thread.once(RoomEvent.TimelineReset, () => res()));
|
||||
|
||||
return thread;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a MatrixClient that supports threads and has all the methods used when
|
||||
* creating a thread that call out to HTTP endpoints mocked out.
|
||||
*/
|
||||
function createClient(canSupport = new Map()): MatrixClient {
|
||||
const client = mock(MatrixClient, "MatrixClient");
|
||||
client.reEmitter = mock(ReEmitter, "ReEmitter");
|
||||
client.canSupport = canSupport;
|
||||
|
||||
jest.spyOn(client, "supportsThreads").mockReturnValue(true);
|
||||
jest.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({});
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
+105
-87
@@ -1,6 +1,7 @@
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { IActionsObject, PushProcessor } from "../../src/pushprocessor";
|
||||
import { ConditionKind, EventType, IContent, MatrixClient, MatrixEvent, PushRuleActionName } from "../../src";
|
||||
import { ConditionKind, EventType, IContent, MatrixClient, MatrixEvent, PushRuleActionName, RuleId } from "../../src";
|
||||
import { mockClientMethodsUser } from "../test-utils/client";
|
||||
|
||||
describe("NotificationService", function () {
|
||||
const testUserId = "@ali:matrix.org";
|
||||
@@ -45,9 +46,8 @@ describe("NotificationService", function () {
|
||||
},
|
||||
};
|
||||
},
|
||||
credentials: {
|
||||
userId: testUserId,
|
||||
},
|
||||
...mockClientMethodsUser(testUserId),
|
||||
supportsIntentionalMentions: () => true,
|
||||
pushRules: {
|
||||
device: {},
|
||||
global: {
|
||||
@@ -97,51 +97,6 @@ describe("NotificationService", function () {
|
||||
pattern: "foo*bar",
|
||||
rule_id: "foobar",
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "sound",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
pattern: "p[io]ng",
|
||||
rule_id: "pingpong",
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "sound",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
pattern: "I ate [0-9] pies",
|
||||
rule_id: "pies",
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "sound",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
pattern: "b[!ai]ke",
|
||||
rule_id: "bakebike",
|
||||
},
|
||||
],
|
||||
override: [
|
||||
{
|
||||
@@ -289,39 +244,6 @@ describe("NotificationService", function () {
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
// TODO: This is not spec compliant behaviour.
|
||||
//
|
||||
// See https://spec.matrix.org/v1.5/client-server-api/#conditions-1 which
|
||||
// describes pattern should glob:
|
||||
//
|
||||
// 1. * matches 0 or more characters;
|
||||
// 2. ? matches exactly one character
|
||||
it("should bing on character group ([abc]) bing words.", function () {
|
||||
testEvent.event.content!.body = "Ping!";
|
||||
let actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
testEvent.event.content!.body = "Pong!";
|
||||
actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
// TODO: This is not spec compliant behaviour. (See above.)
|
||||
it("should bing on character range ([a-z]) bing words.", function () {
|
||||
testEvent.event.content!.body = "I ate 6 pies";
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
// TODO: This is not spec compliant behaviour. (See above.)
|
||||
it("should bing on character negation ([!a]) bing words.", function () {
|
||||
testEvent.event.content!.body = "boke";
|
||||
let actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
testEvent.event.content!.body = "bake";
|
||||
actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(false);
|
||||
});
|
||||
|
||||
it("should not bing on room server ACL changes", function () {
|
||||
testEvent = utils.mkEvent({
|
||||
type: EventType.RoomServerAcl,
|
||||
@@ -572,11 +494,76 @@ describe("NotificationService", function () {
|
||||
});
|
||||
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
if (expected) {
|
||||
expect(actions?.notify).toBeTruthy();
|
||||
} else {
|
||||
expect(actions?.notify).toBeFalsy();
|
||||
}
|
||||
expect(!!actions?.notify).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test event property contains", () => {
|
||||
it.each([
|
||||
// Simple string matching.
|
||||
{ value: "bar", eventValue: ["bar"], expected: true },
|
||||
// Matches are case-sensitive.
|
||||
{ value: "bar", eventValue: ["BAR"], expected: false },
|
||||
// Values should not be type-coerced.
|
||||
{ value: "bar", eventValue: [true], expected: false },
|
||||
{ value: "bar", eventValue: [1], expected: false },
|
||||
{ value: "bar", eventValue: [false], expected: false },
|
||||
// Boolean matching.
|
||||
{ value: true, eventValue: [true], expected: true },
|
||||
{ value: false, eventValue: [false], expected: true },
|
||||
// Types should not be coerced.
|
||||
{ value: true, eventValue: ["true"], expected: false },
|
||||
{ value: true, eventValue: [1], expected: false },
|
||||
{ value: false, eventValue: [null], expected: false },
|
||||
// Null matching.
|
||||
{ value: null, eventValue: [null], expected: true },
|
||||
// Types should not be coerced
|
||||
{ value: null, eventValue: [false], expected: false },
|
||||
{ value: null, eventValue: [0], expected: false },
|
||||
{ value: null, eventValue: [""], expected: false },
|
||||
{ value: null, eventValue: [undefined], expected: false },
|
||||
// Non-array or empty values should never be matched.
|
||||
{ value: "bar", eventValue: "bar", expected: false },
|
||||
{ value: "bar", eventValue: { bar: true }, expected: false },
|
||||
{ value: true, eventValue: { true: true }, expected: false },
|
||||
{ value: true, eventValue: true, expected: false },
|
||||
{ value: null, eventValue: [], expected: false },
|
||||
{ value: null, eventValue: {}, expected: false },
|
||||
{ value: null, eventValue: null, expected: false },
|
||||
{ value: null, eventValue: undefined, expected: false },
|
||||
])("test $value against $eventValue", ({ value, eventValue, expected }) => {
|
||||
matrixClient.pushRules! = {
|
||||
global: {
|
||||
override: [
|
||||
{
|
||||
actions: [PushRuleActionName.Notify],
|
||||
conditions: [
|
||||
{
|
||||
kind: ConditionKind.EventPropertyContains,
|
||||
key: "content.foo",
|
||||
value: value,
|
||||
},
|
||||
],
|
||||
default: true,
|
||||
enabled: true,
|
||||
rule_id: ".m.rule.test",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
testEvent = utils.mkEvent({
|
||||
type: "m.room.message",
|
||||
room: testRoomId,
|
||||
user: "@alfred:localhost",
|
||||
event: true,
|
||||
content: {
|
||||
foo: eventValue,
|
||||
},
|
||||
});
|
||||
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions?.notify).toBe(expected ? true : undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -647,6 +634,37 @@ describe("NotificationService", function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("test intentional mentions behaviour", () => {
|
||||
it.each([RuleId.ContainsUserName, RuleId.ContainsDisplayName, RuleId.AtRoomNotification])(
|
||||
"Rule %s matches unless intentional mentions are enabled",
|
||||
(ruleId) => {
|
||||
const rule = {
|
||||
rule_id: ruleId,
|
||||
actions: [],
|
||||
conditions: [],
|
||||
default: false,
|
||||
enabled: true,
|
||||
};
|
||||
expect(pushProcessor.ruleMatchesEvent(rule, testEvent)).toBe(true);
|
||||
|
||||
// Add the mentions property to the event and the rule is now disabled.
|
||||
testEvent = utils.mkEvent({
|
||||
type: "m.room.message",
|
||||
room: testRoomId,
|
||||
user: "@alfred:localhost",
|
||||
event: true,
|
||||
content: {
|
||||
"body": "",
|
||||
"msgtype": "m.text",
|
||||
"org.matrix.msc3952.mentions": {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(pushProcessor.ruleMatchesEvent(rule, testEvent)).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test PushProcessor.partsForDottedKey", function () {
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
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 { ReceiptType } from "../../src/@types/read_receipts";
|
||||
import { ReceiptAccumulator } from "../../src/receipt-accumulator";
|
||||
|
||||
const roomId = "!foo:bar";
|
||||
|
||||
describe("ReceiptAccumulator", function () {
|
||||
/*
|
||||
* Note: at the time of writing, the ReceiptAccumulator uses the order of
|
||||
* events from sync as the determinant of which one is most recent. This
|
||||
* is correct, but inconsistent with other areas of the code, since we
|
||||
* don't persist this order, so in other places we are forced to use the
|
||||
* timestamp of events and hope that this matches up.
|
||||
*
|
||||
* The tests in this file provide ts values that are consistent with the
|
||||
* sync order, so they should still pass if we change to using ts to
|
||||
* determine order.
|
||||
*
|
||||
* Ideally, we would keep track of sync order so we can use it everywhere.
|
||||
* This would mean we are consistent with how the homeserver sees receipts
|
||||
* and notifications.
|
||||
*/
|
||||
|
||||
it("Discards previous unthreaded receipts for the same user", () => {
|
||||
const acc = new ReceiptAccumulator();
|
||||
|
||||
const receipt1 = newReceipt("$event1", ReceiptType.Read, "@alice:localhost", 1);
|
||||
const receipt2 = newReceipt("$event2", ReceiptType.Read, "@alice:localhost", 2);
|
||||
|
||||
acc.consumeEphemeralEvents([receipt1, receipt2]);
|
||||
|
||||
const newEvent = acc.buildAccumulatedReceiptEvent(roomId);
|
||||
expect(newEvent).toEqual(newReceipt("$event2", ReceiptType.Read, "@alice:localhost", 2));
|
||||
});
|
||||
|
||||
it("Discards previous threaded receipts for the same user in the same thread", () => {
|
||||
const acc = new ReceiptAccumulator();
|
||||
|
||||
const receipt1 = newReceipt("$event1", ReceiptType.Read, "@alice:localhost", 1, "thread1");
|
||||
const receipt2 = newReceipt("$event2", ReceiptType.Read, "@alice:localhost", 2, "thread1");
|
||||
|
||||
acc.consumeEphemeralEvents([receipt1, receipt2]);
|
||||
|
||||
const newEvent = acc.buildAccumulatedReceiptEvent(roomId);
|
||||
expect(newEvent).toEqual(newReceipt("$event2", ReceiptType.Read, "@alice:localhost", 2, "thread1"));
|
||||
});
|
||||
|
||||
it("Collects multiple receipts for the same user if they are in different threads", () => {
|
||||
const acc = new ReceiptAccumulator();
|
||||
|
||||
const receipt1 = newReceipt("$event1", ReceiptType.Read, "@alice:localhost", 1, "thread1");
|
||||
const receipt2 = newReceipt("$event2", ReceiptType.Read, "@alice:localhost", 2, "thread2");
|
||||
|
||||
acc.consumeEphemeralEvents([receipt1, receipt2]);
|
||||
|
||||
const newEvent = acc.buildAccumulatedReceiptEvent(roomId);
|
||||
expect(newEvent).toEqual(
|
||||
newMultiReceipt([
|
||||
["$event1", ReceiptType.Read, "@alice:localhost", 1, "thread1"],
|
||||
["$event2", ReceiptType.Read, "@alice:localhost", 2, "thread2"],
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("Collects multiple receipts for different users", () => {
|
||||
const acc = new ReceiptAccumulator();
|
||||
|
||||
const receipt1 = newReceipt("$event1", ReceiptType.Read, "@alice:localhost", 1, "thread1");
|
||||
const receipt2 = newReceipt("$event2", ReceiptType.Read, "@bobby:localhost", 2, "thread1");
|
||||
|
||||
acc.consumeEphemeralEvents([receipt1, receipt2]);
|
||||
|
||||
const newEvent = acc.buildAccumulatedReceiptEvent(roomId);
|
||||
expect(newEvent).toEqual(
|
||||
newMultiReceipt([
|
||||
["$event1", ReceiptType.Read, "@alice:localhost", 1, "thread1"],
|
||||
["$event2", ReceiptType.Read, "@bobby:localhost", 2, "thread1"],
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("Collects last receipt for various users and threads", () => {
|
||||
const acc = new ReceiptAccumulator();
|
||||
|
||||
// Below, if ts=1, this receipt is going to be superceded by another
|
||||
// with ts=2
|
||||
const receipts = [
|
||||
newReceipt("$event1", ReceiptType.Read, "@alice:localhost", 1),
|
||||
newReceipt("$event2", ReceiptType.Read, "@bobby:localhost", 1, "thread1"),
|
||||
newReceipt("$event3", ReceiptType.Read, "@alice:localhost", 1, "thread1"),
|
||||
newReceipt("$event4", ReceiptType.Read, "@bobby:localhost", 1),
|
||||
newReceipt("$event5", ReceiptType.Read, "@bobby:localhost", 2),
|
||||
newReceipt("$event6", ReceiptType.Read, "@alice:localhost", 2),
|
||||
newReceipt("$event7", ReceiptType.Read, "@bobby:localhost", 2, "thread1"),
|
||||
newReceipt("$event8", ReceiptType.Read, "@bobby:localhost", 1, "thread2"),
|
||||
newReceipt("$event9", ReceiptType.Read, "@bobby:localhost", 2, "thread2"),
|
||||
newReceipt("$eventA", ReceiptType.Read, "@alice:localhost", 2, "thread1"),
|
||||
newReceipt("$eventB", ReceiptType.Read, "@alice:localhost", 1, "thread2"),
|
||||
newReceipt("$eventC", ReceiptType.Read, "@alice:localhost", 2, "thread2"),
|
||||
];
|
||||
|
||||
acc.consumeEphemeralEvents(receipts);
|
||||
const newEvent = acc.buildAccumulatedReceiptEvent(roomId);
|
||||
|
||||
// Only the ts=2 receipts make it through
|
||||
expect(newEvent).toEqual(
|
||||
newMultiReceipt([
|
||||
["$event5", ReceiptType.Read, "@bobby:localhost", 2, undefined],
|
||||
["$event6", ReceiptType.Read, "@alice:localhost", 2, undefined],
|
||||
["$event7", ReceiptType.Read, "@bobby:localhost", 2, "thread1"],
|
||||
["$event9", ReceiptType.Read, "@bobby:localhost", 2, "thread2"],
|
||||
["$eventA", ReceiptType.Read, "@alice:localhost", 2, "thread1"],
|
||||
["$eventC", ReceiptType.Read, "@alice:localhost", 2, "thread2"],
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("Keeps main thread receipts even when an unthreaded receipt came later", () => {
|
||||
const acc = new ReceiptAccumulator();
|
||||
|
||||
// Given receipts for the special thread "main" and also unthreaded
|
||||
// receipts (which have no thread id).
|
||||
const receipt1 = newReceipt("$event1", ReceiptType.Read, "@alice:localhost", 1, "main");
|
||||
const receipt2 = newReceipt("$event2", ReceiptType.Read, "@alice:localhost", 2);
|
||||
|
||||
// When we collect them
|
||||
acc.consumeEphemeralEvents([receipt1, receipt2]);
|
||||
const newEvent = acc.buildAccumulatedReceiptEvent(roomId);
|
||||
|
||||
// We preserve both: thread:main and unthreaded receipts are different
|
||||
// things, with different meanings.
|
||||
expect(newEvent).toEqual(
|
||||
newMultiReceipt([
|
||||
["$event1", ReceiptType.Read, "@alice:localhost", 1, "main"],
|
||||
["$event2", ReceiptType.Read, "@alice:localhost", 2, undefined],
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("Keeps unthreaded receipts even when a main thread receipt came later", () => {
|
||||
const acc = new ReceiptAccumulator();
|
||||
|
||||
// Given receipts for the special thread "main" and also unthreaded
|
||||
// receipts (which have no thread id).
|
||||
const receipt1 = newReceipt("$event1", ReceiptType.Read, "@alice:localhost", 1);
|
||||
const receipt2 = newReceipt("$event2", ReceiptType.Read, "@alice:localhost", 2, "main");
|
||||
|
||||
// When we collect them
|
||||
acc.consumeEphemeralEvents([receipt1, receipt2]);
|
||||
const newEvent = acc.buildAccumulatedReceiptEvent(roomId);
|
||||
|
||||
// We preserve both: thread:main and unthreaded receipts are different
|
||||
// things, with different meanings.
|
||||
expect(newEvent).toEqual(
|
||||
newMultiReceipt([
|
||||
["$event1", ReceiptType.Read, "@alice:localhost", 1, undefined],
|
||||
["$event2", ReceiptType.Read, "@alice:localhost", 2, "main"],
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const newReceipt = (
|
||||
eventId: string,
|
||||
receiptType: ReceiptType,
|
||||
userId: string,
|
||||
ts: number,
|
||||
threadId: string | undefined = undefined,
|
||||
) => {
|
||||
return {
|
||||
type: "m.receipt",
|
||||
room_id: roomId,
|
||||
content: {
|
||||
[eventId]: {
|
||||
[receiptType]: {
|
||||
[userId]: { ts, thread_id: threadId },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const newMultiReceipt = (infoArray: Array<[string, ReceiptType, string, number, string | undefined]>) => {
|
||||
return {
|
||||
type: "m.receipt",
|
||||
room_id: roomId,
|
||||
content: Object.fromEntries(
|
||||
infoArray.map(([eventId, receiptType, userId, ts, threadId]) => {
|
||||
return [
|
||||
eventId,
|
||||
{
|
||||
[receiptType]: {
|
||||
[userId]: { ts, thread_id: threadId },
|
||||
},
|
||||
},
|
||||
];
|
||||
}),
|
||||
),
|
||||
};
|
||||
};
|
||||
@@ -38,6 +38,7 @@ function makeMockClient(opts: {
|
||||
deviceId: string;
|
||||
deviceKey?: string;
|
||||
msc3882Enabled: boolean;
|
||||
msc3882r0Only: boolean;
|
||||
msc3886Enabled: boolean;
|
||||
devices?: Record<string, Partial<DeviceInfo>>;
|
||||
verificationFunction?: (
|
||||
@@ -58,6 +59,17 @@ function makeMockClient(opts: {
|
||||
},
|
||||
};
|
||||
},
|
||||
getCapabilities() {
|
||||
return opts.msc3882r0Only
|
||||
? {}
|
||||
: {
|
||||
capabilities: {
|
||||
"org.matrix.msc3882.get_login_token": {
|
||||
enabled: opts.msc3882Enabled,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
getUserId() {
|
||||
return opts.userId;
|
||||
},
|
||||
@@ -111,6 +123,7 @@ describe("Rendezvous", function () {
|
||||
deviceId: "DEVICEID",
|
||||
msc3886Enabled: false,
|
||||
msc3882Enabled: true,
|
||||
msc3882r0Only: true,
|
||||
});
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
@@ -166,7 +179,13 @@ describe("Rendezvous", function () {
|
||||
await aliceRz.close();
|
||||
});
|
||||
|
||||
it("no protocols", async function () {
|
||||
async function testNoProtocols({
|
||||
msc3882Enabled,
|
||||
msc3882r0Only,
|
||||
}: {
|
||||
msc3882Enabled: boolean;
|
||||
msc3882r0Only: boolean;
|
||||
}) {
|
||||
const aliceTransport = makeTransport("Alice");
|
||||
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
@@ -178,8 +197,9 @@ describe("Rendezvous", function () {
|
||||
const alice = makeMockClient({
|
||||
userId: "alice",
|
||||
deviceId: "ALICE",
|
||||
msc3882Enabled: false,
|
||||
msc3886Enabled: false,
|
||||
msc3882Enabled,
|
||||
msc3882r0Only,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
@@ -218,6 +238,14 @@ describe("Rendezvous", function () {
|
||||
|
||||
await aliceStartProm;
|
||||
await bobStartPromise;
|
||||
}
|
||||
|
||||
it("no protocols - r0", async function () {
|
||||
await testNoProtocols({ msc3882Enabled: false, msc3882r0Only: true });
|
||||
});
|
||||
|
||||
it("no protocols - r1", async function () {
|
||||
await testNoProtocols({ msc3882Enabled: false, msc3882r0Only: false });
|
||||
});
|
||||
|
||||
it("new device declines protocol with outcome unsupported", async function () {
|
||||
@@ -233,6 +261,7 @@ describe("Rendezvous", function () {
|
||||
userId: "alice",
|
||||
deviceId: "ALICE",
|
||||
msc3882Enabled: true,
|
||||
msc3882r0Only: false,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
@@ -291,6 +320,7 @@ describe("Rendezvous", function () {
|
||||
userId: "alice",
|
||||
deviceId: "ALICE",
|
||||
msc3882Enabled: true,
|
||||
msc3882r0Only: false,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
@@ -349,6 +379,7 @@ describe("Rendezvous", function () {
|
||||
userId: "alice",
|
||||
deviceId: "ALICE",
|
||||
msc3882Enabled: true,
|
||||
msc3882r0Only: false,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
@@ -409,6 +440,7 @@ describe("Rendezvous", function () {
|
||||
userId: "alice",
|
||||
deviceId: "ALICE",
|
||||
msc3882Enabled: true,
|
||||
msc3882r0Only: false,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
@@ -477,6 +509,7 @@ describe("Rendezvous", function () {
|
||||
userId: "alice",
|
||||
deviceId: "ALICE",
|
||||
msc3882Enabled: true,
|
||||
msc3882r0Only: false,
|
||||
msc3886Enabled: false,
|
||||
devices,
|
||||
deviceKey: "aaaa",
|
||||
|
||||
@@ -25,6 +25,8 @@ import { EventType, RelationType, UNSTABLE_MSC2716_MARKER } from "../../src/@typ
|
||||
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { M_BEACON } from "../../src/@types/beacon";
|
||||
import { MatrixClient } from "../../src/client";
|
||||
import { DecryptionError } from "../../src/crypto/algorithms";
|
||||
import { defer } from "../../src/utils";
|
||||
|
||||
describe("RoomState", function () {
|
||||
const roomId = "!foo:bar";
|
||||
@@ -886,7 +888,7 @@ describe("RoomState", function () {
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("adds locations to beacons", () => {
|
||||
it("adds locations to beacons", async () => {
|
||||
const location1 = makeBeaconEvent(userA, {
|
||||
beaconInfoId: "$beacon1",
|
||||
timestamp: Date.now() + 1,
|
||||
@@ -906,7 +908,7 @@ describe("RoomState", function () {
|
||||
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
|
||||
const addLocationsSpy = jest.spyOn(beaconInstance, "addLocations");
|
||||
|
||||
state.processBeaconEvents([location1, location2, location3], mockClient);
|
||||
await state.processBeaconEvents([location1, location2, location3], mockClient);
|
||||
|
||||
expect(addLocationsSpy).toHaveBeenCalledTimes(2);
|
||||
// only called with locations for beacon1
|
||||
@@ -978,51 +980,50 @@ describe("RoomState", function () {
|
||||
expect(mockClient.decryptEventIfNeeded).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("decrypts related events if needed", () => {
|
||||
it("decrypts related events if needed", async () => {
|
||||
const location = makeBeaconEvent(userA, {
|
||||
beaconInfoId: beacon1.getId(),
|
||||
});
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
state.processBeaconEvents([location, relatedEncryptedEvent], mockClient);
|
||||
await state.processBeaconEvents([location, relatedEncryptedEvent], mockClient);
|
||||
// discard unrelated events early
|
||||
expect(mockClient.decryptEventIfNeeded).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("listens for decryption on events that are being decrypted", () => {
|
||||
it("awaits for decryption on events that are being decrypted", async () => {
|
||||
const decryptingRelatedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
content: beacon1RelationContent,
|
||||
});
|
||||
jest.spyOn(decryptingRelatedEvent, "isBeingDecrypted").mockReturnValue(true);
|
||||
// spy on event.once
|
||||
const eventOnceSpy = jest.spyOn(decryptingRelatedEvent, "once");
|
||||
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||
await state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||
|
||||
// listener was added
|
||||
expect(eventOnceSpy).toHaveBeenCalled();
|
||||
expect(mockClient.decryptEventIfNeeded).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("listens for decryption on events that have decryption failure", () => {
|
||||
it("listens for decryption on events that have decryption failure", async () => {
|
||||
const failedDecryptionRelatedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
content: beacon1RelationContent,
|
||||
});
|
||||
jest.spyOn(failedDecryptionRelatedEvent, "isDecryptionFailure").mockReturnValue(true);
|
||||
mockClient.decryptEventIfNeeded.mockRejectedValue(new DecryptionError("ERR", "msg"));
|
||||
// spy on event.once
|
||||
const eventOnceSpy = jest.spyOn(decryptingRelatedEvent, "once");
|
||||
const eventOnceSpy = jest.spyOn(failedDecryptionRelatedEvent, "once");
|
||||
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||
await state.processBeaconEvents([failedDecryptionRelatedEvent], mockClient);
|
||||
|
||||
// listener was added
|
||||
expect(eventOnceSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("discard events that are not m.beacon type after decryption", () => {
|
||||
it("discard events that are not m.beacon type after decryption", async () => {
|
||||
const decryptingRelatedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
@@ -1032,7 +1033,7 @@ describe("RoomState", function () {
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
|
||||
const addLocationsSpy = jest.spyOn(beacon, "addLocations").mockClear();
|
||||
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||
await state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||
|
||||
// this event is a message after decryption
|
||||
decryptingRelatedEvent.event.type = EventType.RoomMessage;
|
||||
@@ -1041,7 +1042,7 @@ describe("RoomState", function () {
|
||||
expect(addLocationsSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("adds locations to beacons after decryption", () => {
|
||||
it("adds locations to beacons after decryption", async () => {
|
||||
const decryptingRelatedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
@@ -1051,16 +1052,20 @@ describe("RoomState", function () {
|
||||
beaconInfoId: "$beacon1",
|
||||
timestamp: Date.now() + 1,
|
||||
});
|
||||
jest.spyOn(decryptingRelatedEvent, "isBeingDecrypted").mockReturnValue(true);
|
||||
|
||||
const deferred = defer<void>();
|
||||
mockClient.decryptEventIfNeeded.mockReturnValue(deferred.promise);
|
||||
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
|
||||
const addLocationsSpy = jest.spyOn(beacon, "addLocations").mockClear();
|
||||
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||
const prom = state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||
|
||||
// update type after '''decryption'''
|
||||
decryptingRelatedEvent.event.type = M_BEACON.name;
|
||||
decryptingRelatedEvent.event.content = locationEvent.event.content;
|
||||
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted, decryptingRelatedEvent);
|
||||
deferred.resolve();
|
||||
await prom;
|
||||
|
||||
expect(addLocationsSpy).toHaveBeenCalledWith([decryptingRelatedEvent]);
|
||||
});
|
||||
|
||||
+422
-250
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
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 { Mocked } from "jest-mock";
|
||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
|
||||
|
||||
import { CrossSigningIdentity } from "../../../src/rust-crypto/CrossSigningIdentity";
|
||||
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||
|
||||
describe("CrossSigningIdentity", () => {
|
||||
describe("bootstrapCrossSigning", () => {
|
||||
/** the CrossSigningIdentity implementation under test */
|
||||
let crossSigning: CrossSigningIdentity;
|
||||
|
||||
/** a mocked-up OlmMachine which crossSigning is connected to */
|
||||
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
|
||||
/** A mock OutgoingRequestProcessor which crossSigning is connected to */
|
||||
let outgoingRequestProcessor: Mocked<OutgoingRequestProcessor>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await RustSdkCryptoJs.initAsync();
|
||||
|
||||
olmMachine = {
|
||||
crossSigningStatus: jest.fn(),
|
||||
bootstrapCrossSigning: jest.fn(),
|
||||
close: jest.fn(),
|
||||
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
|
||||
outgoingRequestProcessor = {
|
||||
makeOutgoingRequest: jest.fn(),
|
||||
} as unknown as Mocked<OutgoingRequestProcessor>;
|
||||
|
||||
crossSigning = new CrossSigningIdentity(olmMachine, outgoingRequestProcessor);
|
||||
});
|
||||
|
||||
it("should do nothing if keys are present on-device and in secret storage", async () => {
|
||||
olmMachine.crossSigningStatus.mockResolvedValue({
|
||||
hasMaster: true,
|
||||
hasSelfSigning: true,
|
||||
hasUserSigning: true,
|
||||
});
|
||||
// TODO: secret storage
|
||||
await crossSigning.bootstrapCrossSigning({});
|
||||
expect(olmMachine.bootstrapCrossSigning).not.toHaveBeenCalled();
|
||||
expect(outgoingRequestProcessor.makeOutgoingRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call bootstrapCrossSigning if a reset is forced", async () => {
|
||||
olmMachine.bootstrapCrossSigning.mockResolvedValue([]);
|
||||
await crossSigning.bootstrapCrossSigning({ setupNewCrossSigning: true });
|
||||
expect(olmMachine.bootstrapCrossSigning).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("should call bootstrapCrossSigning if we need new keys", async () => {
|
||||
olmMachine.crossSigningStatus.mockResolvedValue({
|
||||
hasMaster: false,
|
||||
hasSelfSigning: false,
|
||||
hasUserSigning: false,
|
||||
});
|
||||
olmMachine.bootstrapCrossSigning.mockResolvedValue([]);
|
||||
await crossSigning.bootstrapCrossSigning({});
|
||||
expect(olmMachine.bootstrapCrossSigning).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -24,11 +24,12 @@ import {
|
||||
KeysUploadRequest,
|
||||
RoomMessageRequest,
|
||||
SignatureUploadRequest,
|
||||
SigningKeysUploadRequest,
|
||||
ToDeviceRequest,
|
||||
} from "@matrix-org/matrix-sdk-crypto-js";
|
||||
|
||||
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
|
||||
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi } from "../../../src";
|
||||
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi, UIAuthCallback } from "../../../src";
|
||||
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||
|
||||
describe("OutgoingRequestProcessor", () => {
|
||||
@@ -80,6 +81,12 @@ describe("OutgoingRequestProcessor", () => {
|
||||
"https://example.com/_matrix/client/v3/keys/signatures/upload",
|
||||
],
|
||||
["KeysBackupRequest", KeysBackupRequest, "PUT", "https://example.com/_matrix/client/v3/room_keys/keys"],
|
||||
[
|
||||
"SigningKeysUploadRequest",
|
||||
SigningKeysUploadRequest,
|
||||
"POST",
|
||||
"https://example.com/_matrix/client/v3/keys/device_signing/upload",
|
||||
],
|
||||
];
|
||||
|
||||
test.each(tests)(`should handle %ss`, async (_, RequestClass, expectedMethod, expectedPath) => {
|
||||
@@ -171,6 +178,40 @@ describe("OutgoingRequestProcessor", () => {
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
});
|
||||
|
||||
it("should handle SigningKeysUploadRequests with UIA", async () => {
|
||||
// first, mock up a request as we might expect to receive it from the Rust layer ...
|
||||
const testReq = { foo: "bar" };
|
||||
const outgoingRequest = new SigningKeysUploadRequest("1234", JSON.stringify(testReq));
|
||||
|
||||
// also create a UIA callback
|
||||
const authCallback: UIAuthCallback<Object> = async (makeRequest) => {
|
||||
return await makeRequest({ type: "test" });
|
||||
};
|
||||
|
||||
// ... then poke the request into the OutgoingRequestProcessor under test
|
||||
const reqProm = processor.makeOutgoingRequest(outgoingRequest, authCallback);
|
||||
|
||||
// Now: check that it makes a matching HTTP request ...
|
||||
const testResponse = '{"result":1}';
|
||||
httpBackend
|
||||
.when("POST", "/_matrix")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual("https://example.com/_matrix/client/v3/keys/device_signing/upload");
|
||||
expect(JSON.parse(req.rawData)).toEqual({ foo: "bar", auth: { type: "test" } });
|
||||
expect(req.headers["Accept"]).toEqual("application/json");
|
||||
expect(req.headers["Content-Type"]).toEqual("application/json");
|
||||
})
|
||||
.respond(200, testResponse, true);
|
||||
|
||||
// ... and that it calls OlmMachine.markAsSent.
|
||||
const markSentCallPromise = awaitCallToMarkAsSent();
|
||||
await httpBackend.flushAllExpected();
|
||||
|
||||
await Promise.all([reqProm, markSentCallPromise]);
|
||||
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", outgoingRequest.type, testResponse);
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
});
|
||||
|
||||
it("does not explode with unknown requests", async () => {
|
||||
const outgoingRequest = { id: "5678", type: 987 };
|
||||
const markSentCallPromise = awaitCallToMarkAsSent();
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
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 { DeviceKeys, DeviceVerification } from "../../../src";
|
||||
import { downloadDeviceToJsDevice } from "../../../src/rust-crypto/device-converter";
|
||||
|
||||
describe("device-converter", () => {
|
||||
const userId = "@alice:example.com";
|
||||
const deviceId = "xcvf";
|
||||
|
||||
// All parameters for QueryDevice initialization
|
||||
const keys = {
|
||||
[`ed25519:${deviceId}`]: "key1",
|
||||
[`curve25519:${deviceId}`]: "key2",
|
||||
};
|
||||
const algorithms = ["algo1", "algo2"];
|
||||
const signatures = { [userId]: { [deviceId]: "sign1" } };
|
||||
const displayName = "display name";
|
||||
const unsigned = {
|
||||
device_display_name: displayName,
|
||||
};
|
||||
|
||||
describe("downloadDeviceToJsDevice", () => {
|
||||
it("should convert a QueryDevice to a Device", () => {
|
||||
const queryDevice: DeviceKeys[keyof DeviceKeys] = {
|
||||
keys,
|
||||
algorithms,
|
||||
device_id: deviceId,
|
||||
user_id: userId,
|
||||
signatures,
|
||||
unsigned,
|
||||
};
|
||||
const device = downloadDeviceToJsDevice(queryDevice);
|
||||
|
||||
expect(device.deviceId).toBe(deviceId);
|
||||
expect(device.userId).toBe(userId);
|
||||
expect(device.verified).toBe(DeviceVerification.Unverified);
|
||||
expect(device.getIdentityKey()).toBe(keys[`curve25519:${deviceId}`]);
|
||||
expect(device.getFingerprint()).toBe(keys[`ed25519:${deviceId}`]);
|
||||
expect(device.displayName).toBe(displayName);
|
||||
});
|
||||
|
||||
it("should add empty signatures", () => {
|
||||
const queryDevice: DeviceKeys[keyof DeviceKeys] = {
|
||||
keys,
|
||||
algorithms,
|
||||
device_id: deviceId,
|
||||
user_id: userId,
|
||||
};
|
||||
const device = downloadDeviceToJsDevice(queryDevice);
|
||||
|
||||
expect(device.signatures.size).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022-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.
|
||||
@@ -22,11 +22,12 @@ import { Mocked } from "jest-mock";
|
||||
|
||||
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
|
||||
import { initRustCrypto } from "../../../src/rust-crypto";
|
||||
import { IToDeviceEvent, MatrixClient, MatrixHttpApi } from "../../../src";
|
||||
import { IHttpOpts, IToDeviceEvent, MatrixClient, MatrixHttpApi } from "../../../src";
|
||||
import { mkEvent } from "../../test-utils/test-utils";
|
||||
import { CryptoBackend } from "../../../src/common-crypto/CryptoBackend";
|
||||
import { IEventDecryptionResult } from "../../../src/@types/crypto";
|
||||
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||
import { ServerSideSecretStorage } from "../../../src/secret-storage";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
@@ -35,16 +36,15 @@ afterEach(() => {
|
||||
indexedDB = new IDBFactory();
|
||||
});
|
||||
|
||||
describe("RustCrypto", () => {
|
||||
const TEST_USER = "@alice:example.com";
|
||||
const TEST_DEVICE_ID = "TEST_DEVICE";
|
||||
const TEST_USER = "@alice:example.com";
|
||||
const TEST_DEVICE_ID = "TEST_DEVICE";
|
||||
|
||||
describe("RustCrypto", () => {
|
||||
describe(".exportRoomKeys", () => {
|
||||
let rustCrypto: RustCrypto;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockHttpApi = {} as MatrixClient["http"];
|
||||
rustCrypto = (await initRustCrypto(mockHttpApi, TEST_USER, TEST_DEVICE_ID)) as RustCrypto;
|
||||
rustCrypto = await makeTestRustCrypto();
|
||||
});
|
||||
|
||||
it("should return a list", async () => {
|
||||
@@ -53,12 +53,11 @@ describe("RustCrypto", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("to-device messages", () => {
|
||||
describe("call preprocess methods", () => {
|
||||
let rustCrypto: RustCrypto;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockHttpApi = {} as MatrixClient["http"];
|
||||
rustCrypto = (await initRustCrypto(mockHttpApi, TEST_USER, TEST_DEVICE_ID)) as RustCrypto;
|
||||
rustCrypto = await makeTestRustCrypto();
|
||||
});
|
||||
|
||||
it("should pass through unencrypted to-device messages", async () => {
|
||||
@@ -94,6 +93,32 @@ describe("RustCrypto", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("isCrossSigningReady", async () => {
|
||||
const rustCrypto = await makeTestRustCrypto();
|
||||
await expect(rustCrypto.isCrossSigningReady()).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("getCrossSigningKeyId", async () => {
|
||||
const rustCrypto = await makeTestRustCrypto();
|
||||
await expect(rustCrypto.getCrossSigningKeyId()).resolves.toBe(null);
|
||||
});
|
||||
|
||||
it("bootstrapCrossSigning delegates to CrossSigningIdentity", async () => {
|
||||
const rustCrypto = await makeTestRustCrypto();
|
||||
const mockCrossSigningIdentity = {
|
||||
bootstrapCrossSigning: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
// @ts-ignore private property
|
||||
rustCrypto.crossSigningIdentity = mockCrossSigningIdentity;
|
||||
await rustCrypto.bootstrapCrossSigning({});
|
||||
expect(mockCrossSigningIdentity.bootstrapCrossSigning).toHaveBeenCalledWith({});
|
||||
});
|
||||
|
||||
it("isSecretStorageReady", async () => {
|
||||
const rustCrypto = await makeTestRustCrypto();
|
||||
await expect(rustCrypto.isSecretStorageReady()).resolves.toBe(false);
|
||||
});
|
||||
|
||||
describe("outgoing requests", () => {
|
||||
/** the RustCrypto implementation under test */
|
||||
let rustCrypto: RustCrypto;
|
||||
@@ -141,7 +166,13 @@ describe("RustCrypto", () => {
|
||||
makeOutgoingRequest: jest.fn(),
|
||||
} as unknown as Mocked<OutgoingRequestProcessor>;
|
||||
|
||||
rustCrypto = new RustCrypto(olmMachine, {} as MatrixHttpApi<any>, TEST_USER, TEST_DEVICE_ID);
|
||||
rustCrypto = new RustCrypto(
|
||||
olmMachine,
|
||||
{} as MatrixHttpApi<any>,
|
||||
TEST_USER,
|
||||
TEST_DEVICE_ID,
|
||||
{} as ServerSideSecretStorage,
|
||||
);
|
||||
rustCrypto["outgoingRequestProcessor"] = outgoingRequestProcessor;
|
||||
});
|
||||
|
||||
@@ -206,8 +237,7 @@ describe("RustCrypto", () => {
|
||||
let rustCrypto: RustCrypto;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockHttpApi = {} as MatrixClient["http"];
|
||||
rustCrypto = (await initRustCrypto(mockHttpApi, TEST_USER, TEST_DEVICE_ID)) as RustCrypto;
|
||||
rustCrypto = await makeTestRustCrypto();
|
||||
});
|
||||
|
||||
it("should handle unencrypted events", () => {
|
||||
@@ -230,4 +260,74 @@ describe("RustCrypto", () => {
|
||||
expect(res.encrypted).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("get|setTrustCrossSignedDevices", () => {
|
||||
let rustCrypto: RustCrypto;
|
||||
|
||||
beforeEach(async () => {
|
||||
rustCrypto = await makeTestRustCrypto();
|
||||
});
|
||||
|
||||
it("should be true by default", () => {
|
||||
expect(rustCrypto.getTrustCrossSignedDevices()).toBe(true);
|
||||
});
|
||||
|
||||
it("should be easily turn-off-and-on-able", () => {
|
||||
rustCrypto.setTrustCrossSignedDevices(false);
|
||||
expect(rustCrypto.getTrustCrossSignedDevices()).toBe(false);
|
||||
rustCrypto.setTrustCrossSignedDevices(true);
|
||||
expect(rustCrypto.getTrustCrossSignedDevices()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDeviceVerificationStatus", () => {
|
||||
let rustCrypto: RustCrypto;
|
||||
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
|
||||
beforeEach(() => {
|
||||
olmMachine = {
|
||||
getDevice: jest.fn(),
|
||||
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
rustCrypto = new RustCrypto(
|
||||
olmMachine,
|
||||
{} as MatrixClient["http"],
|
||||
TEST_USER,
|
||||
TEST_DEVICE_ID,
|
||||
{} as ServerSideSecretStorage,
|
||||
);
|
||||
});
|
||||
|
||||
it("should call getDevice", async () => {
|
||||
olmMachine.getDevice.mockResolvedValue({
|
||||
isCrossSigningTrusted: jest.fn().mockReturnValue(false),
|
||||
isLocallyTrusted: jest.fn().mockReturnValue(false),
|
||||
isCrossSignedByOwner: jest.fn().mockReturnValue(false),
|
||||
} as unknown as RustSdkCryptoJs.Device);
|
||||
const res = await rustCrypto.getDeviceVerificationStatus("@user:domain", "device");
|
||||
expect(olmMachine.getDevice.mock.calls[0][0].toString()).toEqual("@user:domain");
|
||||
expect(olmMachine.getDevice.mock.calls[0][1].toString()).toEqual("device");
|
||||
expect(res?.crossSigningVerified).toBe(false);
|
||||
expect(res?.localVerified).toBe(false);
|
||||
expect(res?.signedByOwner).toBe(false);
|
||||
});
|
||||
|
||||
it("should return null for unknown device", async () => {
|
||||
olmMachine.getDevice.mockResolvedValue(undefined);
|
||||
const res = await rustCrypto.getDeviceVerificationStatus("@user:domain", "device");
|
||||
expect(res).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/** build a basic RustCrypto instance for testing
|
||||
*
|
||||
* just provides default arguments for initRustCrypto()
|
||||
*/
|
||||
async function makeTestRustCrypto(
|
||||
http: MatrixHttpApi<IHttpOpts & { onlyData: true }> = {} as MatrixClient["http"],
|
||||
userId: string = TEST_USER,
|
||||
deviceId: string = TEST_DEVICE_ID,
|
||||
secretStorage: ServerSideSecretStorage = {} as ServerSideSecretStorage,
|
||||
): Promise<RustCrypto> {
|
||||
return await initRustCrypto(http, userId, deviceId, secretStorage);
|
||||
}
|
||||
|
||||
@@ -58,10 +58,12 @@ describe("MatrixScheduler", function () {
|
||||
let yieldedA = false;
|
||||
scheduler.setProcessFunction(function (event) {
|
||||
if (yieldedA) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(event).toEqual(eventB);
|
||||
return deferB.promise;
|
||||
} else {
|
||||
yieldedA = true;
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(event).toEqual(eventA);
|
||||
return deferA.promise;
|
||||
}
|
||||
@@ -89,6 +91,7 @@ describe("MatrixScheduler", function () {
|
||||
scheduler.setProcessFunction(function (ev) {
|
||||
procCount += 1;
|
||||
if (procCount === 1) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(ev).toEqual(eventA);
|
||||
return deferred.promise;
|
||||
} else if (procCount === 2) {
|
||||
@@ -129,9 +132,11 @@ describe("MatrixScheduler", function () {
|
||||
scheduler.setProcessFunction(function (ev) {
|
||||
procCount += 1;
|
||||
if (procCount === 1) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(ev).toEqual(eventA);
|
||||
return deferA.promise;
|
||||
} else if (procCount === 2) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(ev).toEqual(eventB);
|
||||
return deferB.promise;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
Copyright 2019, 2022-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 { Mocked } from "jest-mock";
|
||||
|
||||
import {
|
||||
AccountDataClient,
|
||||
PassphraseInfo,
|
||||
SecretStorageCallbacks,
|
||||
SecretStorageKeyDescriptionAesV1,
|
||||
SecretStorageKeyDescriptionCommon,
|
||||
ServerSideSecretStorageImpl,
|
||||
trimTrailingEquals,
|
||||
} from "../../src/secret-storage";
|
||||
import { calculateKeyCheck } from "../../src/crypto/aes";
|
||||
import { randomString } from "../../src/randomstring";
|
||||
|
||||
describe("ServerSideSecretStorageImpl", function () {
|
||||
describe(".addKey", function () {
|
||||
it("should allow storing a default key", async function () {
|
||||
const accountDataAdapter = mockAccountDataClient();
|
||||
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
|
||||
const result = await secretStorage.addKey("m.secret_storage.v1.aes-hmac-sha2");
|
||||
|
||||
// it should have made up a 32-character key id
|
||||
expect(result.keyId.length).toEqual(32);
|
||||
expect(accountDataAdapter.setAccountData).toHaveBeenCalledWith(
|
||||
`m.secret_storage.key.${result.keyId}`,
|
||||
result.keyInfo,
|
||||
);
|
||||
});
|
||||
|
||||
it("should allow storing a key with an explicit id", async function () {
|
||||
const accountDataAdapter = mockAccountDataClient();
|
||||
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
|
||||
const result = await secretStorage.addKey("m.secret_storage.v1.aes-hmac-sha2", {}, "myKeyId");
|
||||
|
||||
// it should have made up a 32-character key id
|
||||
expect(result.keyId).toEqual("myKeyId");
|
||||
expect(accountDataAdapter.setAccountData).toHaveBeenCalledWith(
|
||||
"m.secret_storage.key.myKeyId",
|
||||
result.keyInfo,
|
||||
);
|
||||
});
|
||||
|
||||
it("should allow storing a key with a name", async function () {
|
||||
const accountDataAdapter = mockAccountDataClient();
|
||||
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
|
||||
const result = await secretStorage.addKey("m.secret_storage.v1.aes-hmac-sha2", { name: "mykey" });
|
||||
|
||||
expect(result.keyInfo.name).toEqual("mykey");
|
||||
|
||||
expect(accountDataAdapter.setAccountData).toHaveBeenCalledWith(
|
||||
`m.secret_storage.key.${result.keyId}`,
|
||||
result.keyInfo,
|
||||
);
|
||||
});
|
||||
|
||||
it("should allow storing a key with a passphrase", async function () {
|
||||
const accountDataAdapter = mockAccountDataClient();
|
||||
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
|
||||
const passphrase: PassphraseInfo = {
|
||||
algorithm: "m.pbkdf2",
|
||||
iterations: 125,
|
||||
salt: "saltygoodness",
|
||||
bits: 256,
|
||||
};
|
||||
const result = await secretStorage.addKey("m.secret_storage.v1.aes-hmac-sha2", {
|
||||
passphrase,
|
||||
});
|
||||
|
||||
expect(result.keyInfo.passphrase).toEqual(passphrase);
|
||||
|
||||
expect(accountDataAdapter.setAccountData).toHaveBeenCalledWith(
|
||||
`m.secret_storage.key.${result.keyId}`,
|
||||
result.keyInfo,
|
||||
);
|
||||
});
|
||||
|
||||
it("should complain about invalid algorithm", async function () {
|
||||
const accountDataAdapter = mockAccountDataClient();
|
||||
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
|
||||
await expect(() => secretStorage.addKey("bad_alg")).rejects.toThrow("Unknown key algorithm");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getKey", function () {
|
||||
it("should return the specified key", async function () {
|
||||
const accountDataAdapter = mockAccountDataClient();
|
||||
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
|
||||
|
||||
const storedKey = { iv: "iv", mac: "mac" } as SecretStorageKeyDescriptionAesV1;
|
||||
async function mockGetAccountData<T extends Record<string, any>>(eventType: string): Promise<T | null> {
|
||||
if (eventType === "m.secret_storage.key.my_key") {
|
||||
return storedKey as unknown as T;
|
||||
} else {
|
||||
throw new Error(`unexpected eventType ${eventType}`);
|
||||
}
|
||||
}
|
||||
accountDataAdapter.getAccountDataFromServer.mockImplementation(mockGetAccountData);
|
||||
|
||||
const result = await secretStorage.getKey("my_key");
|
||||
expect(result).toEqual(["my_key", storedKey]);
|
||||
});
|
||||
|
||||
it("should return the default key if none is specified", async function () {
|
||||
const accountDataAdapter = mockAccountDataClient();
|
||||
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
|
||||
|
||||
const storedKey = { iv: "iv", mac: "mac" } as SecretStorageKeyDescriptionAesV1;
|
||||
async function mockGetAccountData<T extends Record<string, any>>(eventType: string): Promise<T | null> {
|
||||
if (eventType === "m.secret_storage.default_key") {
|
||||
return { key: "default_key_id" } as unknown as T;
|
||||
} else if (eventType === "m.secret_storage.key.default_key_id") {
|
||||
return storedKey as unknown as T;
|
||||
} else {
|
||||
throw new Error(`unexpected eventType ${eventType}`);
|
||||
}
|
||||
}
|
||||
accountDataAdapter.getAccountDataFromServer.mockImplementation(mockGetAccountData);
|
||||
|
||||
const result = await secretStorage.getKey();
|
||||
expect(result).toEqual(["default_key_id", storedKey]);
|
||||
});
|
||||
|
||||
it("should return null if the key is not found", async function () {
|
||||
const accountDataAdapter = mockAccountDataClient();
|
||||
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
|
||||
// @ts-ignore
|
||||
accountDataAdapter.getAccountDataFromServer.mockResolvedValue(null);
|
||||
|
||||
const result = await secretStorage.getKey("my_key");
|
||||
expect(result).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkKey", function () {
|
||||
it("should return true for a correct key check", async function () {
|
||||
const secretStorage = new ServerSideSecretStorageImpl({} as AccountDataClient, {});
|
||||
|
||||
const myKey = new TextEncoder().encode(randomString(32));
|
||||
const { iv, mac } = await calculateKeyCheck(myKey);
|
||||
|
||||
const keyInfo: SecretStorageKeyDescriptionAesV1 = {
|
||||
name: "my key",
|
||||
passphrase: {} as PassphraseInfo,
|
||||
algorithm: "m.secret_storage.v1.aes-hmac-sha2",
|
||||
iv,
|
||||
mac,
|
||||
};
|
||||
|
||||
const result = await secretStorage.checkKey(myKey, keyInfo);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for an incorrect key check", async function () {
|
||||
const secretStorage = new ServerSideSecretStorageImpl({} as AccountDataClient, {});
|
||||
|
||||
const { iv, mac } = await calculateKeyCheck(new TextEncoder().encode("badkey"));
|
||||
|
||||
const keyInfo: SecretStorageKeyDescriptionAesV1 = {
|
||||
name: "my key",
|
||||
passphrase: {} as PassphraseInfo,
|
||||
algorithm: "m.secret_storage.v1.aes-hmac-sha2",
|
||||
iv,
|
||||
mac,
|
||||
};
|
||||
|
||||
const result = await secretStorage.checkKey(new TextEncoder().encode("goodkey"), keyInfo);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should raise for an unknown algorithm", async function () {
|
||||
const secretStorage = new ServerSideSecretStorageImpl({} as AccountDataClient, {});
|
||||
const keyInfo: SecretStorageKeyDescriptionAesV1 = {
|
||||
name: "my key",
|
||||
passphrase: {} as PassphraseInfo,
|
||||
algorithm: "bad_alg",
|
||||
iv: "iv",
|
||||
mac: "mac",
|
||||
};
|
||||
|
||||
await expect(() => secretStorage.checkKey(new TextEncoder().encode("goodkey"), keyInfo)).rejects.toThrow(
|
||||
"Unknown algorithm",
|
||||
);
|
||||
});
|
||||
|
||||
// XXX: really???
|
||||
it("should return true for an absent mac", async function () {
|
||||
const secretStorage = new ServerSideSecretStorageImpl({} as AccountDataClient, {});
|
||||
const keyInfo: SecretStorageKeyDescriptionAesV1 = {
|
||||
name: "my key",
|
||||
passphrase: {} as PassphraseInfo,
|
||||
algorithm: "m.secret_storage.v1.aes-hmac-sha2",
|
||||
iv: "iv",
|
||||
mac: "",
|
||||
};
|
||||
|
||||
const result = await secretStorage.checkKey(new TextEncoder().encode("goodkey"), keyInfo);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("store", () => {
|
||||
it("should ignore keys with unknown algorithm", async function () {
|
||||
const accountDataAdapter = mockAccountDataClient();
|
||||
const mockCallbacks = { getSecretStorageKey: jest.fn() } as Mocked<SecretStorageCallbacks>;
|
||||
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, mockCallbacks);
|
||||
|
||||
// stub out getAccountData to return a key with an unknown algorithm
|
||||
const storedKey = { algorithm: "badalg" } as SecretStorageKeyDescriptionCommon;
|
||||
async function mockGetAccountData<T extends Record<string, any>>(eventType: string): Promise<T | null> {
|
||||
if (eventType === "m.secret_storage.key.keyid") {
|
||||
return storedKey as unknown as T;
|
||||
} else {
|
||||
throw new Error(`unexpected eventType ${eventType}`);
|
||||
}
|
||||
}
|
||||
accountDataAdapter.getAccountDataFromServer.mockImplementation(mockGetAccountData);
|
||||
|
||||
// suppress the expected warning on the console
|
||||
jest.spyOn(console, "warn").mockImplementation();
|
||||
|
||||
// now attempt the store
|
||||
await secretStorage.store("mysecret", "supersecret", ["keyid"]);
|
||||
|
||||
// we should have stored... nothing
|
||||
expect(accountDataAdapter.setAccountData).toHaveBeenCalledWith("mysecret", { encrypted: {} });
|
||||
|
||||
// ... and emitted a warning.
|
||||
// eslint-disable-next-line no-console
|
||||
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining("unknown algorithm"));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("trimTrailingEquals", () => {
|
||||
it("should strip trailing =", () => {
|
||||
expect(trimTrailingEquals("ab=c===")).toEqual("ab=c");
|
||||
});
|
||||
|
||||
it("should leave strings without trailing = alone", () => {
|
||||
expect(trimTrailingEquals("ab=c")).toEqual("ab=c");
|
||||
});
|
||||
|
||||
it("should leave the empty string alone", () => {
|
||||
const result = trimTrailingEquals("");
|
||||
expect(result).toEqual("");
|
||||
});
|
||||
});
|
||||
|
||||
function mockAccountDataClient(): Mocked<AccountDataClient> {
|
||||
return {
|
||||
getAccountDataFromServer: jest.fn().mockResolvedValue(null),
|
||||
setAccountData: jest.fn().mockResolvedValue({}),
|
||||
} as unknown as Mocked<AccountDataClient>;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
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 "fake-indexeddb/auto";
|
||||
|
||||
import { LocalIndexedDBStoreBackend } from "../../../src/store/indexeddb-local-backend";
|
||||
import { IndexedDBStoreWorker } from "../../../src/store/indexeddb-store-worker";
|
||||
import { defer } from "../../../src/utils";
|
||||
|
||||
function setupWorker(worker: IndexedDBStoreWorker): void {
|
||||
worker.onMessage({ data: { command: "setupWorker", args: [] } } as any);
|
||||
worker.onMessage({ data: { command: "connect", seq: 1 } } as any);
|
||||
}
|
||||
|
||||
describe("IndexedDBStore Worker", () => {
|
||||
it("should pass 'closed' event via postMessage", async () => {
|
||||
const deferred = defer<void>();
|
||||
const postMessage = jest.fn().mockImplementation(({ seq, command }) => {
|
||||
if (seq === 1 && command === "cmd_success") {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
const worker = new IndexedDBStoreWorker(postMessage);
|
||||
setupWorker(worker);
|
||||
|
||||
await deferred.promise;
|
||||
|
||||
// @ts-ignore - private field access
|
||||
(worker.backend as LocalIndexedDBStoreBackend).db!.onclose!({} as Event);
|
||||
expect(postMessage).toHaveBeenCalledWith({
|
||||
command: "closed",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -166,4 +166,119 @@ describe("IndexedDBStore", () => {
|
||||
|
||||
await expect(store.isNewlyCreated()).resolves.toBeFalsy();
|
||||
});
|
||||
|
||||
it("should emit 'closed' if database is unexpectedly closed", async () => {
|
||||
const store = new IndexedDBStore({
|
||||
indexedDB: indexedDB,
|
||||
dbName: "database",
|
||||
localStorage,
|
||||
});
|
||||
await store.startup();
|
||||
|
||||
const deferred = defer<void>();
|
||||
store.on("closed", deferred.resolve);
|
||||
|
||||
// @ts-ignore - private field access
|
||||
(store.backend as LocalIndexedDBStoreBackend).db!.onclose!({} as Event);
|
||||
await deferred.promise;
|
||||
});
|
||||
|
||||
it("should use remote backend if workerFactory passed", async () => {
|
||||
const deferred = defer<void>();
|
||||
class MockWorker {
|
||||
postMessage(data: any) {
|
||||
if (data.command === "setupWorker") {
|
||||
deferred.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const store = new IndexedDBStore({
|
||||
indexedDB: indexedDB,
|
||||
dbName: "database",
|
||||
localStorage,
|
||||
workerFactory: () => new MockWorker() as Worker,
|
||||
});
|
||||
store.startup();
|
||||
await deferred.promise;
|
||||
});
|
||||
|
||||
it("remote worker should pass closed event", async () => {
|
||||
const worker = new (class MockWorker {
|
||||
postMessage(data: any) {}
|
||||
})() as Worker;
|
||||
|
||||
const store = new IndexedDBStore({
|
||||
indexedDB: indexedDB,
|
||||
dbName: "database",
|
||||
localStorage,
|
||||
workerFactory: () => worker,
|
||||
});
|
||||
store.startup();
|
||||
|
||||
const deferred = defer<void>();
|
||||
store.on("closed", deferred.resolve);
|
||||
(worker as any).onmessage({ data: { command: "closed" } });
|
||||
await deferred.promise;
|
||||
});
|
||||
|
||||
it("remote worker should pass command failures", async () => {
|
||||
const worker = new (class MockWorker {
|
||||
private onmessage!: (data: any) => void;
|
||||
postMessage(data: any) {
|
||||
if (data.command === "setupWorker" || data.command === "connect") {
|
||||
this.onmessage({
|
||||
data: {
|
||||
command: "cmd_success",
|
||||
seq: data.seq,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.onmessage({
|
||||
data: {
|
||||
command: "cmd_fail",
|
||||
seq: data.seq,
|
||||
error: new Error("Test"),
|
||||
},
|
||||
});
|
||||
}
|
||||
})() as unknown as Worker;
|
||||
|
||||
const store = new IndexedDBStore({
|
||||
indexedDB: indexedDB,
|
||||
dbName: "database",
|
||||
localStorage,
|
||||
workerFactory: () => worker,
|
||||
});
|
||||
await expect(store.startup()).rejects.toThrow("Test");
|
||||
});
|
||||
|
||||
it("remote worker should terminate upon destroy call", async () => {
|
||||
const terminate = jest.fn();
|
||||
const worker = new (class MockWorker {
|
||||
private onmessage!: (data: any) => void;
|
||||
postMessage(data: any) {
|
||||
this.onmessage({
|
||||
data: {
|
||||
command: "cmd_success",
|
||||
seq: data.seq,
|
||||
result: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
public terminate = terminate;
|
||||
})() as unknown as Worker;
|
||||
|
||||
const store = new IndexedDBStore({
|
||||
indexedDB: indexedDB,
|
||||
dbName: "database",
|
||||
localStorage,
|
||||
workerFactory: () => worker,
|
||||
});
|
||||
await store.startup();
|
||||
await expect(store.destroy()).resolves;
|
||||
expect(terminate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,7 +59,7 @@ describe("MemoryStore", () => {
|
||||
await store.deleteAllData();
|
||||
|
||||
// empty object
|
||||
expect(store.accountData).toEqual({});
|
||||
expect(store.accountData).toEqual(new Map());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -356,6 +356,77 @@ describe("SyncAccumulator", function () {
|
||||
});
|
||||
});
|
||||
|
||||
it("can handle large numbers of identical receipts", () => {
|
||||
const testSize = 1000; // Make this big to check performance (e.g. 10 million ~= 10s)
|
||||
|
||||
const newReceipt = (ts: number) => {
|
||||
return {
|
||||
type: "m.receipt",
|
||||
room_id: "!foo:bar",
|
||||
content: {
|
||||
"$event1:localhost": {
|
||||
[ReceiptType.Read]: {
|
||||
"@alice:localhost": { ts },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const receipts = [];
|
||||
for (let i = 0; i < testSize; i++) {
|
||||
receipts.push(newReceipt(testSize - i));
|
||||
}
|
||||
|
||||
sa.accumulate(
|
||||
syncSkeleton({
|
||||
ephemeral: {
|
||||
events: receipts,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const events = sa.getJSON().roomsData.join["!foo:bar"].ephemeral.events;
|
||||
expect(events.length).toEqual(1);
|
||||
expect(events[0]).toEqual(newReceipt(1));
|
||||
});
|
||||
|
||||
it("can handle large numbers of receipts for different users and events", () => {
|
||||
const testSize = 100; // Make this big to check performance (e.g. 1 million ~= 10s)
|
||||
|
||||
const newReceipt = (ts: number) => {
|
||||
return {
|
||||
type: "m.receipt",
|
||||
room_id: "!foo:bar",
|
||||
content: {
|
||||
[`$event${ts}:localhost`]: {
|
||||
[ReceiptType.Read]: {
|
||||
[`@alice${ts}:localhost`]: { ts },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const receipts = [];
|
||||
for (let i = 0; i < testSize; i++) {
|
||||
receipts.push(newReceipt(testSize - i));
|
||||
}
|
||||
|
||||
sa.accumulate(
|
||||
syncSkeleton({
|
||||
ephemeral: {
|
||||
events: receipts,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const events = sa.getJSON().roomsData.join["!foo:bar"].ephemeral.events;
|
||||
expect(events.length).toEqual(1);
|
||||
expect(events[0]["content"]["$event1:localhost"]).toEqual({ "m.read": { "@alice1:localhost": { ts: 1 } } });
|
||||
expect(Object.keys(events[0]["content"]).length).toEqual(testSize);
|
||||
});
|
||||
|
||||
it("should accumulate threaded read receipts", () => {
|
||||
const receipt1 = {
|
||||
type: "m.receipt",
|
||||
@@ -474,6 +545,25 @@ describe("SyncAccumulator", function () {
|
||||
expect(summary["m.heroes"]).toEqual(["@bob:bar"]);
|
||||
});
|
||||
|
||||
it("should correctly update summary properties to zero", function () {
|
||||
// When we receive updates of a summary property, the last of which is 0
|
||||
sa.accumulate(
|
||||
createSyncResponseWithSummary({
|
||||
"m.heroes": ["@alice:bar"],
|
||||
"m.invited_member_count": 2,
|
||||
}),
|
||||
);
|
||||
sa.accumulate(
|
||||
createSyncResponseWithSummary({
|
||||
"m.heroes": ["@alice:bar"],
|
||||
"m.invited_member_count": 0,
|
||||
}),
|
||||
);
|
||||
const summary = sa.getJSON().roomsData.join["!foo:bar"].summary;
|
||||
// Then we give an answer of 0
|
||||
expect(summary["m.invited_member_count"]).toEqual(0);
|
||||
});
|
||||
|
||||
it("should return correctly adjusted age attributes", () => {
|
||||
const delta = 1000;
|
||||
const startingTs = 1000;
|
||||
|
||||
@@ -24,9 +24,14 @@ import {
|
||||
lexicographicCompare,
|
||||
nextString,
|
||||
prevString,
|
||||
recursiveMapToObject,
|
||||
simpleRetryOperation,
|
||||
stringToBase,
|
||||
sortEventsByLatestContentTimestamp,
|
||||
safeSet,
|
||||
MapWithDefault,
|
||||
globToRegexp,
|
||||
escapeRegExp,
|
||||
} from "../../src/utils";
|
||||
import { logger } from "../../src/logger";
|
||||
import { mkMessage } from "../test-utils/test-utils";
|
||||
@@ -606,6 +611,105 @@ describe("utils", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("recursiveMapToObject", () => {
|
||||
it.each([
|
||||
// empty map
|
||||
{
|
||||
map: new Map(),
|
||||
expected: {},
|
||||
},
|
||||
// one level map
|
||||
{
|
||||
map: new Map<any, any>([
|
||||
["key1", "value 1"],
|
||||
["key2", 23],
|
||||
["key3", undefined],
|
||||
["key4", null],
|
||||
["key5", [1, 2, 3]],
|
||||
]),
|
||||
expected: { key1: "value 1", key2: 23, key3: undefined, key4: null, key5: [1, 2, 3] },
|
||||
},
|
||||
// two level map
|
||||
{
|
||||
map: new Map<any, any>([
|
||||
[
|
||||
"key1",
|
||||
new Map<any, any>([
|
||||
["key1_1", "value 1"],
|
||||
["key1_2", "value 1.2"],
|
||||
]),
|
||||
],
|
||||
["key2", "value 2"],
|
||||
]),
|
||||
expected: { key1: { key1_1: "value 1", key1_2: "value 1.2" }, key2: "value 2" },
|
||||
},
|
||||
// multi level map
|
||||
{
|
||||
map: new Map<any, any>([
|
||||
["key1", new Map<any, any>([["key1_1", new Map<any, any>([["key1_1_1", "value 1.1.1"]])]])],
|
||||
]),
|
||||
expected: { key1: { key1_1: { key1_1_1: "value 1.1.1" } } },
|
||||
},
|
||||
// list of maps
|
||||
{
|
||||
map: new Map<any, any>([
|
||||
[
|
||||
"key1",
|
||||
[new Map<any, any>([["key1_1", "value 1.1"]]), new Map<any, any>([["key1_2", "value 1.2"]])],
|
||||
],
|
||||
]),
|
||||
expected: { key1: [{ key1_1: "value 1.1" }, { key1_2: "value 1.2" }] },
|
||||
},
|
||||
// map → array → array → map
|
||||
{
|
||||
map: new Map<any, any>([["key1", [[new Map<any, any>([["key2", "value 2"]])]]]]),
|
||||
expected: {
|
||||
key1: [
|
||||
[
|
||||
{
|
||||
key2: "value 2",
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
])("%# should convert the value", ({ map, expected }) => {
|
||||
expect(recursiveMapToObject(map)).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("safeSet", () => {
|
||||
it("should set a value", () => {
|
||||
const obj: Record<string, string> = {};
|
||||
safeSet(obj, "testProp", "test value");
|
||||
expect(obj).toEqual({ testProp: "test value" });
|
||||
});
|
||||
|
||||
it.each(["__proto__", "prototype", "constructor"])("should raise an error when setting »%s«", (prop) => {
|
||||
expect(() => {
|
||||
safeSet(<Record<string, string>>{}, prop, "teset value");
|
||||
}).toThrow("Trying to modify prototype or constructor");
|
||||
});
|
||||
});
|
||||
|
||||
describe("MapWithDefault", () => {
|
||||
it("getOrCreate should create the value if it does not exist", () => {
|
||||
const newValue = {};
|
||||
const map = new MapWithDefault(() => newValue);
|
||||
|
||||
// undefined before getOrCreate
|
||||
expect(map.get("test")).toBeUndefined();
|
||||
|
||||
expect(map.getOrCreate("test")).toBe(newValue);
|
||||
|
||||
// default value after getOrCreate
|
||||
expect(map.get("test")).toBe(newValue);
|
||||
|
||||
// test that it always returns the same value
|
||||
expect(map.getOrCreate("test")).toBe(newValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sleep", () => {
|
||||
it("resolves", async () => {
|
||||
await utils.sleep(0);
|
||||
@@ -623,4 +727,19 @@ describe("utils", function () {
|
||||
await utils.immediate();
|
||||
});
|
||||
});
|
||||
|
||||
describe("escapeRegExp", () => {
|
||||
it("should escape XYZ", () => {
|
||||
expect(escapeRegExp("[FIT-Connect Zustelldienst \\(Testumgebung\\)]")).toMatchInlineSnapshot(
|
||||
`"\\[FIT-Connect Zustelldienst \\\\\\(Testumgebung\\\\\\)\\]"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("globToRegexp", () => {
|
||||
it("should not explode when given regexes as globs", () => {
|
||||
const result = globToRegexp("[FIT-Connect Zustelldienst \\(Testumgebung\\)]");
|
||||
expect(result).toMatchInlineSnapshot(`"\\[FIT-Connect Zustelldienst \\\\\\(Testumgebung\\\\\\)\\]"`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+214
-11
@@ -25,6 +25,7 @@ import {
|
||||
CallType,
|
||||
CallState,
|
||||
CallParty,
|
||||
CallDirection,
|
||||
} from "../../../src/webrtc/call";
|
||||
import {
|
||||
MCallAnswer,
|
||||
@@ -431,6 +432,58 @@ describe("Call", function () {
|
||||
expect(transceivers.get("m.usermedia:video")!.sender.track!.id).toBe("usermedia_video_track");
|
||||
});
|
||||
|
||||
it("should handle error on call upgrade", async () => {
|
||||
const onError = jest.fn();
|
||||
call.on(CallEvent.Error, onError);
|
||||
|
||||
await startVoiceCall(client, call);
|
||||
|
||||
await call.onAnswerReceived(
|
||||
makeMockEvent("@test:foo", {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: "party_id",
|
||||
answer: {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
[SDPStreamMetadataKey]: {},
|
||||
}),
|
||||
);
|
||||
|
||||
const mockGetUserMediaStream = jest.fn().mockRejectedValue(new Error("Test error"));
|
||||
client.client.getMediaHandler().getUserMediaStream = mockGetUserMediaStream;
|
||||
|
||||
// then unmute which should cause an upgrade
|
||||
await call.setLocalVideoMuted(false);
|
||||
|
||||
expect(onError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should unmute video after upgrading to video call", async () => {
|
||||
// Regression test for https://github.com/vector-im/element-call/issues/925
|
||||
await startVoiceCall(client, call);
|
||||
// start off with video muted
|
||||
await call.setLocalVideoMuted(true);
|
||||
|
||||
await call.onAnswerReceived(
|
||||
makeMockEvent("@test:foo", {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: "party_id",
|
||||
answer: {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
[SDPStreamMetadataKey]: {},
|
||||
}),
|
||||
);
|
||||
|
||||
// then unmute which should cause an upgrade
|
||||
await call.setLocalVideoMuted(false);
|
||||
|
||||
// video should now be unmuted
|
||||
expect(call.isLocalVideoMuted()).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle SDPStreamMetadata changes", async () => {
|
||||
await startVoiceCall(client, call);
|
||||
|
||||
@@ -712,11 +765,22 @@ describe("Call", function () {
|
||||
|
||||
const dataChannel = call.createDataChannel("data_channel_label", { id: 123 });
|
||||
|
||||
expect(dataChannelCallback).toHaveBeenCalledWith(dataChannel);
|
||||
expect(dataChannelCallback).toHaveBeenCalledWith(dataChannel, call);
|
||||
expect(dataChannel.label).toBe("data_channel_label");
|
||||
expect(dataChannel.id).toBe(123);
|
||||
});
|
||||
|
||||
it("should emit a data channel event when the other side adds a data channel", async () => {
|
||||
await startVoiceCall(client, call);
|
||||
|
||||
const dataChannelCallback = jest.fn();
|
||||
call.on(CallEvent.DataChannel, dataChannelCallback);
|
||||
|
||||
(call.peerConn as unknown as MockRTCPeerConnection).triggerIncomingDataChannel();
|
||||
|
||||
expect(dataChannelCallback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("supportsMatrixCall", () => {
|
||||
it("should return true when the environment is right", () => {
|
||||
expect(supportsMatrixCall()).toBe(true);
|
||||
@@ -990,14 +1054,7 @@ describe("Call", function () {
|
||||
|
||||
mockSendEvent.mockReset();
|
||||
|
||||
let caught = false;
|
||||
try {
|
||||
call.reject();
|
||||
} catch (e) {
|
||||
caught = true;
|
||||
}
|
||||
|
||||
expect(caught).toEqual(true);
|
||||
expect(() => call.reject()).toThrow();
|
||||
expect(client.client.sendEvent).not.toHaveBeenCalled();
|
||||
|
||||
call.hangup(CallErrorCode.UserHangup, true);
|
||||
@@ -1579,7 +1636,7 @@ describe("Call", function () {
|
||||
hasAdvancedBy += advanceBy;
|
||||
|
||||
expect(lengthChangedListener).toHaveBeenCalledTimes(hasAdvancedBy);
|
||||
expect(lengthChangedListener).toHaveBeenCalledWith(hasAdvancedBy);
|
||||
expect(lengthChangedListener).toHaveBeenCalledWith(hasAdvancedBy, call);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1589,12 +1646,18 @@ describe("Call", function () {
|
||||
beforeEach(async () => {
|
||||
jest.useFakeTimers();
|
||||
jest.spyOn(call, "hangup");
|
||||
|
||||
await fakeIncomingCall(client, call, "1");
|
||||
|
||||
mockPeerConn = call.peerConn as unknown as MockRTCPeerConnection;
|
||||
|
||||
mockPeerConn.iceConnectionState = "disconnected";
|
||||
mockPeerConn.iceConnectionStateChangeListener!();
|
||||
jest.spyOn(mockPeerConn, "restartIce");
|
||||
});
|
||||
|
||||
it("should restart ICE gathering after being disconnected for 2 seconds", () => {
|
||||
jest.advanceTimersByTime(3 * 1000);
|
||||
expect(mockPeerConn.restartIce).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should hang up after being disconnected for 30 seconds", () => {
|
||||
@@ -1602,6 +1665,20 @@ describe("Call", function () {
|
||||
expect(call.hangup).toHaveBeenCalledWith(CallErrorCode.IceFailed, false);
|
||||
});
|
||||
|
||||
it("should restart ICE gathering once again after ICE being failed", () => {
|
||||
mockPeerConn.iceConnectionState = "failed";
|
||||
mockPeerConn.iceConnectionStateChangeListener!();
|
||||
expect(mockPeerConn.restartIce).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call hangup after ICE being failed and if there not exists a restartIce method", () => {
|
||||
// @ts-ignore
|
||||
mockPeerConn.restartIce = null;
|
||||
mockPeerConn.iceConnectionState = "failed";
|
||||
mockPeerConn.iceConnectionStateChangeListener!();
|
||||
expect(call.hangup).toHaveBeenCalledWith(CallErrorCode.IceFailed, false);
|
||||
});
|
||||
|
||||
it("should not hangup if we've managed to re-connect", () => {
|
||||
mockPeerConn.iceConnectionState = "connected";
|
||||
mockPeerConn.iceConnectionStateChangeListener!();
|
||||
@@ -1609,4 +1686,130 @@ describe("Call", function () {
|
||||
expect(call.hangup).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Call replace", () => {
|
||||
it("Fires event when call replaced", async () => {
|
||||
const onReplace = jest.fn();
|
||||
call.on(CallEvent.Replaced, onReplace);
|
||||
|
||||
await call.placeVoiceCall();
|
||||
|
||||
const call2 = new MatrixCall({
|
||||
client: client.client,
|
||||
roomId: FAKE_ROOM_ID,
|
||||
});
|
||||
call2.on(CallEvent.Error, errorListener);
|
||||
await fakeIncomingCall(client, call2);
|
||||
|
||||
call.replacedBy(call2);
|
||||
|
||||
expect(onReplace).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe("should handle glare in negotiation process", () => {
|
||||
beforeEach(async () => {
|
||||
// cut methods not want to test
|
||||
call.hangup = () => null;
|
||||
call.isLocalOnHold = () => true;
|
||||
// @ts-ignore
|
||||
call.updateRemoteSDPStreamMetadata = jest.fn();
|
||||
// @ts-ignore
|
||||
call.getRidOfRTXCodecs = jest.fn();
|
||||
// @ts-ignore
|
||||
call.createAnswer = jest.fn().mockResolvedValue({});
|
||||
// @ts-ignore
|
||||
call.sendVoipEvent = jest.fn();
|
||||
});
|
||||
|
||||
it("and reject remote offer if not polite and have pending local offer", async () => {
|
||||
// not polite user == CallDirection.Outbound
|
||||
call.direction = CallDirection.Outbound;
|
||||
// have already a local offer
|
||||
// @ts-ignore
|
||||
call.makingOffer = true;
|
||||
const offerEvent = makeMockEvent("@test:foo", {
|
||||
description: {
|
||||
type: "offer",
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
});
|
||||
// @ts-ignore
|
||||
call.peerConn = {
|
||||
signalingState: "have-local-offer",
|
||||
setRemoteDescription: jest.fn(),
|
||||
};
|
||||
await call.onNegotiateReceived(offerEvent);
|
||||
expect(call.peerConn?.setRemoteDescription).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("and not reject remote offer if not polite and do have pending answer", async () => {
|
||||
// not polite user == CallDirection.Outbound
|
||||
call.direction = CallDirection.Outbound;
|
||||
// have not a local offer
|
||||
// @ts-ignore
|
||||
call.makingOffer = false;
|
||||
|
||||
// If we have a setRemoteDescription() answer operation pending, then
|
||||
// we will be "stable" by the time the next setRemoteDescription() is
|
||||
// executed, so we count this being readyForOffer when deciding whether to
|
||||
// ignore the offer.
|
||||
// @ts-ignore
|
||||
call.isSettingRemoteAnswerPending = true;
|
||||
const offerEvent = makeMockEvent("@test:foo", {
|
||||
description: {
|
||||
type: "offer",
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
});
|
||||
// @ts-ignore
|
||||
call.peerConn = {
|
||||
signalingState: "have-local-offer",
|
||||
setRemoteDescription: jest.fn(),
|
||||
};
|
||||
await call.onNegotiateReceived(offerEvent);
|
||||
expect(call.peerConn?.setRemoteDescription).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("and not reject remote offer if not polite and do not have pending local offer", async () => {
|
||||
// not polite user == CallDirection.Outbound
|
||||
call.direction = CallDirection.Outbound;
|
||||
// have no local offer
|
||||
// @ts-ignore
|
||||
call.makingOffer = false;
|
||||
const offerEvent = makeMockEvent("@test:foo", {
|
||||
description: {
|
||||
type: "offer",
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
});
|
||||
// @ts-ignore
|
||||
call.peerConn = {
|
||||
signalingState: "stable",
|
||||
setRemoteDescription: jest.fn(),
|
||||
};
|
||||
await call.onNegotiateReceived(offerEvent);
|
||||
expect(call.peerConn?.setRemoteDescription).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("and if polite do rollback pending local offer", async () => {
|
||||
// polite user == CallDirection.Inbound
|
||||
call.direction = CallDirection.Inbound;
|
||||
// have already a local offer
|
||||
// @ts-ignore
|
||||
call.makingOffer = true;
|
||||
const offerEvent = makeMockEvent("@test:foo", {
|
||||
description: {
|
||||
type: "offer",
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
});
|
||||
// @ts-ignore
|
||||
call.peerConn = {
|
||||
signalingState: "have-local-offer",
|
||||
setRemoteDescription: jest.fn(),
|
||||
};
|
||||
await call.onNegotiateReceived(offerEvent);
|
||||
expect(call.peerConn?.setRemoteDescription).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -102,7 +102,7 @@ describe("CallFeed", () => {
|
||||
[CallState.Connected, true],
|
||||
[CallState.Connecting, false],
|
||||
])("should react to call state, when !isLocal()", (state: CallState, expected: Boolean) => {
|
||||
call.emit(CallEvent.State, state);
|
||||
call.emit(CallEvent.State, state, CallState.InviteSent, call.typed());
|
||||
|
||||
expect(feed.connected).toBe(expected);
|
||||
});
|
||||
|
||||
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { EventType, GroupCallIntent, GroupCallType, MatrixCall, MatrixEvent, Room, RoomMember } from "../../../src";
|
||||
import { RoomStateEvent } from "../../../src/models/room-state";
|
||||
import { GroupCall, GroupCallEvent, GroupCallState } from "../../../src/webrtc/groupCall";
|
||||
@@ -95,16 +97,16 @@ const FAKE_STATE_EVENTS = [
|
||||
},
|
||||
];
|
||||
|
||||
const mockGetStateEvents = (type: EventType, userId?: string): MatrixEvent[] | MatrixEvent | null => {
|
||||
if (type === EventType.GroupCallMemberPrefix) {
|
||||
return userId === undefined
|
||||
? (FAKE_STATE_EVENTS as MatrixEvent[])
|
||||
: (FAKE_STATE_EVENTS.find((e) => e.getStateKey() === userId) as MatrixEvent);
|
||||
} else {
|
||||
const fakeEvent = { getContent: () => ({}), getTs: () => 0 } as MatrixEvent;
|
||||
return userId === undefined ? [fakeEvent] : fakeEvent;
|
||||
}
|
||||
};
|
||||
const mockGetStateEvents =
|
||||
(events: MatrixEvent[] = FAKE_STATE_EVENTS as MatrixEvent[]) =>
|
||||
(type: EventType, userId?: string): MatrixEvent[] | MatrixEvent | null => {
|
||||
if (type === EventType.GroupCallMemberPrefix) {
|
||||
return userId === undefined ? events : events.find((e) => e.getStateKey() === userId) ?? null;
|
||||
} else {
|
||||
const fakeEvent = { getContent: () => ({}), getTs: () => 0 } as MatrixEvent;
|
||||
return userId === undefined ? [fakeEvent] : fakeEvent;
|
||||
}
|
||||
};
|
||||
|
||||
const ONE_HOUR = 1000 * 60 * 60;
|
||||
|
||||
@@ -142,6 +144,10 @@ describe("Group Call", function () {
|
||||
} as unknown as RoomMember;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
groupCall.leave();
|
||||
});
|
||||
|
||||
it.each(Object.values(GroupCallState).filter((v) => v !== GroupCallState.LocalCallFeedUninitialized))(
|
||||
"throws when initializing local call feed in %s state",
|
||||
async (state: GroupCallState) => {
|
||||
@@ -180,10 +186,7 @@ describe("Group Call", function () {
|
||||
it("sets state to local call feed uninitialized when getUserMedia() fails", async () => {
|
||||
jest.spyOn(mockClient.getMediaHandler(), "getUserMediaStream").mockRejectedValue("Error");
|
||||
|
||||
try {
|
||||
await groupCall.initLocalCallFeed();
|
||||
} catch (e) {}
|
||||
|
||||
await expect(groupCall.initLocalCallFeed()).rejects.toBeTruthy();
|
||||
expect(groupCall.state).toBe(GroupCallState.LocalCallFeedUninitialized);
|
||||
});
|
||||
|
||||
@@ -382,6 +385,10 @@ describe("Group Call", function () {
|
||||
call = new MockMatrixCall(room.roomId, groupCall.groupCallId);
|
||||
|
||||
await groupCall.create();
|
||||
|
||||
const deviceCallMap = new Map<string, MatrixCall>();
|
||||
deviceCallMap.set(FAKE_DEVICE_ID_1, call.typed());
|
||||
(groupCall as any).calls.set(FAKE_USER_ID_1, deviceCallMap);
|
||||
});
|
||||
|
||||
it("ignores changes, if we can't get user id of opponent", async () => {
|
||||
@@ -507,8 +514,7 @@ describe("Group Call", function () {
|
||||
await groupCall.setMicrophoneMuted(false);
|
||||
expect(groupCall.isMicrophoneMuted()).toEqual(false);
|
||||
|
||||
jest.advanceTimersByTime(groupCall.pttMaxTransmitTime + 100);
|
||||
|
||||
await jest.advanceTimersByTimeAsync(groupCall.pttMaxTransmitTime + 100);
|
||||
expect(groupCall.isMicrophoneMuted()).toEqual(true);
|
||||
});
|
||||
|
||||
@@ -567,7 +573,7 @@ describe("Group Call", function () {
|
||||
|
||||
// the call starts muted, so unmute to get in the right state to test
|
||||
await groupCall.setMicrophoneMuted(false);
|
||||
mockCall.localUsermediaFeed.setAudioVideoMuted.mockReset();
|
||||
mocked(mockCall.localUsermediaFeed.setAudioVideoMuted).mockReset();
|
||||
|
||||
let metadataUpdateResolve: () => void;
|
||||
const metadataUpdatePromise = new Promise<void>((resolve) => {
|
||||
@@ -575,7 +581,15 @@ describe("Group Call", function () {
|
||||
});
|
||||
mockCall.sendMetadataUpdate = jest.fn().mockReturnValue(metadataUpdatePromise);
|
||||
|
||||
const getUserMediaStreamFlush = Promise.resolve("stream");
|
||||
// @ts-ignore
|
||||
mockCall.cleint = {
|
||||
getMediaHandler: {
|
||||
getUserMediaStream: jest.fn().mockReturnValue(getUserMediaStreamFlush),
|
||||
},
|
||||
};
|
||||
const mutePromise = groupCall.setMicrophoneMuted(true);
|
||||
await getUserMediaStreamFlush;
|
||||
// we should be muted at this point, before the metadata update has been sent
|
||||
expect(groupCall.isMicrophoneMuted()).toEqual(true);
|
||||
expect(mockCall.localUsermediaFeed.setAudioVideoMuted).toHaveBeenCalled();
|
||||
@@ -688,15 +702,15 @@ describe("Group Call", function () {
|
||||
expect(client1.sendToDevice.mock.calls[0][0]).toBe("m.call.invite");
|
||||
|
||||
const toDeviceCallContent = client1.sendToDevice.mock.calls[0][1];
|
||||
expect(Object.keys(toDeviceCallContent).length).toBe(1);
|
||||
expect(Object.keys(toDeviceCallContent)[0]).toBe(FAKE_USER_ID_2);
|
||||
expect(toDeviceCallContent.size).toBe(1);
|
||||
expect(toDeviceCallContent.has(FAKE_USER_ID_2)).toBe(true);
|
||||
|
||||
const toDeviceBobDevices = toDeviceCallContent[FAKE_USER_ID_2];
|
||||
expect(Object.keys(toDeviceBobDevices).length).toBe(1);
|
||||
expect(Object.keys(toDeviceBobDevices)[0]).toBe(FAKE_DEVICE_ID_2);
|
||||
const toDeviceBobDevices = toDeviceCallContent.get(FAKE_USER_ID_2);
|
||||
expect(toDeviceBobDevices?.size).toBe(1);
|
||||
expect(toDeviceBobDevices?.has(FAKE_DEVICE_ID_2)).toBe(true);
|
||||
|
||||
const bobDeviceMessage = toDeviceBobDevices[FAKE_DEVICE_ID_2];
|
||||
expect(bobDeviceMessage.conf_id).toBe(FAKE_CONF_ID);
|
||||
const bobDeviceMessage = toDeviceBobDevices?.get(FAKE_DEVICE_ID_2);
|
||||
expect(bobDeviceMessage?.conf_id).toBe(FAKE_CONF_ID);
|
||||
} finally {
|
||||
await Promise.all([groupCall1.leave(), groupCall2.leave()]);
|
||||
}
|
||||
@@ -792,7 +806,7 @@ describe("Group Call", function () {
|
||||
call.isLocalVideoMuted = jest.fn().mockReturnValue(true);
|
||||
call.setLocalVideoMuted = jest.fn();
|
||||
|
||||
call.emit(CallEvent.State, CallState.Connected);
|
||||
call.emit(CallEvent.State, CallState.Connected, CallState.InviteSent, call);
|
||||
|
||||
expect(call.setMicrophoneMuted).toHaveBeenCalledWith(false);
|
||||
expect(call.setLocalVideoMuted).toHaveBeenCalledWith(false);
|
||||
@@ -811,7 +825,7 @@ describe("Group Call", function () {
|
||||
mockClient = typedMockClient as unknown as MatrixClient;
|
||||
|
||||
room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_1);
|
||||
room.currentState.getStateEvents = jest.fn().mockImplementation(mockGetStateEvents);
|
||||
room.currentState.getStateEvents = jest.fn().mockImplementation(mockGetStateEvents());
|
||||
room.currentState.members[FAKE_USER_ID_1] = {
|
||||
userId: FAKE_USER_ID_1,
|
||||
membership: "join",
|
||||
@@ -882,14 +896,34 @@ describe("Group Call", function () {
|
||||
expect(await groupCall.setMicrophoneMuted(false)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when no permission for audio stream", async () => {
|
||||
it("returns false when no permission for audio stream and localCallFeed do not have an audio track", async () => {
|
||||
const groupCall = await createAndEnterGroupCall(mockClient, room);
|
||||
// @ts-ignore
|
||||
jest.spyOn(groupCall.localCallFeed, "hasAudioTrack", "get").mockReturnValue(false);
|
||||
jest.spyOn(mockClient.getMediaHandler(), "getUserMediaStream").mockRejectedValueOnce(
|
||||
new Error("No Permission"),
|
||||
);
|
||||
expect(await groupCall.setMicrophoneMuted(false)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when user media stream null", async () => {
|
||||
const groupCall = await createAndEnterGroupCall(mockClient, room);
|
||||
// @ts-ignore
|
||||
jest.spyOn(groupCall.localCallFeed, "hasAudioTrack", "get").mockReturnValue(false);
|
||||
// @ts-ignore
|
||||
jest.spyOn(mockClient.getMediaHandler(), "getUserMediaStream").mockResolvedValue({} as MediaStream);
|
||||
expect(await groupCall.setMicrophoneMuted(false)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when no permission for audio stream but localCallFeed has a audio track already", async () => {
|
||||
const groupCall = await createAndEnterGroupCall(mockClient, room);
|
||||
// @ts-ignore
|
||||
jest.spyOn(groupCall.localCallFeed, "hasAudioTrack", "get").mockReturnValue(true);
|
||||
jest.spyOn(mockClient.getMediaHandler(), "getUserMediaStream");
|
||||
expect(mockClient.getMediaHandler().getUserMediaStream).not.toHaveBeenCalled();
|
||||
expect(await groupCall.setMicrophoneMuted(false)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when unmuting video with no video device", async () => {
|
||||
const groupCall = await createAndEnterGroupCall(mockClient, room);
|
||||
jest.spyOn(mockClient.getMediaHandler(), "hasVideoDevice").mockResolvedValue(false);
|
||||
@@ -987,7 +1021,14 @@ describe("Group Call", function () {
|
||||
mockClient = typedMockClient as unknown as MatrixClient;
|
||||
|
||||
room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_2);
|
||||
room.getMember = jest.fn().mockImplementation((userId) => ({ userId }));
|
||||
room.currentState.members[FAKE_USER_ID_1] = {
|
||||
userId: FAKE_USER_ID_1,
|
||||
membership: "join",
|
||||
} as unknown as RoomMember;
|
||||
room.currentState.members[FAKE_USER_ID_2] = {
|
||||
userId: FAKE_USER_ID_2,
|
||||
membership: "join",
|
||||
} as unknown as RoomMember;
|
||||
|
||||
groupCall = await createAndEnterGroupCall(mockClient, room);
|
||||
});
|
||||
@@ -1060,6 +1101,71 @@ describe("Group Call", function () {
|
||||
expect(call.answerWithCallFeeds).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const aliceEnters = () => {
|
||||
room.currentState.getStateEvents = jest.fn().mockImplementation(
|
||||
mockGetStateEvents([
|
||||
{
|
||||
getContent: () => ({
|
||||
"m.calls": [
|
||||
{
|
||||
"m.call_id": groupCall.groupCallId,
|
||||
"m.devices": [
|
||||
{
|
||||
device_id: FAKE_DEVICE_ID_1,
|
||||
session_id: FAKE_SESSION_ID_1,
|
||||
expires_ts: Date.now() + ONE_HOUR,
|
||||
feeds: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
getStateKey: () => FAKE_USER_ID_1,
|
||||
getRoomId: () => FAKE_ROOM_ID,
|
||||
getTs: () => 0,
|
||||
},
|
||||
] as unknown as MatrixEvent[]),
|
||||
);
|
||||
room.currentState.emit(RoomStateEvent.Update, room.currentState);
|
||||
};
|
||||
|
||||
const aliceLeaves = () => {
|
||||
room.currentState.getStateEvents = jest
|
||||
.fn()
|
||||
.mockImplementation(mockGetStateEvents([] as unknown as MatrixEvent[]));
|
||||
room.currentState.emit(RoomStateEvent.Update, room.currentState);
|
||||
};
|
||||
|
||||
it("enables tracks on expected calls, then disables them when the participant leaves", async () => {
|
||||
aliceEnters();
|
||||
|
||||
const mockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId);
|
||||
mockCall.answerWithCallFeeds.mockImplementation(([feed]) => (mockCall.localUsermediaFeed = feed));
|
||||
mockClient.emit(CallEventHandlerEvent.Incoming, mockCall as unknown as MatrixCall);
|
||||
|
||||
// Tracks should be enabled
|
||||
expect(mockCall.localUsermediaFeed.stream.getTracks().every((t) => t.enabled)).toBe(true);
|
||||
|
||||
aliceLeaves();
|
||||
|
||||
// Tracks should be disabled
|
||||
expect(mockCall.localUsermediaFeed.stream.getTracks().every((t) => !t.enabled)).toBe(true);
|
||||
});
|
||||
|
||||
it("disables tracks on unexpected calls, then enables them when the participant joins", async () => {
|
||||
const mockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId);
|
||||
mockCall.answerWithCallFeeds.mockImplementation(([feed]) => (mockCall.localUsermediaFeed = feed));
|
||||
mockClient.emit(CallEventHandlerEvent.Incoming, mockCall as unknown as MatrixCall);
|
||||
|
||||
// Tracks should be disabled
|
||||
expect(mockCall.localUsermediaFeed.stream.getTracks().every((t) => !t.enabled)).toBe(true);
|
||||
|
||||
aliceEnters();
|
||||
|
||||
// Tracks should be enabled
|
||||
expect(mockCall.localUsermediaFeed.stream.getTracks().every((t) => t.enabled)).toBe(true);
|
||||
});
|
||||
|
||||
describe("handles call being replaced", () => {
|
||||
let callChangedListener: jest.Mock;
|
||||
let oldMockCall: MockMatrixCall;
|
||||
@@ -1080,7 +1186,7 @@ describe("Group Call", function () {
|
||||
});
|
||||
|
||||
it("handles regular case", () => {
|
||||
oldMockCall.emit(CallEvent.Replaced, newMockCall.typed());
|
||||
oldMockCall.emit(CallEvent.Replaced, newMockCall.typed(), oldMockCall.typed());
|
||||
|
||||
expect(oldMockCall.hangup).toHaveBeenCalled();
|
||||
expect(callChangedListener).toHaveBeenCalledWith(newCallsMap);
|
||||
@@ -1091,7 +1197,7 @@ describe("Group Call", function () {
|
||||
it("handles case where call is missing from the calls map", () => {
|
||||
// @ts-ignore
|
||||
groupCall.calls = new Map();
|
||||
oldMockCall.emit(CallEvent.Replaced, newMockCall.typed());
|
||||
oldMockCall.emit(CallEvent.Replaced, newMockCall.typed(), oldMockCall.typed());
|
||||
|
||||
expect(oldMockCall.hangup).toHaveBeenCalled();
|
||||
expect(callChangedListener).toHaveBeenCalledWith(newCallsMap);
|
||||
@@ -1157,7 +1263,7 @@ describe("Group Call", function () {
|
||||
userId: FAKE_USER_ID_2,
|
||||
membership: "join",
|
||||
} as unknown as RoomMember;
|
||||
room.currentState.getStateEvents = jest.fn().mockImplementation(mockGetStateEvents);
|
||||
room.currentState.getStateEvents = jest.fn().mockImplementation(mockGetStateEvents());
|
||||
|
||||
groupCall = await createAndEnterGroupCall(mockClient, room);
|
||||
});
|
||||
@@ -1547,4 +1653,77 @@ describe("Group Call", function () {
|
||||
expect(room.currentState.getStateEvents(EventType.GroupCallMemberPrefix, FAKE_USER_ID_2)).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("collection stats", () => {
|
||||
let groupCall: GroupCall;
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(0);
|
||||
});
|
||||
|
||||
afterAll(() => jest.useRealTimers());
|
||||
|
||||
beforeEach(async () => {
|
||||
const typedMockClient = new MockCallMatrixClient(FAKE_USER_ID_1, FAKE_DEVICE_ID_1, FAKE_SESSION_ID_1);
|
||||
const mockClient = typedMockClient.typed();
|
||||
const room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_1);
|
||||
groupCall = new GroupCall(
|
||||
mockClient,
|
||||
room,
|
||||
GroupCallType.Video,
|
||||
false,
|
||||
GroupCallIntent.Prompt,
|
||||
FAKE_CONF_ID,
|
||||
);
|
||||
});
|
||||
it("should be undefined if not get stats", async () => {
|
||||
// @ts-ignore
|
||||
const stats = groupCall.stats;
|
||||
expect(stats).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should be defined after first access", async () => {
|
||||
groupCall.getGroupCallStats();
|
||||
// @ts-ignore
|
||||
const stats = groupCall.stats;
|
||||
expect(stats).toBeDefined();
|
||||
});
|
||||
|
||||
it("with every number should do nothing if no stats exists.", async () => {
|
||||
groupCall.setGroupCallStatsInterval(0);
|
||||
// @ts-ignore
|
||||
let stats = groupCall.stats;
|
||||
expect(stats).toBeUndefined();
|
||||
|
||||
groupCall.setGroupCallStatsInterval(10000);
|
||||
// @ts-ignore
|
||||
stats = groupCall.stats;
|
||||
expect(stats).toBeUndefined();
|
||||
});
|
||||
|
||||
it("with number should stop existing stats", async () => {
|
||||
const stats = groupCall.getGroupCallStats();
|
||||
// @ts-ignore
|
||||
const stop = jest.spyOn(stats, "stop");
|
||||
// @ts-ignore
|
||||
const start = jest.spyOn(stats, "start");
|
||||
groupCall.setGroupCallStatsInterval(0);
|
||||
|
||||
expect(stop).toHaveBeenCalled();
|
||||
expect(start).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("with number should restart existing stats", async () => {
|
||||
const stats = groupCall.getGroupCallStats();
|
||||
// @ts-ignore
|
||||
const stop = jest.spyOn(stats, "stop");
|
||||
// @ts-ignore
|
||||
const start = jest.spyOn(stats, "start");
|
||||
groupCall.setGroupCallStatsInterval(10000);
|
||||
|
||||
expect(stop).toHaveBeenCalled();
|
||||
expect(start).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -98,6 +98,23 @@ describe("Group Call Event Handler", function () {
|
||||
|
||||
expect(groupCall.state).toBe(GroupCallState.Ended);
|
||||
});
|
||||
|
||||
it("terminates call when redacted", async () => {
|
||||
await groupCallEventHandler.start();
|
||||
mockClient.emitRoomState(makeMockGroupCallStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID), {
|
||||
roomId: FAKE_ROOM_ID,
|
||||
} as unknown as RoomState);
|
||||
|
||||
const groupCall = groupCallEventHandler.groupCalls.get(FAKE_ROOM_ID)!;
|
||||
|
||||
expect(groupCall.state).toBe(GroupCallState.LocalCallFeedUninitialized);
|
||||
|
||||
mockClient.emitRoomState(makeMockGroupCallStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID, undefined, true), {
|
||||
roomId: FAKE_ROOM_ID,
|
||||
} as unknown as RoomState);
|
||||
|
||||
expect(groupCall.state).toBe(GroupCallState.Ended);
|
||||
});
|
||||
});
|
||||
|
||||
it("waits until client starts syncing", async () => {
|
||||
@@ -222,9 +239,9 @@ describe("Group Call Event Handler", function () {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const setupCallAndStart = async (content?: IContent) => {
|
||||
const setupCallAndStart = async (content?: IContent, redacted?: boolean) => {
|
||||
mocked(mockRoom.currentState.getStateEvents).mockReturnValue([
|
||||
makeMockGroupCallStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID, content),
|
||||
makeMockGroupCallStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID, content, redacted),
|
||||
] as unknown as MatrixEvent);
|
||||
mockClient.getRooms.mockReturnValue([mockRoom]);
|
||||
await groupCallEventHandler.start();
|
||||
@@ -285,5 +302,24 @@ describe("Group Call Event Handler", function () {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("ignores redacted calls", async () => {
|
||||
await setupCallAndStart(
|
||||
{
|
||||
// Real event contents to make sure that it's specifically the
|
||||
// event being redacted that causes it to be ignored
|
||||
"m.type": GroupCallType.Video,
|
||||
"m.intent": GroupCallIntent.Prompt,
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
expect(mockClientEmit).not.toHaveBeenCalledWith(
|
||||
GroupCallEventHandlerEvent.Incoming,
|
||||
expect.objectContaining({
|
||||
groupCallId: FAKE_GROUP_CALL_ID,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
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 { CallStatsReportGatherer } from "../../../../src/webrtc/stats/callStatsReportGatherer";
|
||||
import { StatsReportEmitter } from "../../../../src/webrtc/stats/statsReportEmitter";
|
||||
import { MediaSsrcHandler } from "../../../../src/webrtc/stats/media/mediaSsrcHandler";
|
||||
|
||||
const CALL_ID = "CALL_ID";
|
||||
const USER_ID = "USER_ID";
|
||||
|
||||
describe("CallStatsReportGatherer", () => {
|
||||
let collector: CallStatsReportGatherer;
|
||||
let rtcSpy: RTCPeerConnection;
|
||||
let emitter: StatsReportEmitter;
|
||||
beforeEach(() => {
|
||||
rtcSpy = { getStats: () => new Promise<RTCStatsReport>(() => null) } as RTCPeerConnection;
|
||||
rtcSpy.addEventListener = jest.fn();
|
||||
emitter = new StatsReportEmitter();
|
||||
collector = new CallStatsReportGatherer(CALL_ID, USER_ID, rtcSpy, emitter);
|
||||
});
|
||||
|
||||
describe("on process stats", () => {
|
||||
it("if active calculate stats reports", async () => {
|
||||
const getStats = jest.spyOn(rtcSpy, "getStats");
|
||||
const report = {} as RTCStatsReport;
|
||||
report.forEach = jest.fn().mockReturnValue([]);
|
||||
getStats.mockResolvedValue(report);
|
||||
const actual = await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID");
|
||||
expect(getStats).toHaveBeenCalled();
|
||||
expect(actual).toEqual({
|
||||
isFirstCollection: true,
|
||||
receivedMedia: 0,
|
||||
receivedAudioMedia: 0,
|
||||
receivedVideoMedia: 0,
|
||||
audioTrackSummary: {
|
||||
count: 0,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 0,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
});
|
||||
expect(collector.getActive()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("if not active do not calculate stats reports", async () => {
|
||||
collector.setActive(false);
|
||||
const getStats = jest.spyOn(rtcSpy, "getStats");
|
||||
await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID");
|
||||
expect(getStats).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("if get reports fails, the collector becomes inactive", async () => {
|
||||
expect(collector.getActive()).toBeTruthy();
|
||||
const getStats = jest.spyOn(rtcSpy, "getStats");
|
||||
getStats.mockRejectedValue(new Error("unknown"));
|
||||
await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID");
|
||||
expect(getStats).toHaveBeenCalled();
|
||||
expect(collector.getActive()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("if active and getStats returns not an RTCStatsReport inside a promise the collector fails and becomes inactive", async () => {
|
||||
const getStats = jest.spyOn(rtcSpy, "getStats");
|
||||
// @ts-ignore
|
||||
getStats.mockReturnValue({});
|
||||
const actual = await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID");
|
||||
expect(actual).toEqual({
|
||||
isFirstCollection: true,
|
||||
receivedMedia: 0,
|
||||
receivedAudioMedia: 0,
|
||||
receivedVideoMedia: 0,
|
||||
audioTrackSummary: {
|
||||
count: 0,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 0,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
});
|
||||
expect(getStats).toHaveBeenCalled();
|
||||
expect(collector.getActive()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("if active and the collector runs not the first time the Summery Stats is marked as not fits collection", async () => {
|
||||
const getStats = jest.spyOn(rtcSpy, "getStats");
|
||||
// @ts-ignore
|
||||
collector.previousStatsReport = {} as RTCStatsReport;
|
||||
const report = {} as RTCStatsReport;
|
||||
report.forEach = jest.fn().mockReturnValue([]);
|
||||
getStats.mockResolvedValue(report);
|
||||
const actual = await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID");
|
||||
expect(getStats).toHaveBeenCalled();
|
||||
expect(actual).toEqual({
|
||||
isFirstCollection: false,
|
||||
receivedMedia: 0,
|
||||
receivedAudioMedia: 0,
|
||||
receivedVideoMedia: 0,
|
||||
audioTrackSummary: {
|
||||
count: 0,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 0,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
});
|
||||
expect(collector.getActive()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("on signal state change event", () => {
|
||||
let events: { [key: string]: any };
|
||||
beforeEach(() => {
|
||||
events = [];
|
||||
// Define the addEventListener method with a Jest mock function
|
||||
rtcSpy.addEventListener = jest.fn((event: any, callback: any) => {
|
||||
events[event] = callback;
|
||||
});
|
||||
|
||||
collector = new CallStatsReportGatherer(CALL_ID, USER_ID, rtcSpy, emitter);
|
||||
});
|
||||
it("in case of stable, parse remote and local description", () => {
|
||||
// @ts-ignore
|
||||
const mediaSsrcHandler = {
|
||||
parse: jest.fn(),
|
||||
ssrcToMid: jest.fn(),
|
||||
findMidBySsrc: jest.fn(),
|
||||
getSsrcToMidMap: jest.fn(),
|
||||
} as MediaSsrcHandler;
|
||||
|
||||
const remoteSDP = "sdp";
|
||||
const localSDP = "sdp";
|
||||
|
||||
// @ts-ignore
|
||||
rtcSpy.signalingState = "stable";
|
||||
|
||||
// @ts-ignore
|
||||
rtcSpy.currentRemoteDescription = <RTCSessionDescription>{ sdp: remoteSDP };
|
||||
// @ts-ignore
|
||||
rtcSpy.currentLocalDescription = <RTCSessionDescription>{ sdp: localSDP };
|
||||
|
||||
// @ts-ignore
|
||||
collector.trackStats.mediaSsrcHandler = mediaSsrcHandler;
|
||||
|
||||
events["signalingstatechange"]();
|
||||
expect(mediaSsrcHandler.parse).toHaveBeenCalledWith(remoteSDP, "remote");
|
||||
expect(mediaSsrcHandler.parse).toHaveBeenCalledWith(localSDP, "local");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
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 { TrackID } from "../../../../src/webrtc/stats/statsReport";
|
||||
import { MediaTrackStats } from "../../../../src/webrtc/stats/media/mediaTrackStats";
|
||||
import { ConnectionStatsReportBuilder } from "../../../../src/webrtc/stats/connectionStatsReportBuilder";
|
||||
|
||||
describe("StatsReportBuilder", () => {
|
||||
const LOCAL_VIDEO_TRACK_ID = "LOCAL_VIDEO_TRACK_ID";
|
||||
const LOCAL_AUDIO_TRACK_ID = "LOCAL_AUDIO_TRACK_ID";
|
||||
const REMOTE_AUDIO_TRACK_ID = "REMOTE_AUDIO_TRACK_ID";
|
||||
const REMOTE_VIDEO_TRACK_ID = "REMOTE_VIDEO_TRACK_ID";
|
||||
const localAudioTrack = new MediaTrackStats(LOCAL_AUDIO_TRACK_ID, "local", "audio");
|
||||
const localVideoTrack = new MediaTrackStats(LOCAL_VIDEO_TRACK_ID, "local", "video");
|
||||
const remoteAudioTrack = new MediaTrackStats(REMOTE_AUDIO_TRACK_ID, "remote", "audio");
|
||||
const remoteVideoTrack = new MediaTrackStats(REMOTE_VIDEO_TRACK_ID, "remote", "video");
|
||||
const stats = new Map<TrackID, MediaTrackStats>([
|
||||
[LOCAL_AUDIO_TRACK_ID, localAudioTrack],
|
||||
[LOCAL_VIDEO_TRACK_ID, localVideoTrack],
|
||||
[REMOTE_AUDIO_TRACK_ID, remoteAudioTrack],
|
||||
[REMOTE_VIDEO_TRACK_ID, remoteVideoTrack],
|
||||
]);
|
||||
beforeEach(() => {
|
||||
buildData();
|
||||
});
|
||||
|
||||
describe("should build stats", () => {
|
||||
it("by media track stats.", async () => {
|
||||
expect(ConnectionStatsReportBuilder.build(stats)).toEqual({
|
||||
bitrate: {
|
||||
audio: {
|
||||
download: 4000,
|
||||
upload: 5000,
|
||||
},
|
||||
download: 5004000,
|
||||
upload: 3005000,
|
||||
video: {
|
||||
download: 5000000,
|
||||
upload: 3000000,
|
||||
},
|
||||
},
|
||||
codec: {
|
||||
local: new Map([
|
||||
["LOCAL_AUDIO_TRACK_ID", "opus"],
|
||||
["LOCAL_VIDEO_TRACK_ID", "v8"],
|
||||
]),
|
||||
remote: new Map([
|
||||
["REMOTE_AUDIO_TRACK_ID", "opus"],
|
||||
["REMOTE_VIDEO_TRACK_ID", "v9"],
|
||||
]),
|
||||
},
|
||||
framerate: {
|
||||
local: new Map([
|
||||
["LOCAL_AUDIO_TRACK_ID", 0],
|
||||
["LOCAL_VIDEO_TRACK_ID", 30],
|
||||
]),
|
||||
remote: new Map([
|
||||
["REMOTE_AUDIO_TRACK_ID", 0],
|
||||
["REMOTE_VIDEO_TRACK_ID", 60],
|
||||
]),
|
||||
},
|
||||
packetLoss: {
|
||||
download: 7,
|
||||
total: 15,
|
||||
upload: 28,
|
||||
},
|
||||
resolution: {
|
||||
local: new Map([
|
||||
["LOCAL_AUDIO_TRACK_ID", { height: -1, width: -1 }],
|
||||
["LOCAL_VIDEO_TRACK_ID", { height: 460, width: 780 }],
|
||||
]),
|
||||
remote: new Map([
|
||||
["REMOTE_AUDIO_TRACK_ID", { height: -1, width: -1 }],
|
||||
["REMOTE_VIDEO_TRACK_ID", { height: 960, width: 1080 }],
|
||||
]),
|
||||
},
|
||||
jitter: new Map([
|
||||
["REMOTE_AUDIO_TRACK_ID", 0.1],
|
||||
["REMOTE_VIDEO_TRACK_ID", 50],
|
||||
]),
|
||||
audioConcealment: new Map([
|
||||
["REMOTE_AUDIO_TRACK_ID", { concealedAudio: 3000, totalAudioDuration: 3000 * 20 }],
|
||||
]),
|
||||
totalAudioConcealment: {
|
||||
concealedAudio: 3000,
|
||||
totalAudioDuration: (1 / 0.05) * 3000,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const buildData = (): void => {
|
||||
localAudioTrack.setCodec("opus");
|
||||
localAudioTrack.setLoss({ packetsTotal: 10, packetsLost: 5, isDownloadStream: false });
|
||||
localAudioTrack.setBitrate({ download: 0, upload: 5000 });
|
||||
|
||||
remoteAudioTrack.setCodec("opus");
|
||||
remoteAudioTrack.setLoss({ packetsTotal: 20, packetsLost: 0, isDownloadStream: true });
|
||||
remoteAudioTrack.setBitrate({ download: 4000, upload: 0 });
|
||||
remoteAudioTrack.setJitter(0.1);
|
||||
remoteAudioTrack.setAudioConcealment(3000, 3000 * 20);
|
||||
|
||||
localVideoTrack.setCodec("v8");
|
||||
localVideoTrack.setLoss({ packetsTotal: 30, packetsLost: 6, isDownloadStream: false });
|
||||
localVideoTrack.setBitrate({ download: 0, upload: 3000000 });
|
||||
localVideoTrack.setFramerate(30);
|
||||
localVideoTrack.setResolution({ width: 780, height: 460 });
|
||||
|
||||
remoteVideoTrack.setCodec("v9");
|
||||
remoteVideoTrack.setLoss({ packetsTotal: 40, packetsLost: 4, isDownloadStream: true });
|
||||
remoteVideoTrack.setBitrate({ download: 5000000, upload: 0 });
|
||||
remoteVideoTrack.setFramerate(60);
|
||||
remoteVideoTrack.setResolution({ width: 1080, height: 960 });
|
||||
remoteVideoTrack.setJitter(50);
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
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 { ConnectionStatsBuilder } from "../../../../src/webrtc/stats/connectionStatsBuilder";
|
||||
|
||||
describe("ConnectionStatsReporter", () => {
|
||||
describe("should on bandwidth stats", () => {
|
||||
it("build bandwidth report if chromium starts attributes available", () => {
|
||||
const stats = {
|
||||
availableIncomingBitrate: 1000,
|
||||
availableOutgoingBitrate: 2000,
|
||||
} as RTCIceCandidatePairStats;
|
||||
expect(ConnectionStatsBuilder.buildBandwidthReport(stats)).toEqual({ download: 1, upload: 2 });
|
||||
});
|
||||
it("build empty bandwidth report if chromium starts attributes not available", () => {
|
||||
const stats = {} as RTCIceCandidatePairStats;
|
||||
expect(ConnectionStatsBuilder.buildBandwidthReport(stats)).toEqual({ download: 0, upload: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("should on connection stats", () => {
|
||||
it("build bandwidth report if chromium starts attributes available", () => {
|
||||
const stats = {
|
||||
availableIncomingBitrate: 1000,
|
||||
availableOutgoingBitrate: 2000,
|
||||
} as RTCIceCandidatePairStats;
|
||||
expect(ConnectionStatsBuilder.buildBandwidthReport(stats)).toEqual({ download: 1, upload: 2 });
|
||||
});
|
||||
it("build empty bandwidth report if chromium starts attributes not available", () => {
|
||||
const stats = {} as RTCIceCandidatePairStats;
|
||||
expect(ConnectionStatsBuilder.buildBandwidthReport(stats)).toEqual({ download: 0, upload: 0 });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
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 { GroupCallStats } from "../../../../src/webrtc/stats/groupCallStats";
|
||||
import { CallStatsReportSummary } from "../../../../src/webrtc/stats/callStatsReportSummary";
|
||||
|
||||
const GROUP_CALL_ID = "GROUP_ID";
|
||||
const LOCAL_USER_ID = "LOCAL_USER_ID";
|
||||
const TIME_INTERVAL = 10000;
|
||||
|
||||
describe("GroupCallStats", () => {
|
||||
let stats: GroupCallStats;
|
||||
beforeEach(() => {
|
||||
stats = new GroupCallStats(GROUP_CALL_ID, LOCAL_USER_ID, TIME_INTERVAL);
|
||||
});
|
||||
|
||||
describe("should on adding a stats collector", () => {
|
||||
it("creating a new one if not existing.", async () => {
|
||||
expect(stats.addStatsReportGatherer("CALL_ID", "USER_ID", mockRTCPeerConnection())).toBeTruthy();
|
||||
});
|
||||
|
||||
it("creating only one when trying add the same collector multiple times.", async () => {
|
||||
expect(stats.addStatsReportGatherer("CALL_ID", "USER_ID", mockRTCPeerConnection())).toBeTruthy();
|
||||
expect(stats.addStatsReportGatherer("CALL_ID", "USER_ID", mockRTCPeerConnection())).toBeFalsy();
|
||||
// The User ID is not relevant! Because for stats the call is needed and the user id is for monitoring
|
||||
expect(stats.addStatsReportGatherer("CALL_ID", "SOME_OTHER_USER_ID", mockRTCPeerConnection())).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("should on removing a stats collector", () => {
|
||||
it("returning `true` if the collector exists", async () => {
|
||||
expect(stats.addStatsReportGatherer("CALL_ID", "USER_ID", mockRTCPeerConnection())).toBeTruthy();
|
||||
expect(stats.removeStatsReportGatherer("CALL_ID")).toBeTruthy();
|
||||
});
|
||||
it("returning false if the collector not exists", async () => {
|
||||
expect(stats.removeStatsReportGatherer("CALL_ID_NOT_EXIST")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("should on get stats collector", () => {
|
||||
it("returning `undefined` if collector not existing", async () => {
|
||||
expect(stats.getStatsReportGatherer("CALL_ID")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returning Collector if collector existing", async () => {
|
||||
expect(stats.addStatsReportGatherer("CALL_ID", "USER_ID", mockRTCPeerConnection())).toBeTruthy();
|
||||
expect(stats.getStatsReportGatherer("CALL_ID")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("should on start", () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("starting processing stats as well without stats collectors", async () => {
|
||||
// @ts-ignore
|
||||
stats.processStats = jest.fn();
|
||||
stats.start();
|
||||
jest.advanceTimersByTime(TIME_INTERVAL);
|
||||
// @ts-ignore
|
||||
expect(stats.processStats).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("not starting processing stats if interval 0", async () => {
|
||||
const statsDisabled = new GroupCallStats(GROUP_CALL_ID, LOCAL_USER_ID, 0);
|
||||
// @ts-ignore
|
||||
statsDisabled.processStats = jest.fn();
|
||||
statsDisabled.start();
|
||||
jest.advanceTimersByTime(TIME_INTERVAL);
|
||||
// @ts-ignore
|
||||
expect(statsDisabled.processStats).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("starting processing and calling the collectors", async () => {
|
||||
stats.addStatsReportGatherer("CALL_ID", "USER_ID", mockRTCPeerConnection());
|
||||
const collector = stats.getStatsReportGatherer("CALL_ID");
|
||||
stats.reports.emitSummaryStatsReport = jest.fn();
|
||||
const summaryStats = {
|
||||
isFirstCollection: true,
|
||||
receivedMedia: 0,
|
||||
receivedAudioMedia: 0,
|
||||
receivedVideoMedia: 0,
|
||||
audioTrackSummary: {
|
||||
count: 0,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 0,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
} as CallStatsReportSummary;
|
||||
let processStatsSpy;
|
||||
if (collector) {
|
||||
processStatsSpy = jest.spyOn(collector, "processStats").mockResolvedValue(summaryStats);
|
||||
stats.start();
|
||||
jest.advanceTimersByTime(TIME_INTERVAL);
|
||||
} else {
|
||||
throw new Error("Test failed, because no Collector found!");
|
||||
}
|
||||
expect(processStatsSpy).toHaveBeenCalledWith(GROUP_CALL_ID, LOCAL_USER_ID);
|
||||
});
|
||||
|
||||
it("doing nothing if process already running", async () => {
|
||||
// @ts-ignore
|
||||
jest.spyOn(global, "setInterval").mockReturnValue(22);
|
||||
stats.start();
|
||||
expect(setInterval).toHaveBeenCalledTimes(1);
|
||||
stats.start();
|
||||
stats.start();
|
||||
stats.start();
|
||||
stats.start();
|
||||
expect(setInterval).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("should on stop", () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
it("finish stats process if was started", async () => {
|
||||
// @ts-ignore
|
||||
jest.spyOn(global, "setInterval").mockReturnValue(22);
|
||||
jest.spyOn(global, "clearInterval");
|
||||
stats.start();
|
||||
expect(setInterval).toHaveBeenCalledTimes(1);
|
||||
stats.stop();
|
||||
expect(clearInterval).toHaveBeenCalledWith(22);
|
||||
});
|
||||
|
||||
it("do nothing if stats process was not started", async () => {
|
||||
jest.spyOn(global, "clearInterval");
|
||||
stats.stop();
|
||||
expect(clearInterval).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const mockRTCPeerConnection = (): RTCPeerConnection => {
|
||||
const pc = {} as RTCPeerConnection;
|
||||
pc.addEventListener = jest.fn();
|
||||
pc.getStats = jest.fn().mockResolvedValue(null);
|
||||
return pc;
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
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 { Mid, Ssrc, MediaSsrcHandler } from "../../../../../src/webrtc/stats/media/mediaSsrcHandler";
|
||||
import { REMOTE_SFU_DESCRIPTION } from "../../../../test-utils/webrtc";
|
||||
|
||||
describe("MediaSsrcHandler", () => {
|
||||
const remoteMap = new Map<Mid, Ssrc[]>([
|
||||
["0", ["2963372119"]],
|
||||
["1", ["1212931603"]],
|
||||
]);
|
||||
let handler: MediaSsrcHandler;
|
||||
beforeEach(() => {
|
||||
handler = new MediaSsrcHandler();
|
||||
});
|
||||
describe("should parse description", () => {
|
||||
it("and build mid ssrc map", async () => {
|
||||
handler.parse(REMOTE_SFU_DESCRIPTION, "remote");
|
||||
expect(handler.getSsrcToMidMap("remote")).toEqual(remoteMap);
|
||||
});
|
||||
});
|
||||
|
||||
describe("should on find mid by ssrc", () => {
|
||||
it("and return mid if mapping exists.", async () => {
|
||||
handler.parse(REMOTE_SFU_DESCRIPTION, "remote");
|
||||
expect(handler.findMidBySsrc("2963372119", "remote")).toEqual("0");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
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 { MediaTrackHandler } from "../../../../../src/webrtc/stats/media/mediaTrackHandler";
|
||||
|
||||
describe("TrackHandler", () => {
|
||||
let pc: RTCPeerConnection;
|
||||
let handler: MediaTrackHandler;
|
||||
beforeEach(() => {
|
||||
pc = {
|
||||
getTransceivers: (): RTCRtpTransceiver[] => [mockTransceiver("1", "audio"), mockTransceiver("2", "video")],
|
||||
} as RTCPeerConnection;
|
||||
handler = new MediaTrackHandler(pc);
|
||||
});
|
||||
describe("should get local tracks", () => {
|
||||
it("returns video track", async () => {
|
||||
expect(handler.getLocalTracks("video")).toEqual([
|
||||
{
|
||||
id: `sender-track-2`,
|
||||
kind: "video",
|
||||
} as MediaStreamTrack,
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns audio track", async () => {
|
||||
expect(handler.getLocalTracks("audio")).toEqual([
|
||||
{
|
||||
id: `sender-track-1`,
|
||||
kind: "audio",
|
||||
} as MediaStreamTrack,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("should get local track by mid", () => {
|
||||
it("returns video track", async () => {
|
||||
expect(handler.getLocalTrackIdByMid("2")).toEqual("sender-track-2");
|
||||
});
|
||||
|
||||
it("returns audio track", async () => {
|
||||
expect(handler.getLocalTrackIdByMid("1")).toEqual("sender-track-1");
|
||||
});
|
||||
|
||||
it("returns undefined if not exists", async () => {
|
||||
expect(handler.getLocalTrackIdByMid("3")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("should get remote track by mid", () => {
|
||||
it("returns video track", async () => {
|
||||
expect(handler.getRemoteTrackIdByMid("2")).toEqual("receiver-track-2");
|
||||
});
|
||||
|
||||
it("returns audio track", async () => {
|
||||
expect(handler.getRemoteTrackIdByMid("1")).toEqual("receiver-track-1");
|
||||
});
|
||||
|
||||
it("returns undefined if not exists", async () => {
|
||||
expect(handler.getRemoteTrackIdByMid("3")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("should get track by id", () => {
|
||||
it("returns remote track", async () => {
|
||||
expect(handler.getTackById("receiver-track-2")).toEqual({
|
||||
id: `receiver-track-2`,
|
||||
kind: "video",
|
||||
} as MediaStreamTrack);
|
||||
});
|
||||
|
||||
it("returns local track", async () => {
|
||||
expect(handler.getTackById("sender-track-1")).toEqual({
|
||||
id: `sender-track-1`,
|
||||
kind: "audio",
|
||||
} as MediaStreamTrack);
|
||||
});
|
||||
|
||||
it("returns undefined if not exists", async () => {
|
||||
expect(handler.getTackById("sender-track-3")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("should get simulcast track count", () => {
|
||||
it("returns 2", async () => {
|
||||
expect(handler.getActiveSimulcastStreams()).toEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("should get Transceiver by Track ID", () => {
|
||||
it("and returns remote Transceiver if exits", async () => {
|
||||
expect(handler.getTransceiverByTrackId(`receiver-track-1`)?.mid).toEqual("1");
|
||||
});
|
||||
|
||||
it("and returns local Transceiver if exits", async () => {
|
||||
expect(handler.getTransceiverByTrackId(`sender-track-2`)?.mid).toEqual("2");
|
||||
});
|
||||
|
||||
it("returns undefined if Transceiver not exits", async () => {
|
||||
expect(handler.getTransceiverByTrackId("22")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const mockTransceiver = (mid: string, kind: "video" | "audio"): RTCRtpTransceiver => {
|
||||
return {
|
||||
mid,
|
||||
currentDirection: "sendrecv",
|
||||
sender: {
|
||||
track: { id: `sender-track-${mid}`, kind } as MediaStreamTrack,
|
||||
} as RTCRtpSender,
|
||||
receiver: {
|
||||
track: { id: `receiver-track-${mid}`, kind } as MediaStreamTrack,
|
||||
} as RTCRtpReceiver,
|
||||
} as RTCRtpTransceiver;
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
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 { MediaTrackHandler } from "../../../../../src/webrtc/stats/media/mediaTrackHandler";
|
||||
import { MediaTrackStatsHandler } from "../../../../../src/webrtc/stats/media/mediaTrackStatsHandler";
|
||||
import { MediaSsrcHandler } from "../../../../../src/webrtc/stats/media/mediaSsrcHandler";
|
||||
|
||||
describe("MediaTrackStatsHandler", () => {
|
||||
let statsHandler: MediaTrackStatsHandler;
|
||||
let ssrcHandler: MediaSsrcHandler;
|
||||
let trackHandler: MediaTrackHandler;
|
||||
beforeEach(() => {
|
||||
ssrcHandler = {} as MediaSsrcHandler;
|
||||
trackHandler = {} as MediaTrackHandler;
|
||||
trackHandler.getLocalTrackIdByMid = jest.fn().mockReturnValue("2222");
|
||||
trackHandler.getRemoteTrackIdByMid = jest.fn().mockReturnValue("5555");
|
||||
trackHandler.getLocalTracks = jest.fn().mockReturnValue([{ id: "2222" } as MediaStreamTrack]);
|
||||
trackHandler.getTackById = jest.fn().mockReturnValue([{ id: "2222", kind: "audio" } as MediaStreamTrack]);
|
||||
statsHandler = new MediaTrackStatsHandler(ssrcHandler, trackHandler);
|
||||
});
|
||||
describe("should find track stats", () => {
|
||||
it("and returns stats if `trackIdentifier` exists in report", () => {
|
||||
const report = { trackIdentifier: "123" };
|
||||
expect(statsHandler.findTrack2Stats(report, "remote")?.trackId).toEqual("123");
|
||||
});
|
||||
it("and returns stats if `mid` exists in report", () => {
|
||||
const reportIn = { mid: "1", type: "inbound-rtp" };
|
||||
expect(statsHandler.findTrack2Stats(reportIn, "remote")?.trackId).toEqual("5555");
|
||||
const reportOut = { mid: "1", type: "outbound-rtp" };
|
||||
expect(statsHandler.findTrack2Stats(reportOut, "local")?.trackId).toEqual("2222");
|
||||
});
|
||||
it("and returns undefined if `ssrc` exists in report but not on connection", () => {
|
||||
const report = { ssrc: "142443", type: "inbound-rtp" };
|
||||
ssrcHandler.findMidBySsrc = jest.fn().mockReturnValue(undefined);
|
||||
expect(statsHandler.findTrack2Stats(report, "local")?.trackId).toBeUndefined();
|
||||
});
|
||||
it("and returns undefined if `ssrc` exists in inbound-rtp report", () => {
|
||||
const report = { ssrc: "142443", type: "inbound-rtp" };
|
||||
ssrcHandler.findMidBySsrc = jest.fn().mockReturnValue("2");
|
||||
expect(statsHandler.findTrack2Stats(report, "remote")?.trackId).toEqual("5555");
|
||||
});
|
||||
it("and returns undefined if `ssrc` exists in outbound-rtp report", () => {
|
||||
const report = { ssrc: "142443", type: "outbound-rtp" };
|
||||
ssrcHandler.findMidBySsrc = jest.fn().mockReturnValue("2");
|
||||
expect(statsHandler.findTrack2Stats(report, "local")?.trackId).toEqual("2222");
|
||||
});
|
||||
it("and returns undefined if needed property not existing", () => {
|
||||
const report = {};
|
||||
expect(statsHandler.findTrack2Stats(report, "remote")?.trackId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
describe("should find local video track stats", () => {
|
||||
it("and returns stats if `trackIdentifier` exists in report", async () => {
|
||||
const report = { trackIdentifier: "2222" };
|
||||
expect(statsHandler.findLocalVideoTrackStats(report)?.trackId).toEqual("2222");
|
||||
});
|
||||
it("and returns stats if `mid` exists in report", () => {
|
||||
const report = { mid: "1" };
|
||||
expect(statsHandler.findLocalVideoTrackStats(report)?.trackId).toEqual("2222");
|
||||
});
|
||||
it("and returns undefined if `ssrc` exists", () => {
|
||||
const report = { ssrc: "142443", type: "outbound-rtp" };
|
||||
ssrcHandler.findMidBySsrc = jest.fn().mockReturnValue("2");
|
||||
expect(statsHandler.findTrack2Stats(report, "local")?.trackId).toEqual("2222");
|
||||
});
|
||||
it("and returns undefined if needed property not existing", async () => {
|
||||
const report = {};
|
||||
expect(statsHandler.findTrack2Stats(report, "remote")?.trackId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("should find a Transceiver by Track id", () => {
|
||||
it("and returns undefined if Transceiver not existing", async () => {
|
||||
trackHandler.getTransceiverByTrackId = jest.fn().mockReturnValue(undefined);
|
||||
expect(statsHandler.findTransceiverByTrackId("12")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("and returns Transceiver if existing", async () => {
|
||||
const ts = {} as RTCRtpTransceiver;
|
||||
trackHandler.getTransceiverByTrackId = jest.fn().mockReturnValue(ts);
|
||||
expect(statsHandler.findTransceiverByTrackId("12")).toEqual(ts);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
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 { StatsReportEmitter } from "../../../../src/webrtc/stats/statsReportEmitter";
|
||||
import {
|
||||
ByteSentStatsReport,
|
||||
ConnectionStatsReport,
|
||||
StatsReport,
|
||||
SummaryStatsReport,
|
||||
} from "../../../../src/webrtc/stats/statsReport";
|
||||
|
||||
describe("StatsReportEmitter", () => {
|
||||
let emitter: StatsReportEmitter;
|
||||
beforeEach(() => {
|
||||
emitter = new StatsReportEmitter();
|
||||
});
|
||||
|
||||
it("should emit and receive ByteSendStatsReport", async () => {
|
||||
const report = {} as ByteSentStatsReport;
|
||||
return new Promise((resolve, _) => {
|
||||
emitter.on(StatsReport.BYTE_SENT_STATS, (r) => {
|
||||
expect(r).toBe(report);
|
||||
resolve(null);
|
||||
return;
|
||||
});
|
||||
emitter.emitByteSendReport(report);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit and receive ConnectionStatsReport", async () => {
|
||||
const report = {} as ConnectionStatsReport;
|
||||
return new Promise((resolve, _) => {
|
||||
emitter.on(StatsReport.CONNECTION_STATS, (r) => {
|
||||
expect(r).toBe(report);
|
||||
resolve(null);
|
||||
return;
|
||||
});
|
||||
emitter.emitConnectionStatsReport(report);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit and receive SummaryStatsReport", async () => {
|
||||
const report = {} as SummaryStatsReport;
|
||||
return new Promise((resolve, _) => {
|
||||
emitter.on(StatsReport.SUMMARY_STATS, (r) => {
|
||||
expect(r).toBe(report);
|
||||
resolve(null);
|
||||
return;
|
||||
});
|
||||
emitter.emitSummaryStatsReport(report);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,592 @@
|
||||
/*
|
||||
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 { SummaryStatsReportGatherer } from "../../../../src/webrtc/stats/summaryStatsReportGatherer";
|
||||
import { StatsReportEmitter } from "../../../../src/webrtc/stats/statsReportEmitter";
|
||||
|
||||
describe("SummaryStatsReportGatherer", () => {
|
||||
let reporter: SummaryStatsReportGatherer;
|
||||
let emitter: StatsReportEmitter;
|
||||
beforeEach(() => {
|
||||
emitter = new StatsReportEmitter();
|
||||
emitter.emitSummaryStatsReport = jest.fn();
|
||||
reporter = new SummaryStatsReportGatherer(emitter);
|
||||
});
|
||||
|
||||
describe("build Summary Stats Report", () => {
|
||||
it("should do nothing if summary list empty", async () => {
|
||||
reporter.build([]);
|
||||
expect(emitter.emitSummaryStatsReport).not.toHaveBeenCalled();
|
||||
});
|
||||
it("should do nothing if a summary stats element collection the is first time", async () => {
|
||||
reporter.build([
|
||||
{
|
||||
isFirstCollection: true,
|
||||
receivedMedia: 10,
|
||||
receivedAudioMedia: 4,
|
||||
receivedVideoMedia: 6,
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 100,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(emitter.emitSummaryStatsReport).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should trigger new summary report", async () => {
|
||||
const summary = [
|
||||
{
|
||||
isFirstCollection: false,
|
||||
receivedMedia: 10,
|
||||
receivedAudioMedia: 4,
|
||||
receivedVideoMedia: 6,
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 100,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
isFirstCollection: false,
|
||||
receivedMedia: 13,
|
||||
receivedAudioMedia: 0,
|
||||
receivedVideoMedia: 13,
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 5,
|
||||
totalAudio: 100,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
isFirstCollection: false,
|
||||
receivedMedia: 0,
|
||||
receivedAudioMedia: 0,
|
||||
receivedVideoMedia: 0,
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 10,
|
||||
totalAudio: 100,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
isFirstCollection: false,
|
||||
receivedMedia: 15,
|
||||
receivedAudioMedia: 6,
|
||||
receivedVideoMedia: 9,
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 100,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
reporter.build(summary);
|
||||
expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({
|
||||
percentageReceivedMedia: 0.5,
|
||||
percentageReceivedAudioMedia: 0.5,
|
||||
percentageReceivedVideoMedia: 0.75,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
peerConnections: 4,
|
||||
percentageConcealedAudio: 0.0375,
|
||||
});
|
||||
});
|
||||
|
||||
it("as received video Media, although video was not received, but because video muted", async () => {
|
||||
const summary = [
|
||||
{
|
||||
isFirstCollection: false,
|
||||
receivedMedia: 10,
|
||||
receivedAudioMedia: 10,
|
||||
receivedVideoMedia: 0,
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 1,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
reporter.build(summary);
|
||||
expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({
|
||||
percentageReceivedMedia: 1,
|
||||
percentageReceivedAudioMedia: 1,
|
||||
percentageReceivedVideoMedia: 1,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
peerConnections: 1,
|
||||
percentageConcealedAudio: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("as received no video Media, because only on video was muted", async () => {
|
||||
const summary = [
|
||||
{
|
||||
isFirstCollection: false,
|
||||
receivedMedia: 10,
|
||||
receivedAudioMedia: 10,
|
||||
receivedVideoMedia: 0,
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 2,
|
||||
muted: 1,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
reporter.build(summary);
|
||||
expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({
|
||||
percentageReceivedMedia: 0,
|
||||
percentageReceivedAudioMedia: 1,
|
||||
percentageReceivedVideoMedia: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
peerConnections: 1,
|
||||
percentageConcealedAudio: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("as received no audio Media, although audio not received and audio muted", async () => {
|
||||
const summary = [
|
||||
{
|
||||
isFirstCollection: false,
|
||||
receivedMedia: 100,
|
||||
receivedAudioMedia: 0,
|
||||
receivedVideoMedia: 100,
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 1,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
reporter.build(summary);
|
||||
expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({
|
||||
percentageReceivedMedia: 0,
|
||||
percentageReceivedAudioMedia: 0,
|
||||
percentageReceivedVideoMedia: 1,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
peerConnections: 1,
|
||||
percentageConcealedAudio: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("should find max jitter and max packet loss", async () => {
|
||||
const summary = [
|
||||
{
|
||||
isFirstCollection: false,
|
||||
receivedMedia: 1,
|
||||
receivedAudioMedia: 1,
|
||||
receivedVideoMedia: 1,
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
isFirstCollection: false,
|
||||
receivedMedia: 1,
|
||||
receivedAudioMedia: 1,
|
||||
receivedVideoMedia: 1,
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 20,
|
||||
maxPacketLoss: 5,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
isFirstCollection: false,
|
||||
receivedMedia: 1,
|
||||
receivedAudioMedia: 1,
|
||||
receivedVideoMedia: 1,
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 2,
|
||||
maxPacketLoss: 5,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 2,
|
||||
maxPacketLoss: 5,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
isFirstCollection: false,
|
||||
receivedMedia: 1,
|
||||
receivedAudioMedia: 1,
|
||||
receivedVideoMedia: 1,
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 2,
|
||||
maxPacketLoss: 5,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 40,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
reporter.build(summary);
|
||||
expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({
|
||||
percentageReceivedMedia: 1,
|
||||
percentageReceivedAudioMedia: 1,
|
||||
percentageReceivedVideoMedia: 1,
|
||||
maxJitter: 20,
|
||||
maxPacketLoss: 40,
|
||||
peerConnections: 4,
|
||||
percentageConcealedAudio: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("as received video Media, if no audio track received should count as received Media", async () => {
|
||||
const summary = [
|
||||
{
|
||||
isFirstCollection: false,
|
||||
receivedMedia: 10,
|
||||
receivedAudioMedia: 0,
|
||||
receivedVideoMedia: 10,
|
||||
audioTrackSummary: {
|
||||
count: 0,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
reporter.build(summary);
|
||||
expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({
|
||||
percentageReceivedMedia: 1,
|
||||
percentageReceivedAudioMedia: 1,
|
||||
percentageReceivedVideoMedia: 1,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
peerConnections: 1,
|
||||
percentageConcealedAudio: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("as received audio Media, if no video track received should count as received Media", async () => {
|
||||
const summary = [
|
||||
{
|
||||
isFirstCollection: false,
|
||||
receivedMedia: 1,
|
||||
receivedAudioMedia: 22,
|
||||
receivedVideoMedia: 0,
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 0,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
reporter.build(summary);
|
||||
expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({
|
||||
percentageReceivedMedia: 1,
|
||||
percentageReceivedAudioMedia: 1,
|
||||
percentageReceivedVideoMedia: 1,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
peerConnections: 1,
|
||||
percentageConcealedAudio: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("as received no media at all, as received Media", async () => {
|
||||
const summary = [
|
||||
{
|
||||
isFirstCollection: false,
|
||||
receivedMedia: 0,
|
||||
receivedAudioMedia: 0,
|
||||
receivedVideoMedia: 0,
|
||||
audioTrackSummary: {
|
||||
count: 0,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 0,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
reporter.build(summary);
|
||||
expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({
|
||||
percentageReceivedMedia: 1,
|
||||
percentageReceivedAudioMedia: 1,
|
||||
percentageReceivedVideoMedia: 1,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
peerConnections: 1,
|
||||
percentageConcealedAudio: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("should filter the first time summery stats", async () => {
|
||||
const summary = [
|
||||
{
|
||||
isFirstCollection: false,
|
||||
receivedMedia: 1,
|
||||
receivedAudioMedia: 1,
|
||||
receivedVideoMedia: 1,
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
isFirstCollection: true,
|
||||
receivedMedia: 1,
|
||||
receivedAudioMedia: 1,
|
||||
receivedVideoMedia: 1,
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 20,
|
||||
maxPacketLoss: 5,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
isFirstCollection: false,
|
||||
receivedMedia: 1,
|
||||
receivedAudioMedia: 1,
|
||||
receivedVideoMedia: 1,
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 2,
|
||||
maxPacketLoss: 5,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 2,
|
||||
maxPacketLoss: 5,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
isFirstCollection: false,
|
||||
receivedMedia: 1,
|
||||
receivedAudioMedia: 1,
|
||||
receivedVideoMedia: 1,
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 2,
|
||||
maxPacketLoss: 5,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 40,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
reporter.build(summary);
|
||||
expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({
|
||||
percentageReceivedMedia: 1,
|
||||
percentageReceivedAudioMedia: 1,
|
||||
percentageReceivedVideoMedia: 1,
|
||||
maxJitter: 2,
|
||||
maxPacketLoss: 40,
|
||||
peerConnections: 3,
|
||||
percentageConcealedAudio: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,405 @@
|
||||
/*
|
||||
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 { TrackStatsBuilder } from "../../../../src/webrtc/stats/trackStatsBuilder";
|
||||
import { MediaTrackStats } from "../../../../src/webrtc/stats/media/mediaTrackStats";
|
||||
|
||||
describe("TrackStatsBuilder", () => {
|
||||
describe("should on frame and resolution stats", () => {
|
||||
it("creating empty frame and resolution report, if no data available.", async () => {
|
||||
const trackStats = new MediaTrackStats("1", "local", "video");
|
||||
TrackStatsBuilder.buildFramerateResolution(trackStats, {});
|
||||
expect(trackStats.getFramerate()).toEqual(0);
|
||||
expect(trackStats.getResolution()).toEqual({ width: -1, height: -1 });
|
||||
});
|
||||
it("creating empty frame and resolution report.", async () => {
|
||||
const trackStats = new MediaTrackStats("1", "remote", "video");
|
||||
TrackStatsBuilder.buildFramerateResolution(trackStats, {
|
||||
framesPerSecond: 22.2,
|
||||
frameHeight: 180,
|
||||
frameWidth: 360,
|
||||
});
|
||||
expect(trackStats.getFramerate()).toEqual(22);
|
||||
expect(trackStats.getResolution()).toEqual({ width: 360, height: 180 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("should on simulcast", () => {
|
||||
it("creating simulcast framerate.", async () => {
|
||||
const trackStats = new MediaTrackStats("1", "local", "video");
|
||||
TrackStatsBuilder.calculateSimulcastFramerate(
|
||||
trackStats,
|
||||
{
|
||||
framesSent: 100,
|
||||
timestamp: 1678957001000,
|
||||
},
|
||||
{
|
||||
framesSent: 10,
|
||||
timestamp: 1678957000000,
|
||||
},
|
||||
3,
|
||||
);
|
||||
expect(trackStats.getFramerate()).toEqual(30);
|
||||
});
|
||||
});
|
||||
|
||||
describe("should on bytes received stats", () => {
|
||||
it("creating build bitrate received report.", async () => {
|
||||
const trackStats = new MediaTrackStats("1", "remote", "video");
|
||||
TrackStatsBuilder.buildBitrateReceived(
|
||||
trackStats,
|
||||
{
|
||||
bytesReceived: 2001000,
|
||||
timestamp: 1678957010,
|
||||
},
|
||||
{ bytesReceived: 2000000, timestamp: 1678957000 },
|
||||
);
|
||||
expect(trackStats.getBitrate()).toEqual({ download: 800, upload: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("should on bytes send stats", () => {
|
||||
it("creating build bitrate send report.", async () => {
|
||||
const trackStats = new MediaTrackStats("1", "local", "video");
|
||||
TrackStatsBuilder.buildBitrateSend(
|
||||
trackStats,
|
||||
{
|
||||
bytesSent: 2001000,
|
||||
timestamp: 1678957010,
|
||||
},
|
||||
{ bytesSent: 2000000, timestamp: 1678957000 },
|
||||
);
|
||||
expect(trackStats.getBitrate()).toEqual({ download: 0, upload: 800 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("should on codec stats", () => {
|
||||
it("creating build bitrate send report.", async () => {
|
||||
const trackStats = new MediaTrackStats("1", "remote", "video");
|
||||
const remote = {} as RTCStatsReport;
|
||||
remote.get = jest.fn().mockReturnValue({ mimeType: "video/v8" });
|
||||
TrackStatsBuilder.buildCodec(remote, trackStats, { codecId: "codecID" });
|
||||
expect(trackStats.getCodec()).toEqual("v8");
|
||||
});
|
||||
});
|
||||
|
||||
describe("should on package lost stats", () => {
|
||||
it("creating build package lost on send report.", async () => {
|
||||
const trackStats = new MediaTrackStats("1", "local", "video");
|
||||
TrackStatsBuilder.buildPacketsLost(
|
||||
trackStats,
|
||||
{
|
||||
type: "outbound-rtp",
|
||||
packetsSent: 200,
|
||||
packetsLost: 120,
|
||||
},
|
||||
{
|
||||
packetsSent: 100,
|
||||
packetsLost: 30,
|
||||
},
|
||||
);
|
||||
expect(trackStats.getLoss()).toEqual({ packetsTotal: 190, packetsLost: 90, isDownloadStream: false });
|
||||
});
|
||||
it("creating build package lost on received report.", async () => {
|
||||
const trackStats = new MediaTrackStats("1", "remote", "video");
|
||||
TrackStatsBuilder.buildPacketsLost(
|
||||
trackStats,
|
||||
{
|
||||
type: "inbound-rtp",
|
||||
packetsReceived: 300,
|
||||
packetsLost: 100,
|
||||
},
|
||||
{
|
||||
packetsReceived: 100,
|
||||
packetsLost: 20,
|
||||
},
|
||||
);
|
||||
expect(trackStats.getLoss()).toEqual({ packetsTotal: 280, packetsLost: 80, isDownloadStream: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe("should set state of a TrackStats", () => {
|
||||
it("to not alive if Transceiver undefined", async () => {
|
||||
const trackStats = new MediaTrackStats("1", "remote", "video");
|
||||
TrackStatsBuilder.setTrackStatsState(trackStats, undefined);
|
||||
expect(trackStats.alive).toBeFalsy();
|
||||
});
|
||||
|
||||
it("to not alive if Transceiver has no local track", async () => {
|
||||
const trackStats = new MediaTrackStats("1", "local", "video");
|
||||
const ts = {
|
||||
sender: {
|
||||
track: null,
|
||||
} as RTCRtpSender,
|
||||
} as RTCRtpTransceiver;
|
||||
|
||||
TrackStatsBuilder.setTrackStatsState(trackStats, ts);
|
||||
expect(trackStats.alive).toBeFalsy();
|
||||
});
|
||||
|
||||
it("to alive if Transceiver remote and track is alive", async () => {
|
||||
const trackStats = new MediaTrackStats("1", "remote", "video");
|
||||
trackStats.alive = false;
|
||||
const ts = {
|
||||
receiver: {
|
||||
track: {
|
||||
readyState: "live",
|
||||
enabled: false,
|
||||
muted: false,
|
||||
} as MediaStreamTrack,
|
||||
} as RTCRtpReceiver,
|
||||
} as RTCRtpTransceiver;
|
||||
|
||||
TrackStatsBuilder.setTrackStatsState(trackStats, ts);
|
||||
expect(trackStats.alive).toBeTruthy();
|
||||
});
|
||||
|
||||
it("to alive if Transceiver local and track is live", async () => {
|
||||
const trackStats = new MediaTrackStats("1", "local", "video");
|
||||
trackStats.alive = false;
|
||||
const ts = {
|
||||
sender: {
|
||||
track: {
|
||||
readyState: "live",
|
||||
enabled: false,
|
||||
muted: false,
|
||||
} as MediaStreamTrack,
|
||||
} as RTCRtpSender,
|
||||
} as RTCRtpTransceiver;
|
||||
|
||||
TrackStatsBuilder.setTrackStatsState(trackStats, ts);
|
||||
expect(trackStats.alive).toBeTruthy();
|
||||
});
|
||||
|
||||
it("to not alive if Transceiver track is ended", async () => {
|
||||
const trackStats = new MediaTrackStats("1", "remote", "video");
|
||||
const ts = {
|
||||
receiver: {
|
||||
track: {
|
||||
readyState: "ended",
|
||||
enabled: false,
|
||||
muted: false,
|
||||
} as MediaStreamTrack,
|
||||
} as RTCRtpReceiver,
|
||||
} as RTCRtpTransceiver;
|
||||
|
||||
TrackStatsBuilder.setTrackStatsState(trackStats, ts);
|
||||
expect(trackStats.alive).toBeFalsy();
|
||||
});
|
||||
|
||||
it("to not alive and muted if Transceiver track is live and muted", async () => {
|
||||
const trackStats = new MediaTrackStats("1", "remote", "video");
|
||||
const ts = {
|
||||
receiver: {
|
||||
track: {
|
||||
readyState: "live",
|
||||
enabled: false,
|
||||
muted: true,
|
||||
} as MediaStreamTrack,
|
||||
} as RTCRtpReceiver,
|
||||
} as RTCRtpTransceiver;
|
||||
|
||||
TrackStatsBuilder.setTrackStatsState(trackStats, ts);
|
||||
expect(trackStats.alive).toBeTruthy();
|
||||
expect(trackStats.muted).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("should build Track Summary", () => {
|
||||
it("and returns empty summary if stats list empty", async () => {
|
||||
const summary = TrackStatsBuilder.buildTrackSummary([]);
|
||||
expect(summary).toEqual({
|
||||
audioTrackSummary: {
|
||||
count: 0,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 0,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("and returns summary if stats list not empty and ignore local summery", async () => {
|
||||
const trackStatsList = buildMockTrackStatsList();
|
||||
const summary = TrackStatsBuilder.buildTrackSummary(trackStatsList);
|
||||
expect(summary).toEqual({
|
||||
audioTrackSummary: {
|
||||
count: 2,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 3,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("and returns summary and count muted if alive", async () => {
|
||||
const trackStatsList = buildMockTrackStatsList();
|
||||
trackStatsList[1].muted = true;
|
||||
trackStatsList[5].muted = true;
|
||||
const summary = TrackStatsBuilder.buildTrackSummary(trackStatsList);
|
||||
expect(summary).toEqual({
|
||||
audioTrackSummary: {
|
||||
count: 2,
|
||||
muted: 1,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 3,
|
||||
muted: 1,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("and returns summary and ignore muted if not alive", async () => {
|
||||
const trackStatsList = buildMockTrackStatsList();
|
||||
trackStatsList[1].muted = true;
|
||||
trackStatsList[1].alive = false;
|
||||
const summary = TrackStatsBuilder.buildTrackSummary(trackStatsList);
|
||||
expect(summary).toEqual({
|
||||
audioTrackSummary: {
|
||||
count: 2,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 3,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("and returns summary and build max jitter, packet loss and audio conealment", async () => {
|
||||
const trackStatsList = buildMockTrackStatsList();
|
||||
// video remote
|
||||
trackStatsList[1].setJitter(12);
|
||||
trackStatsList[4].setJitter(66);
|
||||
trackStatsList[6].setJitter(1);
|
||||
trackStatsList[1].setLoss({ packetsLost: 55, packetsTotal: 0, isDownloadStream: true });
|
||||
trackStatsList[4].setLoss({ packetsLost: 0, packetsTotal: 0, isDownloadStream: true });
|
||||
trackStatsList[6].setLoss({ packetsLost: 1, packetsTotal: 0, isDownloadStream: true });
|
||||
// audio remote
|
||||
trackStatsList[2].setJitter(1);
|
||||
trackStatsList[5].setJitter(15);
|
||||
trackStatsList[2].setLoss({ packetsLost: 5, packetsTotal: 0, isDownloadStream: true });
|
||||
trackStatsList[5].setLoss({ packetsLost: 0, packetsTotal: 0, isDownloadStream: true });
|
||||
trackStatsList[2].setAudioConcealment(220, 2000);
|
||||
trackStatsList[5].setAudioConcealment(180, 2000);
|
||||
|
||||
const summary = TrackStatsBuilder.buildTrackSummary(trackStatsList);
|
||||
expect(summary).toEqual({
|
||||
audioTrackSummary: {
|
||||
count: 2,
|
||||
muted: 0,
|
||||
maxJitter: 15,
|
||||
maxPacketLoss: 5,
|
||||
concealedAudio: 400,
|
||||
totalAudio: 4000,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 3,
|
||||
muted: 0,
|
||||
maxJitter: 66,
|
||||
maxPacketLoss: 55,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("should build jitter value in Track Stats", () => {
|
||||
it("and returns track stats without jitter if report not 'inbound-rtp'", async () => {
|
||||
const trackStats = new MediaTrackStats("1", "remote", "video");
|
||||
TrackStatsBuilder.buildJitter(trackStats, { jitter: 0.01 });
|
||||
expect(trackStats.getJitter()).toEqual(0);
|
||||
});
|
||||
|
||||
it("and returns track stats with jitter", async () => {
|
||||
const trackStats = new MediaTrackStats("1", "remote", "video");
|
||||
TrackStatsBuilder.buildJitter(trackStats, { type: "inbound-rtp", jitter: 0.01 });
|
||||
expect(trackStats.getJitter()).toEqual(10);
|
||||
});
|
||||
|
||||
it("and returns negative jitter if stats has no jitter value", async () => {
|
||||
const trackStats = new MediaTrackStats("1", "remote", "video");
|
||||
TrackStatsBuilder.buildJitter(trackStats, { type: "inbound-rtp" });
|
||||
expect(trackStats.getJitter()).toEqual(-1);
|
||||
});
|
||||
|
||||
it("and returns jitter as number", async () => {
|
||||
const trackStats = new MediaTrackStats("1", "remote", "video");
|
||||
TrackStatsBuilder.buildJitter(trackStats, { type: "inbound-rtp", jitter: "0.5" });
|
||||
expect(trackStats.getJitter()).toEqual(500);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function buildMockTrackStatsList(): MediaTrackStats[] {
|
||||
const trackStats1 = new MediaTrackStats("1", "local", "video");
|
||||
trackStats1.muted = false;
|
||||
trackStats1.alive = true;
|
||||
const trackStats2 = new MediaTrackStats("1", "remote", "video");
|
||||
trackStats2.muted = false;
|
||||
trackStats2.alive = true;
|
||||
const trackStats3 = new MediaTrackStats("1", "remote", "audio");
|
||||
trackStats3.muted = false;
|
||||
trackStats3.alive = true;
|
||||
const trackStats4 = new MediaTrackStats("1", "local", "audio");
|
||||
trackStats4.muted = false;
|
||||
trackStats4.alive = true;
|
||||
const trackStats5 = new MediaTrackStats("1", "remote", "video");
|
||||
trackStats5.muted = false;
|
||||
trackStats5.alive = true;
|
||||
const trackStats6 = new MediaTrackStats("1", "remote", "audio");
|
||||
trackStats6.muted = false;
|
||||
trackStats6.alive = true;
|
||||
const trackStats7 = new MediaTrackStats("1", "remote", "video");
|
||||
trackStats7.muted = false;
|
||||
trackStats7.alive = true;
|
||||
return [trackStats1, trackStats2, trackStats3, trackStats4, trackStats5, trackStats6, trackStats7];
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
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 { TransportStatsBuilder } from "../../../../src/webrtc/stats/transportStatsBuilder";
|
||||
import { TransportStats } from "../../../../src/webrtc/stats/transportStats";
|
||||
|
||||
describe("TransportStatsReporter", () => {
|
||||
describe("should on build report", () => {
|
||||
const REMOTE_CANDIDATE_ID = "REMOTE_CANDIDATE_ID";
|
||||
const LOCAL_CANDIDATE_ID = "LOCAL_CANDIDATE_ID";
|
||||
const localIC = { ip: "88.88.99.1", port: 56670, protocol: "tcp", candidateType: "local", networkType: "lan" };
|
||||
const remoteIC = {
|
||||
ip: "123.88.99.1",
|
||||
port: 46670,
|
||||
protocol: "udp",
|
||||
candidateType: "srfx",
|
||||
networkType: "wifi",
|
||||
};
|
||||
const isFocus = false;
|
||||
const rtt = 200000;
|
||||
|
||||
it("build new transport stats if all properties there", () => {
|
||||
const { report, stats } = mockStatsReport(isFocus, 0);
|
||||
const conferenceStatsTransport: TransportStats[] = [];
|
||||
const transportStats = TransportStatsBuilder.buildReport(report, stats, conferenceStatsTransport, isFocus);
|
||||
expect(transportStats).toEqual([
|
||||
{
|
||||
ip: `${remoteIC.ip + 0}:${remoteIC.port}`,
|
||||
type: remoteIC.protocol,
|
||||
localIp: `${localIC.ip + 0}:${localIC.port}`,
|
||||
isFocus,
|
||||
localCandidateType: localIC.candidateType,
|
||||
remoteCandidateType: remoteIC.candidateType,
|
||||
networkType: localIC.networkType,
|
||||
rtt,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("build next transport stats if candidates different", () => {
|
||||
const mock1 = mockStatsReport(isFocus, 0);
|
||||
const mock2 = mockStatsReport(isFocus, 1);
|
||||
let transportStats: TransportStats[] = [];
|
||||
transportStats = TransportStatsBuilder.buildReport(mock1.report, mock1.stats, transportStats, isFocus);
|
||||
transportStats = TransportStatsBuilder.buildReport(mock2.report, mock2.stats, transportStats, isFocus);
|
||||
expect(transportStats).toEqual([
|
||||
{
|
||||
ip: `${remoteIC.ip + 0}:${remoteIC.port}`,
|
||||
type: remoteIC.protocol,
|
||||
localIp: `${localIC.ip + 0}:${localIC.port}`,
|
||||
isFocus,
|
||||
localCandidateType: localIC.candidateType,
|
||||
remoteCandidateType: remoteIC.candidateType,
|
||||
networkType: localIC.networkType,
|
||||
rtt,
|
||||
},
|
||||
{
|
||||
ip: `${remoteIC.ip + 1}:${remoteIC.port}`,
|
||||
type: remoteIC.protocol,
|
||||
localIp: `${localIC.ip + 1}:${localIC.port}`,
|
||||
isFocus,
|
||||
localCandidateType: localIC.candidateType,
|
||||
remoteCandidateType: remoteIC.candidateType,
|
||||
networkType: localIC.networkType,
|
||||
rtt,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("build not a second transport stats if candidates the same", () => {
|
||||
const mock1 = mockStatsReport(isFocus, 0);
|
||||
const mock2 = mockStatsReport(isFocus, 0);
|
||||
let transportStats: TransportStats[] = [];
|
||||
transportStats = TransportStatsBuilder.buildReport(mock1.report, mock1.stats, transportStats, isFocus);
|
||||
transportStats = TransportStatsBuilder.buildReport(mock2.report, mock2.stats, transportStats, isFocus);
|
||||
expect(transportStats).toEqual([
|
||||
{
|
||||
ip: `${remoteIC.ip + 0}:${remoteIC.port}`,
|
||||
type: remoteIC.protocol,
|
||||
localIp: `${localIC.ip + 0}:${localIC.port}`,
|
||||
isFocus,
|
||||
localCandidateType: localIC.candidateType,
|
||||
remoteCandidateType: remoteIC.candidateType,
|
||||
networkType: localIC.networkType,
|
||||
rtt,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
const mockStatsReport = (
|
||||
isFocus: boolean,
|
||||
prifix: number,
|
||||
): { report: RTCStatsReport; stats: RTCIceCandidatePairStats } => {
|
||||
const report = {} as RTCStatsReport;
|
||||
report.get = (key: string) => {
|
||||
if (key === LOCAL_CANDIDATE_ID) {
|
||||
return { ...localIC, ip: localIC.ip + prifix };
|
||||
}
|
||||
if (key === REMOTE_CANDIDATE_ID) {
|
||||
return { ...remoteIC, ip: remoteIC.ip + prifix };
|
||||
}
|
||||
// remote
|
||||
return {};
|
||||
};
|
||||
const stats = {
|
||||
remoteCandidateId: REMOTE_CANDIDATE_ID,
|
||||
localCandidateId: LOCAL_CANDIDATE_ID,
|
||||
currentRoundTripTime: 200,
|
||||
} as RTCIceCandidatePairStats;
|
||||
return { report, stats };
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
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 { ValueFormatter } from "../../../../src/webrtc/stats/valueFormatter";
|
||||
|
||||
describe("ValueFormatter", () => {
|
||||
describe("on get non negative values", () => {
|
||||
it("formatter shod return number", async () => {
|
||||
expect(ValueFormatter.getNonNegativeValue("2")).toEqual(2);
|
||||
expect(ValueFormatter.getNonNegativeValue(0)).toEqual(0);
|
||||
expect(ValueFormatter.getNonNegativeValue("-2")).toEqual(0);
|
||||
expect(ValueFormatter.getNonNegativeValue("")).toEqual(0);
|
||||
expect(ValueFormatter.getNonNegativeValue(NaN)).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -63,6 +63,7 @@ export function isDmMemberCountCondition(condition: AnyMemberCountCondition): bo
|
||||
export enum ConditionKind {
|
||||
EventMatch = "event_match",
|
||||
EventPropertyIs = "event_property_is",
|
||||
EventPropertyContains = "event_property_contains",
|
||||
ContainsDisplayName = "contains_display_name",
|
||||
RoomMemberCount = "room_member_count",
|
||||
SenderNotificationPermission = "sender_notification_permission",
|
||||
@@ -88,6 +89,11 @@ export interface IEventPropertyIsCondition extends IPushRuleCondition<ConditionK
|
||||
value: string | boolean | null | number;
|
||||
}
|
||||
|
||||
export interface IEventPropertyContainsCondition extends IPushRuleCondition<ConditionKind.EventPropertyContains> {
|
||||
key: string;
|
||||
value: string | boolean | null | number;
|
||||
}
|
||||
|
||||
export interface IContainsDisplayNameCondition extends IPushRuleCondition<ConditionKind.ContainsDisplayName> {
|
||||
// no additional fields
|
||||
}
|
||||
@@ -114,6 +120,7 @@ export interface ICallStartedPrefixCondition extends IPushRuleCondition<Conditio
|
||||
export type PushRuleCondition =
|
||||
| IEventMatchCondition
|
||||
| IEventPropertyIsCondition
|
||||
| IEventPropertyContainsCondition
|
||||
| IContainsDisplayNameCondition
|
||||
| IRoomMemberCountCondition
|
||||
| ISenderNotificationPermissionCondition
|
||||
@@ -130,6 +137,8 @@ export enum PushRuleKind {
|
||||
|
||||
export enum RuleId {
|
||||
Master = ".m.rule.master",
|
||||
IsUserMention = ".org.matrix.msc3952.is_user_mention",
|
||||
IsRoomMention = ".org.matrix.msc3952.is_room_mention",
|
||||
ContainsDisplayName = ".m.rule.contains_display_name",
|
||||
ContainsUserName = ".m.rule.contains_user_name",
|
||||
AtRoomNotification = ".m.rule.roomnotif",
|
||||
|
||||
@@ -112,6 +112,12 @@ export interface LoginTokenPostResponse {
|
||||
login_token: string;
|
||||
/**
|
||||
* Expiration in seconds.
|
||||
*
|
||||
* @deprecated this is only provided for compatibility with original revision of the MSC.
|
||||
*/
|
||||
expires_in: number;
|
||||
/**
|
||||
* Expiration in milliseconds.
|
||||
*/
|
||||
expires_in_ms: number;
|
||||
}
|
||||
|
||||
@@ -235,6 +235,13 @@ export const LOCAL_NOTIFICATION_SETTINGS_PREFIX = new UnstableValue(
|
||||
"org.matrix.msc3890.local_notification_settings",
|
||||
);
|
||||
|
||||
/**
|
||||
* https://github.com/matrix-org/matrix-doc/pull/4023
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export const UNSIGNED_THREAD_ID_FIELD = new UnstableValue("thread_id", "org.matrix.msc4023.thread_id");
|
||||
|
||||
export interface IEncryptedFile {
|
||||
url: string;
|
||||
mimetype?: string;
|
||||
|
||||
@@ -38,7 +38,7 @@ export interface CachedReceipt {
|
||||
data: Receipt;
|
||||
}
|
||||
|
||||
export type ReceiptCache = { [eventId: string]: CachedReceipt[] };
|
||||
export type ReceiptCache = Map<string, CachedReceipt[]>;
|
||||
|
||||
export interface ReceiptContent {
|
||||
[eventId: string]: {
|
||||
@@ -49,11 +49,8 @@ export interface ReceiptContent {
|
||||
}
|
||||
|
||||
// We will only hold a synthetic receipt if we do not have a real receipt or the synthetic is newer.
|
||||
export type Receipts = {
|
||||
[receiptType: string]: {
|
||||
[userId: string]: [WrappedReceipt | null, WrappedReceipt | null]; // Pair<real receipt, synthetic receipt> (both nullable)
|
||||
};
|
||||
};
|
||||
// map: receipt type → user Id → receipt
|
||||
export type Receipts = Map<string, Map<string, [real: WrappedReceipt | null, synthetic: WrappedReceipt | null]>>;
|
||||
|
||||
export type CachedReceiptStructure = {
|
||||
eventId: string;
|
||||
|
||||
@@ -186,6 +186,7 @@ export interface IRelationsRequestOpts {
|
||||
to?: string;
|
||||
limit?: number;
|
||||
dir?: Direction;
|
||||
recurse?: boolean; // MSC3981 Relations Recursion https://github.com/matrix-org/matrix-spec-proposals/pull/3981
|
||||
}
|
||||
|
||||
export interface IRelationsResponse {
|
||||
|
||||
+2
-2
@@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { IAuthData } from "../interactive-auth";
|
||||
import { IAuthDict, IAuthData } from "../interactive-auth";
|
||||
|
||||
/**
|
||||
* Helper type to represent HTTP request body for a UIA enabled endpoint
|
||||
*/
|
||||
export type UIARequest<T> = T & {
|
||||
auth?: IAuthData;
|
||||
auth?: IAuthDict;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+3
-1
@@ -25,7 +25,7 @@ export class ReEmitter {
|
||||
public constructor(private readonly target: EventEmitter) {}
|
||||
|
||||
// Map from emitter to event name to re-emitter
|
||||
private reEmitters = new Map<EventEmitter, Map<string, (...args: any[]) => void>>();
|
||||
private reEmitters = new WeakMap<EventEmitter, Map<string, (...args: any[]) => void>>();
|
||||
|
||||
public reEmit(source: EventEmitter, eventNames: string[]): void {
|
||||
let reEmittersByEvent = this.reEmitters.get(source);
|
||||
@@ -35,6 +35,8 @@ export class ReEmitter {
|
||||
}
|
||||
|
||||
for (const eventName of eventNames) {
|
||||
if (reEmittersByEvent.has(eventName)) continue;
|
||||
|
||||
// We include the source as the last argument for event handlers which may need it,
|
||||
// such as read receipt listeners on the client class which won't have the context
|
||||
// of the room.
|
||||
|
||||
@@ -21,6 +21,7 @@ import { MatrixError } from "./http-api";
|
||||
import { IndexedToDeviceBatch, ToDeviceBatch, ToDeviceBatchWithTxnId, ToDevicePayload } from "./models/ToDeviceMessage";
|
||||
import { MatrixScheduler } from "./scheduler";
|
||||
import { SyncState } from "./sync";
|
||||
import { MapWithDefault } from "./utils";
|
||||
|
||||
const MAX_BATCH_SIZE = 20;
|
||||
|
||||
@@ -122,12 +123,9 @@ export class ToDeviceMessageQueue {
|
||||
* Attempts to send a batch of to-device messages.
|
||||
*/
|
||||
private async sendBatch(batch: IndexedToDeviceBatch): Promise<void> {
|
||||
const contentMap: Record<string, Record<string, ToDevicePayload>> = {};
|
||||
const contentMap: MapWithDefault<string, Map<string, ToDevicePayload>> = new MapWithDefault(() => new Map());
|
||||
for (const item of batch.batch) {
|
||||
if (!contentMap[item.userId]) {
|
||||
contentMap[item.userId] = {};
|
||||
}
|
||||
contentMap[item.userId][item.deviceId] = item.payload;
|
||||
contentMap.getOrCreate(item.userId).set(item.deviceId, item.payload);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
|
||||
@@ -142,7 +142,7 @@ export class AutoDiscovery {
|
||||
},
|
||||
};
|
||||
|
||||
if (!wellknown || !wellknown["m.homeserver"]) {
|
||||
if (!wellknown?.["m.homeserver"]) {
|
||||
logger.error("No m.homeserver key in config");
|
||||
|
||||
clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
|
||||
@@ -171,7 +171,7 @@ export class AutoDiscovery {
|
||||
|
||||
// Step 3: Make sure the homeserver URL points to a homeserver.
|
||||
const hsVersions = await this.fetchWellKnownObject(`${hsUrl}/_matrix/client/versions`);
|
||||
if (!hsVersions || !hsVersions.raw?.["versions"]) {
|
||||
if (!hsVersions?.raw?.["versions"]) {
|
||||
logger.error("Invalid /versions response");
|
||||
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HOMESERVER;
|
||||
|
||||
@@ -345,7 +345,7 @@ export class AutoDiscovery {
|
||||
|
||||
const response = await this.fetchWellKnownObject(`https://${domain}/.well-known/matrix/client`);
|
||||
if (!response) return {};
|
||||
return response.raw || {};
|
||||
return response.raw ?? {};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+323
-161
File diff suppressed because it is too large
Load Diff
@@ -14,31 +14,27 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
|
||||
import type { IToDeviceEvent } from "../sync-accumulator";
|
||||
import type { DeviceTrustLevel, UserTrustLevel } from "../crypto/CrossSigning";
|
||||
import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { Room } from "../models/room";
|
||||
import { CryptoApi } from "../crypto-api";
|
||||
import { CrossSigningInfo, UserTrustLevel } from "../crypto/CrossSigning";
|
||||
import { IEncryptedEventInfo } from "../crypto/api";
|
||||
import { IEventDecryptionResult } from "../@types/crypto";
|
||||
import { VerificationRequest } from "../crypto/verification/request/VerificationRequest";
|
||||
|
||||
/**
|
||||
* Common interface for the crypto implementations
|
||||
*/
|
||||
export interface CryptoBackend extends SyncCryptoCallbacks {
|
||||
/**
|
||||
* Global override for whether the client should ever send encrypted
|
||||
* messages to unverified devices. This provides the default for rooms which
|
||||
* do not specify a value.
|
||||
*
|
||||
* If true, all unverified devices will be blacklisted by default
|
||||
*/
|
||||
globalBlacklistUnverifiedDevices: boolean;
|
||||
|
||||
export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
|
||||
/**
|
||||
* Whether sendMessage in a room with unknown and unverified devices
|
||||
* should throw an error and not send the message. This has 'Global' for
|
||||
* symmetry with setGlobalBlacklistUnverifiedDevices but there is currently
|
||||
* no room-level equivalent for this setting.
|
||||
*
|
||||
* @remarks this is here, rather than in `CryptoApi`, because I don't think we're
|
||||
* going to support it in the rust crypto implementation.
|
||||
*/
|
||||
globalErrorOnUnknownDevices: boolean;
|
||||
|
||||
@@ -47,16 +43,6 @@ export interface CryptoBackend extends SyncCryptoCallbacks {
|
||||
*/
|
||||
stop(): void;
|
||||
|
||||
/**
|
||||
* Checks if the user has previously published cross-signing keys
|
||||
*
|
||||
* This means downloading the devicelist for the user and checking if the list includes
|
||||
* the cross-signing pseudo-device.
|
||||
|
||||
* @returns true if the user has previously published cross-signing keys
|
||||
*/
|
||||
userHasCrossSigningKeys(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Get the verification level for a given user
|
||||
*
|
||||
@@ -66,24 +52,6 @@ export interface CryptoBackend extends SyncCryptoCallbacks {
|
||||
*/
|
||||
checkUserTrust(userId: string): UserTrustLevel;
|
||||
|
||||
/**
|
||||
* Get the verification level for a given device
|
||||
*
|
||||
* TODO: define this better
|
||||
*
|
||||
* @param userId - user to be checked
|
||||
* @param deviceId - device to be checked
|
||||
*/
|
||||
checkDeviceTrust(userId: string, deviceId: string): DeviceTrustLevel;
|
||||
|
||||
/**
|
||||
* Perform any background tasks that can be done before a message is ready to
|
||||
* send, in order to speed up sending of the message.
|
||||
*
|
||||
* @param room - the room the event is in
|
||||
*/
|
||||
prepareToEncrypt(room: Room): void;
|
||||
|
||||
/**
|
||||
* Encrypt an event according to the configuration of the room.
|
||||
*
|
||||
@@ -112,14 +80,24 @@ export interface CryptoBackend extends SyncCryptoCallbacks {
|
||||
getEventEncryptionInfo(event: MatrixEvent): IEncryptedEventInfo;
|
||||
|
||||
/**
|
||||
* Get a list containing all of the room keys
|
||||
* Finds a DM verification request that is already in progress for the given room id
|
||||
*
|
||||
* This should be encrypted before returning it to the user.
|
||||
* @param roomId - the room to use for verification
|
||||
*
|
||||
* @returns a promise which resolves to a list of
|
||||
* session export objects
|
||||
* @returns the VerificationRequest that is in progress, if any
|
||||
*/
|
||||
exportRoomKeys(): Promise<IMegolmSessionData[]>;
|
||||
findVerificationRequestDMInProgress(roomId: string): VerificationRequest | undefined;
|
||||
|
||||
/**
|
||||
* Get the cross signing information for a given user.
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @param userId - the user ID to get the cross-signing info for.
|
||||
*
|
||||
* @returns the cross signing information for the user.
|
||||
*/
|
||||
getStoredCrossSigningForUser(userId: string): CrossSigningInfo | null;
|
||||
}
|
||||
|
||||
/** The methods which crypto implementations should expose to the Sync api */
|
||||
@@ -138,6 +116,22 @@ export interface SyncCryptoCallbacks {
|
||||
*/
|
||||
preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]>;
|
||||
|
||||
/**
|
||||
* Called by the /sync loop when one time key counts and unused fallback key details are received.
|
||||
*
|
||||
* @param oneTimeKeysCounts - the received one time key counts
|
||||
* @param unusedFallbackKeys - the received unused fallback keys
|
||||
*/
|
||||
processKeyCounts(oneTimeKeysCounts?: Record<string, number>, unusedFallbackKeys?: string[]): Promise<void>;
|
||||
|
||||
/**
|
||||
* Handle the notification from /sync that device lists have
|
||||
* been changed.
|
||||
*
|
||||
* @param deviceLists - device_lists field from /sync
|
||||
*/
|
||||
processDeviceLists(deviceLists: IDeviceLists): Promise<void>;
|
||||
|
||||
/**
|
||||
* Called by the /sync loop whenever an m.room.encryption event is received.
|
||||
*
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as utils from "./utils";
|
||||
import { encodeParams } from "./utils";
|
||||
|
||||
/**
|
||||
* Get the HTTP URL for an MXC URI.
|
||||
@@ -74,6 +74,6 @@ export function getHttpUriForMxc(
|
||||
serverAndMediaId = serverAndMediaId.slice(0, fragmentOffset);
|
||||
}
|
||||
|
||||
const urlParams = Object.keys(params).length === 0 ? "" : "?" + utils.encodeParams(params);
|
||||
const urlParams = Object.keys(params).length === 0 ? "" : "?" + encodeParams(params);
|
||||
return baseUrl + prefix + serverAndMediaId + urlParams + fragment;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user