Compare commits
315 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 22865fd834 | |||
| abc9911e95 | |||
| efdae0d66f | |||
| 2bf554761c | |||
| c2687643b5 | |||
| a33758eda6 | |||
| 8faed02cc5 | |||
| 3ae0dab47a | |||
| 95394e4cbe | |||
| 8c9bbc01fc | |||
| eb888791a3 | |||
| 6c0b2f55e1 | |||
| c9a5eaece3 | |||
| 64505de36b | |||
| 65d858f9a3 | |||
| 1da5e8f56a | |||
| 5efd4c2915 | |||
| bc03950f8a | |||
| c09da9a23f | |||
| e874468ba3 | |||
| 6fedda91f9 | |||
| d22a39f5d7 | |||
| 4fc6ba884e | |||
| c30e498013 | |||
| 8240bf0ae7 | |||
| 2321c44687 | |||
| 0137e9d5a8 | |||
| 28bbc51752 | |||
| 0db3ac9b43 | |||
| 53039b78ee | |||
| 2a06d19431 | |||
| a747eef04c | |||
| 583823c2ef | |||
| 26d13c15c3 | |||
| c850ca3179 | |||
| 8438533532 | |||
| 475f82c5ce | |||
| 936e7c3072 | |||
| 82ed7bd86a | |||
| cb67eae858 | |||
| e4937e6222 | |||
| 5cdd524da7 | |||
| 0ff0093380 | |||
| b352405c89 | |||
| 0d73d0c6c7 | |||
| d2f76d4956 | |||
| c680dd7eb2 | |||
| e24bb0f50c | |||
| 1ed3b13f0d | |||
| 4f628bf64c | |||
| 7d5c003716 | |||
| dbab185f9d | |||
| cfcd191cbf | |||
| 514633c5fa | |||
| 5bffb7df4f | |||
| 9e1897dcd0 | |||
| 5f3ddc37a1 | |||
| 78a225795b | |||
| 467b49a0dc | |||
| 06e083874a | |||
| 0f25429849 | |||
| 32ddf2813d | |||
| 1ed082f3d4 | |||
| 706002cdcb | |||
| 731de1108c | |||
| 2da6c0c605 | |||
| 9f1d0c3896 | |||
| 0b290fffa1 | |||
| 97844f0e47 | |||
| 85a55c79cd | |||
| 63d4195453 | |||
| d5a35f8a99 | |||
| d1259b241c | |||
| a573727662 | |||
| dce8acbf17 | |||
| 4ba1341f8f | |||
| e517d009bf | |||
| dc2d03dea5 | |||
| d5bb9e7600 | |||
| d908036f50 | |||
| afc3c6213b | |||
| 7884c22e41 | |||
| 887d8a7663 | |||
| 2c68ee2254 | |||
| d445823d0b | |||
| abe4630687 | |||
| 8664b66238 | |||
| 596826ab4d | |||
| c8ec5421c7 | |||
| b8078f9916 | |||
| 92342c07ed | |||
| 3e989006aa | |||
| 8e0ef5ff2c | |||
| 78d05942a3 | |||
| c22a6858c8 | |||
| 461aeae281 | |||
| 0511e313d3 | |||
| da3d5c4a43 | |||
| 4c26b55c9a | |||
| 3711ad7e61 | |||
| 3031152444 | |||
| 51ebd2fcde | |||
| 27dd856778 | |||
| 7fee37680f | |||
| 2541ca04c2 | |||
| d55c6a36df | |||
| 8c0736a719 | |||
| 50b042d1ff | |||
| a818dc1e9d | |||
| d8dae65a4d | |||
| 8be286308c | |||
| 84498bf77d | |||
| a1f4b07b7d | |||
| ee8413beff | |||
| e4d4628cc8 | |||
| b2e09250d9 | |||
| 6176faef48 | |||
| 453cdd9eda | |||
| 6529f02c28 | |||
| d3dfcd9242 | |||
| a26fc46ed4 | |||
| be3913e8a5 | |||
| c1e0192baf | |||
| 8123e9a3f1 | |||
| 0425f4e5c8 | |||
| 5b74b446d4 | |||
| 624914a565 | |||
| 4da9627727 | |||
| 1cb30bfe9b | |||
| 12308b4c07 | |||
| 7f25162725 | |||
| 4826868a8f | |||
| 91bde6afa1 | |||
| 95842c2b91 | |||
| c27c357688 | |||
| b474439256 | |||
| 42dc498359 | |||
| ca914c97e0 | |||
| f96dac1e5b | |||
| 7e0d92cbe0 | |||
| fe46fec161 | |||
| 2cf7d819d9 | |||
| 74c109adac | |||
| 5d7218476a | |||
| d03db17405 | |||
| 7520340c46 | |||
| b63845a413 | |||
| 1b7695cdca | |||
| f4a796ca2f | |||
| 58a5d09aed | |||
| bc620796c3 | |||
| b1cfed1b21 | |||
| c700d8daa2 | |||
| f94dbdec0f | |||
| 173d9c331a | |||
| 04ebcf7be7 | |||
| 5e185ae1e7 | |||
| 322cc6da10 | |||
| 014e674a4e | |||
| 87acd9dd88 | |||
| bbccb98c06 | |||
| ca835a7cf7 | |||
| b8fb10a1d1 | |||
| 5e9d2e064e | |||
| d2753a9aea | |||
| c6eda55110 | |||
| 7ce243110f | |||
| 20d26db37d | |||
| a2a25e71ac | |||
| 1e7bc2f31c | |||
| eec5040bd0 | |||
| 24174c9233 | |||
| 8a2cd3f43c | |||
| eebf40590f | |||
| f5e0b3007b | |||
| 0d5b6138ae | |||
| 45b02fed5a | |||
| 4f63b47134 | |||
| 6edf3990f6 | |||
| c89f220e52 | |||
| 9675a1584d | |||
| c81199b9d5 | |||
| 6bdb087883 | |||
| 7da620c5be | |||
| c4f00895b1 | |||
| f8c3973efd | |||
| 0c0775c0bf | |||
| 70edf0f34d | |||
| b46b31563e | |||
| 8007bc5fe8 | |||
| d178fbf9cd | |||
| f81036346f | |||
| 1a364c93c3 | |||
| 1cd6fe7775 | |||
| 89d0133c61 | |||
| a8b3369dd0 | |||
| 99600e87f1 | |||
| 7cf59d64e6 | |||
| 5967c670d8 | |||
| 2fe35fed13 | |||
| 2d1308c733 | |||
| 11348f9532 | |||
| 869576747c | |||
| 35ea144bca | |||
| 5bf29ef543 | |||
| 99b3cf2279 | |||
| 5e2acb558b | |||
| 19494e093b | |||
| ab217bdc35 | |||
| c4d32a3292 | |||
| 8e01b654bc | |||
| dc406ee2e8 | |||
| be8b769542 | |||
| 5973a15f68 | |||
| 3c28cfc96a | |||
| c99378501b | |||
| b10a804a03 | |||
| 2337d5a7af | |||
| 5a49ed4ebb | |||
| 22db9eb245 | |||
| 4cddc7397d | |||
| 418b69914a | |||
| 0082964345 | |||
| 96b3c79566 | |||
| 41a6f18125 | |||
| a2b2e8dbdf | |||
| abd920f0f4 | |||
| 5333d0e0ba | |||
| c885542628 | |||
| 81b58388ee | |||
| 0d486eaade | |||
| 76b9c3950b | |||
| 6176cb6d7b | |||
| b9a107f9ff | |||
| 06e8cea63d | |||
| 815c36e075 | |||
| d355073d10 | |||
| 2ef3ebb466 | |||
| 92f7481fdd | |||
| 8df30ed068 | |||
| 49624d5d73 | |||
| 8e5128ad3c | |||
| 8ac2f2a78d | |||
| 630440c59c | |||
| 6932437360 | |||
| f381dfe991 | |||
| 6a98b835a8 | |||
| ae01c0915c | |||
| a597a9d660 | |||
| 9661cdecf2 | |||
| 533070c603 | |||
| eae1c2d48b | |||
| 070a89d89d | |||
| ffc9fb34d0 | |||
| d030c83cee | |||
| c115e055c6 | |||
| 0f65088fd9 | |||
| a1ff63adcb | |||
| 31fc5f23be | |||
| febef3fc7c | |||
| f2625348d8 | |||
| 6d1d04782a | |||
| 5e67a173c8 | |||
| 9780643ce7 | |||
| 608c6ece56 | |||
| 3a55efb476 | |||
| 48d4f1b0cc | |||
| a80e90b42d | |||
| 2c13e133b7 | |||
| 68898aeff2 | |||
| f604ab2f63 | |||
| b7d45e83f8 | |||
| db0e3cfbb0 | |||
| 87b90cc983 | |||
| cc9545e313 | |||
| ed2792e6d8 | |||
| dd53ec722f | |||
| b03dc6ac43 | |||
| 13c7e0ebda | |||
| 2cd63ca4b9 | |||
| 479c4278a6 | |||
| 636fc3daaa | |||
| 1d1309870a | |||
| 13b8f01062 | |||
| cd672ec4cf | |||
| 2363703b64 | |||
| 1250bb8833 | |||
| 016ef12c4a | |||
| 84d193a9a2 | |||
| 9d5f1bb4fc | |||
| 228131edf3 | |||
| 23ad637aad | |||
| 103617c70e | |||
| 8d84621b07 | |||
| 41878c7a43 | |||
| f31e83fd03 | |||
| b515cdbdbb | |||
| f4b6f91ee2 | |||
| df4536492c | |||
| 2e98da4224 | |||
| 48d9d9b4c9 | |||
| d90ae11e2b | |||
| 3f246c6080 | |||
| 68911520d3 | |||
| 393a8d0cdb | |||
| 51b63092b4 | |||
| b49c9639b9 | |||
| c588611fc0 | |||
| 5b34e4beaf | |||
| 91f16e5e8e | |||
| 9cf257da0e | |||
| 188de3c4c8 | |||
| 67019a3486 | |||
| a39b1203f2 | |||
| c49a527e5e |
@@ -0,0 +1 @@
|
||||
_docs
|
||||
@@ -103,11 +103,8 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
{
|
||||
// We don't need amazing docs in our spec files
|
||||
files: ["src/**/*.ts"],
|
||||
rules: {
|
||||
"tsdoc/syntax": "error",
|
||||
// We use some select jsdoc rules as the tsdoc linter has only one rule
|
||||
"jsdoc/no-types": "error",
|
||||
"jsdoc/empty-tags": "error",
|
||||
"jsdoc/check-property-names": "error",
|
||||
|
||||
+11
-4
@@ -1,8 +1,15 @@
|
||||
* @matrix-org/element-web
|
||||
/.github/workflows/** @matrix-org/element-web-app-team
|
||||
/package.json @matrix-org/element-web-app-team
|
||||
/yarn.lock @matrix-org/element-web-app-team
|
||||
* @matrix-org/element-web-reviewers
|
||||
/.github/workflows/** @matrix-org/element-web-team
|
||||
/package.json @matrix-org/element-web-team
|
||||
/yarn.lock @matrix-org/element-web-team
|
||||
/src/webrtc @matrix-org/element-call-reviewers
|
||||
/src/matrixrtc @matrix-org/element-call-reviewers
|
||||
/spec/*/webrtc @matrix-org/element-call-reviewers
|
||||
/spec/*/matrixrtc @matrix-org/element-call-reviewers
|
||||
|
||||
/src/crypto @matrix-org/element-crypto-web-reviewers
|
||||
/src/rust-crypto @matrix-org/element-crypto-web-reviewers
|
||||
/spec/integ/crypto @matrix-org/element-crypto-web-reviewers
|
||||
/spec/unit/crypto.spec.ts @matrix-org/element-crypto-web-reviewers
|
||||
/spec/unit/crypto @matrix-org/element-crypto-web-reviewers
|
||||
/spec/unit/rust-crypto @matrix-org/element-crypto-web-reviewers
|
||||
|
||||
@@ -2,12 +2,7 @@
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Tests written for new code (and old code if feasible)
|
||||
- [ ] Linter and other CI checks pass
|
||||
- [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md))
|
||||
|
||||
<!--
|
||||
If you would like to specify text for the changelog entry other than your PR title, add the following:
|
||||
|
||||
Notes: Add super cool feature
|
||||
-->
|
||||
- [ ] Tests written for new code (and old code if feasible).
|
||||
- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation.
|
||||
- [ ] Linter and other CI checks pass.
|
||||
- [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md)).
|
||||
|
||||
@@ -22,7 +22,7 @@ runs:
|
||||
|
||||
- name: Upload tarball signature
|
||||
if: ${{ inputs.upload-url }}
|
||||
uses: shogo82148/actions-upload-release-asset@dccd6d23e64fd6a746dce6814c0bde0a04886085 # v1
|
||||
uses: shogo82148/actions-upload-release-asset@8f032eff0255912cc9c8455797fd6d72f25c7ab7 # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ env.VERSION }}.tar.gz.asc
|
||||
|
||||
@@ -29,13 +29,13 @@ runs:
|
||||
|
||||
- name: Upload asset signatures
|
||||
if: inputs.gpg-fingerprint
|
||||
uses: shogo82148/actions-upload-release-asset@dccd6d23e64fd6a746dce6814c0bde0a04886085 # v1
|
||||
uses: shogo82148/actions-upload-release-asset@8f032eff0255912cc9c8455797fd6d72f25c7ab7 # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ inputs.asset-path }}.asc
|
||||
|
||||
- name: Upload assets
|
||||
uses: shogo82148/actions-upload-release-asset@dccd6d23e64fd6a746dce6814c0bde0a04886085 # v1
|
||||
uses: shogo82148/actions-upload-release-asset@8f032eff0255912cc9c8455797fd6d72f25c7ab7 # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ inputs.asset-path }}
|
||||
|
||||
@@ -22,10 +22,14 @@ version-resolver:
|
||||
exclude-labels:
|
||||
- "T-Task"
|
||||
- "X-Reverted"
|
||||
- "backport staging"
|
||||
exclude-contributors:
|
||||
- "RiotRobot"
|
||||
template: |
|
||||
$CHANGES
|
||||
#no-changes-template: ""
|
||||
prerelease: true
|
||||
prerelease-identifier: rc
|
||||
include-pre-releases: false
|
||||
stable-ref: master
|
||||
staging-ref: staging
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
# Triggers after the "Downstream artifacts" build has finished, to run the
|
||||
# cypress tests (with access to repo secrets)
|
||||
|
||||
name: matrix-react-sdk Cypress End to End Tests
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build downstream artifacts"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
|
||||
cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
cypress:
|
||||
name: Cypress
|
||||
|
||||
# We only want to run the cypress tests on merge queue to prevent regressions
|
||||
# from creeping in. They take a long time to run and consume 4 concurrent runners.
|
||||
if: github.event.workflow_run.event == 'merge_group'
|
||||
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/cypress.yaml@f6ef476f7905cc2b1f060f1a360b482e7546e682
|
||||
permissions:
|
||||
actions: read
|
||||
issues: read
|
||||
statuses: write
|
||||
pull-requests: read
|
||||
secrets:
|
||||
# secrets are not automatically shared with called workflows, so share the cypress dashboard key, and the Kiwi login details
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
TCMS_USERNAME: ${{ secrets.TCMS_USERNAME }}
|
||||
TCMS_PASSWORD: ${{ secrets.TCMS_PASSWORD }}
|
||||
with:
|
||||
react-sdk-repository: matrix-org/matrix-react-sdk
|
||||
|
||||
# We want to make the cypress tests a required check for the merge queue.
|
||||
#
|
||||
# Unfortunately, github doesn't distinguish between "checks needed for branch
|
||||
# protection" (ie, the things that must pass before the PR will even be added
|
||||
# to the merge queue) and "checks needed in the merge queue". We just have to add
|
||||
# the check to the branch protection list.
|
||||
#
|
||||
# Ergo, if we know we're not going to run the cypress tests, we need to add a
|
||||
# passing status check manually.
|
||||
mark_skipped:
|
||||
if: github.event.workflow_run.event != 'merge_group'
|
||||
permissions:
|
||||
statuses: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: Sibz/github-status-action@650dd1a882a76dbbbc4576fb5974b8d22f29847f # v1.1.6
|
||||
with:
|
||||
authToken: "${{ secrets.GITHUB_TOKEN }}"
|
||||
state: success
|
||||
description: Cypress skipped
|
||||
context: "${{ github.workflow }} / cypress"
|
||||
sha: "${{ github.event.workflow_run.head_sha }}"
|
||||
@@ -11,18 +11,16 @@ jobs:
|
||||
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# 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@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
workflow: static_analysis.yml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
name: docs
|
||||
path: docs
|
||||
|
||||
- name: 📤 Deploy to Netlify
|
||||
uses: matrix-org/netlify-pr-preview@v2
|
||||
uses: matrix-org/netlify-pr-preview@v3
|
||||
with:
|
||||
path: docs
|
||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
name: Build downstream artifacts
|
||||
on:
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
pull_request: {}
|
||||
|
||||
# For now at least, we don't run this or the cypress-tests against pushes
|
||||
# to develop or master.
|
||||
#
|
||||
# Note that if we later choose to do so, we'll need to find a way to stop
|
||||
# the results in Cypress Cloud from clobbering those from the 'develop'
|
||||
# branch of matrix-react-sdk.
|
||||
#
|
||||
#push:
|
||||
# branches: [develop, master]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
build-element-web:
|
||||
name: Build element-web
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.84.1
|
||||
with:
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
react-sdk-repository: matrix-org/matrix-react-sdk
|
||||
@@ -0,0 +1,33 @@
|
||||
# Triggers after the "Downstream artifacts" build has finished, to run the
|
||||
# matrix-react-sdk playwright tests (with access to repo secrets)
|
||||
|
||||
name: matrix-react-sdk End to End Tests
|
||||
on:
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
pull_request: {}
|
||||
|
||||
# For now at least, we don't run this or the downstream-end-to-end-tests against pushes
|
||||
# to develop or master.
|
||||
#
|
||||
#push:
|
||||
# branches: [develop, master]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
|
||||
cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
playwright:
|
||||
name: Playwright
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/end-to-end-tests.yaml@develop
|
||||
permissions:
|
||||
actions: read
|
||||
issues: read
|
||||
pull-requests: read
|
||||
with:
|
||||
react-sdk-repository: matrix-org/matrix-react-sdk
|
||||
# We only want to run the playwright tests on merge queue to prevent regressions
|
||||
# from creeping in. They take a long time to run and consume multiple concurrent runners.
|
||||
skip: ${{ github.event_name != 'merge_group' }}
|
||||
@@ -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@bf47d102fdb849e755b0b0023ea3e81a44b6f570 # v2
|
||||
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
repository: ${{ matrix.repo }}
|
||||
|
||||
@@ -14,11 +14,18 @@ jobs:
|
||||
name: Preview Changelog
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: matrix-org/allchange@main
|
||||
- uses: mheap/github-action-required-labels@132879b972cb7f2ac593006455875098e73cc7f2 # v5
|
||||
if: github.event_name != 'merge_group'
|
||||
with:
|
||||
ghToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
requireLabel: true
|
||||
labels: |
|
||||
X-Breaking-Change
|
||||
T-Deprecation
|
||||
T-Enhancement
|
||||
T-Defect
|
||||
T-Task
|
||||
Dependencies
|
||||
mode: minimum
|
||||
count: 1
|
||||
|
||||
prevent-blocked:
|
||||
name: Prevent Blocked
|
||||
@@ -39,7 +46,8 @@ jobs:
|
||||
if: github.event.action == 'opened'
|
||||
steps:
|
||||
- name: Check membership
|
||||
uses: tspascoal/get-user-teams-membership@ba78054988f58bea69b7c6136d563236f8ed2fc0 # v3
|
||||
if: github.event.pull_request.user.login != 'renovate[bot]'
|
||||
uses: tspascoal/get-user-teams-membership@57e9f42acd78f4d0f496b3be4368fc5f62696662 # v3
|
||||
id: teams
|
||||
with:
|
||||
username: ${{ github.event.pull_request.user.login }}
|
||||
@@ -48,7 +56,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
- name: Add label
|
||||
if: ${{ steps.teams.outputs.isTeamMember == 'false' }}
|
||||
if: steps.teams.outputs.isTeamMember == 'false'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# Workflow used by other workflows to generate draft releases.
|
||||
name: Release Drafter Reusable
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
include-changes:
|
||||
description: Project to include changelog entries from in this release.
|
||||
type: string
|
||||
required: false
|
||||
concurrency: release-drafter-action
|
||||
jobs:
|
||||
draft:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: staging
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: package.json
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
|
||||
- uses: t3chguy/release-drafter@105e541c2c3d857f032bd522c0764694758fabad
|
||||
id: draft-release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
disable-autolabeler: true
|
||||
|
||||
- name: Get actions scripts
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
persist-credentials: false
|
||||
path: .action-repo
|
||||
sparse-checkout: |
|
||||
.github/actions
|
||||
scripts/release
|
||||
|
||||
- name: Ingest upstream changes
|
||||
if: inputs.include-changes
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
||||
DEPENDENCY: ${{ inputs.include-changes }}
|
||||
VERSION: ${{ steps.draft-release.outputs.tag_name }}
|
||||
with:
|
||||
retries: 3
|
||||
script: |
|
||||
const { RELEASE_ID: releaseId, DEPENDENCY, VERSION } = process.env;
|
||||
const { owner, repo } = context.repo;
|
||||
const script = require("./.action-repo/scripts/release/merge-release-notes.js");
|
||||
|
||||
let deps = [];
|
||||
if (DEPENDENCY.includes("/")) {
|
||||
deps.push(DEPENDENCY.replace("$VERSION", VERSION))
|
||||
} else {
|
||||
const fromVersion = JSON.parse((await github.request(`https://raw.githubusercontent.com/${owner}/${repo}/master/package.json`)).data).dependencies[DEPENDENCY];
|
||||
const toVersion = require("./package.json").dependencies[DEPENDENCY];
|
||||
|
||||
if (toVersion.endsWith("#develop")) {
|
||||
core.warning(`${DEPENDENCY} will be kept at ${fromVersion}`, { title: "Develop dependency found" });
|
||||
} else {
|
||||
deps.push([DEPENDENCY, fromVersion, toVersion]);
|
||||
}
|
||||
}
|
||||
|
||||
if (deps.length) {
|
||||
const notes = await script({
|
||||
github,
|
||||
releaseId,
|
||||
dependencies: deps,
|
||||
});
|
||||
|
||||
await github.rest.repos.updateRelease({
|
||||
owner,
|
||||
repo,
|
||||
release_id: releaseId,
|
||||
body: notes,
|
||||
tag_name: VERSION,
|
||||
});
|
||||
}
|
||||
@@ -1,21 +1,13 @@
|
||||
# Generates the draft release for the js-sdk
|
||||
# Normally triggered whenever anything is merged to the staging branch, but
|
||||
# also has a workflow dispatch trigger in case it needs running manually due
|
||||
# to failures / workflow updates etc.
|
||||
name: Release Drafter
|
||||
on:
|
||||
push:
|
||||
branches: [staging]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
previous-version:
|
||||
description: What release to use as a base for release note purposes
|
||||
required: false
|
||||
type: string
|
||||
workflow_dispatch: {}
|
||||
concurrency: ${{ github.workflow }}
|
||||
jobs:
|
||||
draft:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@e64b19c4c46173209ed9f2e5a2f4ca7de89a0e86 # v5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
disable-autolabeler: true
|
||||
previous-version: ${{ inputs.previous-version }}
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-drafter-workflow.yml@develop
|
||||
|
||||
@@ -20,10 +20,8 @@ on:
|
||||
description: Publish to npm
|
||||
type: boolean
|
||||
default: false
|
||||
dependencies:
|
||||
description: |
|
||||
List of dependencies to update in `npm-dep=version` format.
|
||||
`version` can be `"current"` to leave it at the current version.
|
||||
downstreams:
|
||||
description: List of github projects (owner/repo) which should have their dependency bumped to the newly released version (in JSON string array string syntax)
|
||||
type: string
|
||||
required: false
|
||||
include-changes:
|
||||
@@ -53,15 +51,15 @@ jobs:
|
||||
- name: Load GPG key
|
||||
id: gpg
|
||||
if: inputs.gpg-fingerprint
|
||||
uses: crazy-max/ghaction-import-gpg@82a020f1f7f605c65dd2449b392a52c3fcfef7ef # v6
|
||||
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
fingerprint: ${{ inputs.gpg-fingerprint }}
|
||||
|
||||
- name: Get draft release
|
||||
id: release
|
||||
uses: cardinalby/git-get-release-action@cedef2faf69cb7c55b285bad07688d04430b7ada # v1
|
||||
id: draft-release
|
||||
uses: cardinalby/git-get-release-action@5172c3a026600b1d459b117738c605fabc9e4e44 # v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
@@ -88,18 +86,12 @@ jobs:
|
||||
id: prepare
|
||||
run: |
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
{
|
||||
echo "RELEASE_NOTES<<EOF"
|
||||
echo "$BODY"
|
||||
echo "EOF"
|
||||
} >> $GITHUB_ENV
|
||||
|
||||
HAS_DIST=0
|
||||
jq -e .scripts.dist package.json >/dev/null 2>&1 && HAS_DIST=1
|
||||
echo "has-dist-script=$HAS_DIST" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
BODY: ${{ steps.release.outputs.body }}
|
||||
VERSION: ${{ steps.release.outputs.tag_name }}
|
||||
VERSION: ${{ steps.draft-release.outputs.tag_name }}
|
||||
|
||||
- name: Finalise version
|
||||
if: inputs.final
|
||||
@@ -132,76 +124,23 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: "yarn install --frozen-lockfile"
|
||||
|
||||
- name: Update dependencies
|
||||
id: update-dependencies
|
||||
if: inputs.dependencies
|
||||
run: |
|
||||
UPDATED=()
|
||||
while IFS= read -r DEPENDENCY; do
|
||||
[ -z "$DEPENDENCY" ] && continue
|
||||
IFS="=" read -r PACKAGE UPDATE_VERSION <<< "$DEPENDENCY"
|
||||
|
||||
CURRENT_VERSION=$(cat package.json | jq -r .dependencies[\"$PACKAGE\"])
|
||||
echo "Current $PACKAGE version is $CURRENT_VERSION"
|
||||
|
||||
if [ "$CURRENT_VERSION" == "null" ]
|
||||
then
|
||||
echo "Unable to find $PACKAGE in package.json"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$UPDATE_VERSION" == "current" ] || [ "$UPDATE_VERSION" == "$CURRENT_VERSION" ]
|
||||
then
|
||||
echo "Not updating dependency $PACKAGE"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Upgrading $PACKAGE to $UPDATE_VERSION..."
|
||||
yarn upgrade "$PACKAGE@$UPDATE_VERSION" --exact
|
||||
git add -u
|
||||
git commit -m "Upgrade $PACKAGE to $UPDATE_VERSION"
|
||||
UPDATED+=("$PACKAGE")
|
||||
done <<< "$DEPENDENCIES"
|
||||
|
||||
JSON=$(jq --compact-output --null-input '$ARGS.positional' --args -- "${UPDATED[@]}")
|
||||
echo "updated=$JSON" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
DEPENDENCIES: ${{ inputs.dependencies }}
|
||||
|
||||
- name: Prevent develop dependencies
|
||||
if: inputs.dependencies
|
||||
- name: Handle develop dependencies
|
||||
run: |
|
||||
ret=0
|
||||
cat package.json | jq '.dependencies[]' | grep -q '#develop' || ret=$?
|
||||
if [ "$ret" -eq 0 ]; then
|
||||
echo "package.json contains develop dependencies. Refusing to release."
|
||||
exit
|
||||
fi
|
||||
cat package.json | jq -r '.dependencies | to_entries | .[] | "\(.key) \(.value)"' | grep '#develop$' | while read -r dep ; do
|
||||
IFS=" "
|
||||
PACKAGE=${dep[0]}
|
||||
VERSION=${dep[1]}
|
||||
|
||||
echo "::warning title=Develop dependency found::$DEPENDENCY will be kept at $VERSION"
|
||||
yarn upgrade "$PACKAGE@$VERSION" --exact
|
||||
git add -u
|
||||
git commit -m "Keep $PACKAGE at $VERSION"
|
||||
done
|
||||
|
||||
- name: Bump package.json version
|
||||
run: yarn version --no-git-tag-version --new-version "${VERSION#v}"
|
||||
|
||||
- name: Ingest upstream changes
|
||||
if: |
|
||||
inputs.include-changes &&
|
||||
(!inputs.dependencies || contains(fromJSON(steps.update-dependencies.outputs.updated), inputs.include-changes))
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
RELEASE_ID: ${{ steps.release.outputs.id }}
|
||||
DEPENDENCY: ${{ inputs.include-changes }}
|
||||
with:
|
||||
retries: 3
|
||||
script: |
|
||||
const { RELEASE_ID: releaseId, DEPENDENCY, VERSION } = process.env;
|
||||
const { owner, repo } = context.repo;
|
||||
const script = require("./.action-repo/scripts/release/merge-release-notes.js");
|
||||
const notes = await script({
|
||||
github,
|
||||
releaseId,
|
||||
dependencies: [DEPENDENCY.replace("$VERSION", VERSION)],
|
||||
});
|
||||
core.exportVariable("RELEASE_NOTES", notes);
|
||||
|
||||
- name: Add to CHANGELOG.md
|
||||
if: inputs.final
|
||||
run: |
|
||||
@@ -219,6 +158,8 @@ jobs:
|
||||
cat CHANGELOG.md.old >> CHANGELOG.md
|
||||
rm CHANGELOG.md.old
|
||||
git add CHANGELOG.md
|
||||
env:
|
||||
RELEASE_NOTES: ${{ steps.draft-release.outputs.body }}
|
||||
|
||||
- name: Run pre-release script to update package.json fields
|
||||
run: |
|
||||
@@ -237,7 +178,7 @@ jobs:
|
||||
uses: ./.action-repo/.github/actions/upload-release-assets
|
||||
with:
|
||||
gpg-fingerprint: ${{ inputs.gpg-fingerprint }}
|
||||
upload-url: ${{ steps.release.outputs.upload_url }}
|
||||
upload-url: ${{ steps.draft-release.outputs.upload_url }}
|
||||
asset-path: ${{ inputs.asset-path }}
|
||||
|
||||
- name: Create signed tag
|
||||
@@ -252,7 +193,7 @@ jobs:
|
||||
uses: ./.action-repo/.github/actions/sign-release-tarball
|
||||
with:
|
||||
gpg-fingerprint: ${{ inputs.gpg-fingerprint }}
|
||||
upload-url: ${{ steps.release.outputs.upload_url }}
|
||||
upload-url: ${{ steps.draft-release.outputs.upload_url }}
|
||||
|
||||
# We defer pushing changes until after the release assets are built,
|
||||
# signed & uploaded to improve the atomicity of this action.
|
||||
@@ -273,7 +214,7 @@ jobs:
|
||||
if: inputs.expected-asset-count
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
RELEASE_ID: ${{ steps.release.outputs.id }}
|
||||
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
||||
EXPECTED_ASSET_COUNT: ${{ inputs.expected-asset-count }}
|
||||
with:
|
||||
retries: 3
|
||||
@@ -301,7 +242,7 @@ jobs:
|
||||
- name: Publish release
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
RELEASE_ID: ${{ steps.release.outputs.id }}
|
||||
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
||||
FINAL: ${{ inputs.final }}
|
||||
with:
|
||||
retries: 3
|
||||
@@ -335,15 +276,16 @@ jobs:
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
update-labels:
|
||||
name: Advance release blocker labels
|
||||
post-release:
|
||||
name: Post release steps
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: repository
|
||||
run: echo "REPO=${GITHUB_REPOSITORY#*/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: garganshu/github-label-updater@3770d15ebfed2fe2cb06a241047bc340f774a7d1 # v1.0.0
|
||||
- name: Advance release blocker labels
|
||||
uses: garganshu/github-label-updater@3770d15ebfed2fe2cb06a241047bc340f774a7d1 # v1.0.0
|
||||
with:
|
||||
owner: ${{ github.repository_owner }}
|
||||
repo: ${{ steps.repository.outputs.REPO }}
|
||||
@@ -351,3 +293,39 @@ jobs:
|
||||
filter-labels: X-Upcoming-Release-Blocker
|
||||
remove-labels: X-Upcoming-Release-Blocker
|
||||
add-labels: X-Release-Blocker
|
||||
|
||||
# - name: Wait for master->develop gitflow merge
|
||||
# if: inputs.final
|
||||
# uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
|
||||
# with:
|
||||
# ref: master
|
||||
# repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# wait-interval: 10
|
||||
# check-name: merge
|
||||
# allowed-conclusions: success
|
||||
|
||||
bump-downstreams:
|
||||
name: Update npm dependency in downstream projects
|
||||
needs: npm
|
||||
runs-on: ubuntu-latest
|
||||
if: inputs.downstreams
|
||||
strategy:
|
||||
matrix:
|
||||
repo: ${{ fromJSON(inputs.downstreams) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ matrix.repo }}
|
||||
ref: staging
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
- name: Bump dependency
|
||||
env:
|
||||
DEPENDENCY: ${{ needs.npm.outputs.id }}
|
||||
run: |
|
||||
git config --global user.email "releases@riot.im"
|
||||
git config --global user.name "RiotRobot"
|
||||
yarn upgrade "$DEPENDENCY" --exact
|
||||
git add package.json yarn.lock
|
||||
git commit -am"Upgrade dependency to $DEPENDENCY"
|
||||
git push origin staging
|
||||
|
||||
@@ -4,10 +4,16 @@ on:
|
||||
secrets:
|
||||
NPM_TOKEN:
|
||||
required: true
|
||||
outputs:
|
||||
id:
|
||||
description: "The npm package@version string we published"
|
||||
value: ${{ jobs.npm.outputs.id }}
|
||||
jobs:
|
||||
npm:
|
||||
name: Publish to npm
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
id: ${{ steps.npm-publish.outputs.id }}
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -25,7 +31,7 @@ jobs:
|
||||
|
||||
- name: 🚀 Publish to npm
|
||||
id: npm-publish
|
||||
uses: JS-DevTools/npm-publish@4b07b26a2f6e0a51846e1870223e545bae91c552 # v3.0.1
|
||||
uses: JS-DevTools/npm-publish@19c28f1ef146469e409470805ea4279d47c3d35c # v3.1.1
|
||||
with:
|
||||
token: ${{ secrets.NPM_TOKEN }}
|
||||
access: public
|
||||
|
||||
@@ -28,6 +28,7 @@ jobs:
|
||||
with:
|
||||
final: ${{ inputs.mode == 'final' }}
|
||||
npm: ${{ inputs.npm }}
|
||||
downstreams: '["matrix-org/matrix-react-sdk", "element-hq/element-web"]'
|
||||
|
||||
docs:
|
||||
name: Publish Documentation
|
||||
@@ -38,12 +39,6 @@ jobs:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 🧮 Checkout gh-pages
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: gh-pages
|
||||
path: _docs
|
||||
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -52,25 +47,26 @@ jobs:
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --frozen-lockfile"
|
||||
|
||||
- name: 🔨 Install symlinks
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y symlinks
|
||||
|
||||
- name: 📖 Generate docs
|
||||
run: |
|
||||
yarn tpv purge --yes --out _docs --stale --major 10
|
||||
yarn gendoc
|
||||
symlinks -rc _docs
|
||||
run: yarn gendoc
|
||||
|
||||
- name: 🔨 Set up git
|
||||
run: |
|
||||
git config --global user.email "releases@riot.im"
|
||||
git config --global user.name "RiotRobot"
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: _docs
|
||||
|
||||
- name: 🚀 Deploy
|
||||
run: |
|
||||
git add . --all
|
||||
git commit -m "Update docs"
|
||||
git push
|
||||
working-directory: _docs
|
||||
docs-deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: docs
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
@@ -5,19 +5,23 @@ on:
|
||||
secrets:
|
||||
SONAR_TOKEN:
|
||||
required: true
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
inputs:
|
||||
extra_args:
|
||||
type: string
|
||||
sharded:
|
||||
type: boolean
|
||||
required: false
|
||||
description: "Extra args to pass to SonarCloud"
|
||||
description: "Whether to combine multiple LCOV and jest-sonar-report files in coverage artifact"
|
||||
jobs:
|
||||
sonarqube:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
if: |
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event != 'merge_group'
|
||||
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@faaa4d96fecf273bd762985e0e7f9f933c774918 # v1
|
||||
# This provides an easy link to this workflow_run from the PR before Sonarcloud is done.
|
||||
- uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: pending
|
||||
@@ -25,24 +29,53 @@ jobs:
|
||||
sha: ${{ github.event.workflow_run.head_sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- name: "🧮 Checkout code"
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
with:
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
if: ${{ !inputs.sharded }}
|
||||
with:
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
name: coverage
|
||||
path: coverage
|
||||
- name: 📥 Download sharded artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
if: inputs.sharded
|
||||
with:
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
pattern: coverage-*
|
||||
path: coverage
|
||||
merge-multiple: true
|
||||
|
||||
- id: extra_args
|
||||
run: |
|
||||
coverage=$(find coverage -type f -name '*lcov.info' -printf '%h/%f,' | tr -d '\r\n' | sed 's/,$//g')
|
||||
echo "sonar.javascript.lcov.reportPaths=$coverage" >> sonar-project.properties
|
||||
reports=$(find coverage -type f -name 'jest-sonar-report*.xml' -printf '%h/%f,' | tr -d '\r\n' | sed 's/,$//g')
|
||||
echo "sonar.testExecutionReportPaths=$reports" >> sonar-project.properties
|
||||
|
||||
- name: "🩻 SonarCloud Scan"
|
||||
id: sonarcloud
|
||||
uses: matrix-org/sonarcloud-workflow-action@v2.7
|
||||
uses: matrix-org/sonarcloud-workflow-action@v3.2
|
||||
# workflow_run fails report against the develop commit always, we don't want that for PRs
|
||||
continue-on-error: ${{ github.event.workflow_run.head_branch != 'develop' }}
|
||||
with:
|
||||
skip_checkout: true
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
version_cmd: "cat package.json | jq -r .version"
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
revision: ${{ github.event.workflow_run.head_sha }}
|
||||
token: ${{ secrets.SONAR_TOKEN }}
|
||||
coverage_run_id: ${{ github.event.workflow_run.id }}
|
||||
coverage_workflow_name: tests.yml
|
||||
coverage_extract_path: coverage
|
||||
extra_args: ${{ inputs.extra_args }}
|
||||
|
||||
- uses: Sibz/github-status-action@faaa4d96fecf273bd762985e0e7f9f933c774918 # v1
|
||||
- uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
|
||||
if: always()
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -8,38 +8,12 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
|
||||
cancel-in-progress: true
|
||||
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 }}
|
||||
testExecutionReportPaths: ${{ steps.extra_args.outputs.testExecutionReportPaths }}
|
||||
steps:
|
||||
# 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@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2
|
||||
with:
|
||||
workflow: tests.yaml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: coverage
|
||||
path: coverage
|
||||
|
||||
- id: extra_args
|
||||
run: |
|
||||
coverage=$(find coverage -type f -name '*lcov.info' | tr '\n' ',' | sed 's/,$//g')
|
||||
echo "reportPaths=$coverage" >> $GITHUB_OUTPUT
|
||||
reports=$(find coverage -type f -name 'jest-sonar-report*.xml' | tr '\n' ',' | sed 's/,$//g')
|
||||
echo "testExecutionReportPaths=$reports" >> $GITHUB_OUTPUT
|
||||
|
||||
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:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
with:
|
||||
extra_args: -Dsonar.javascript.lcov.reportPaths=${{ needs.prepare.outputs.reportPaths }} -Dsonar.testExecutionReportPaths=${{ needs.prepare.outputs.testExecutionReportPaths }}
|
||||
sharded: true
|
||||
|
||||
@@ -83,17 +83,26 @@ jobs:
|
||||
- name: Generate Docs
|
||||
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
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docs
|
||||
path: _docs
|
||||
# We'll only use this in a workflow_run, then we're done with it
|
||||
retention-days: 1
|
||||
|
||||
analyse_dead_code:
|
||||
name: "Analyse Dead Code"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
|
||||
- name: Run linter
|
||||
run: "yarn run lint:knip"
|
||||
|
||||
@@ -52,13 +52,13 @@ jobs:
|
||||
|
||||
- name: Move coverage files into place
|
||||
if: env.ENABLE_COVERAGE == 'true'
|
||||
run: mv coverage/lcov.info coverage/${{ steps.setupNode.output.node-version }}-${{ matrix.specs }}.lcov.info
|
||||
run: mv coverage/lcov.info coverage/${{ steps.setupNode.outputs.node-version }}-${{ matrix.specs }}.lcov.info
|
||||
|
||||
- name: Upload Artifact
|
||||
if: env.ENABLE_COVERAGE == 'true'
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage
|
||||
name: coverage-${{ matrix.specs }}-${{ matrix.node == 'lts/*' && 'lts' || matrix.node }}
|
||||
path: |
|
||||
coverage
|
||||
!coverage/lcov-report
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
steps:
|
||||
- name: Skip SonarCloud on merge queues
|
||||
if: env.ENABLE_COVERAGE == 'false'
|
||||
uses: Sibz/github-status-action@faaa4d96fecf273bd762985e0e7f9f933c774918 # v1
|
||||
uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
name: Move new issues into Issue triage board
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
automate-project-columns-next:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/add-to-project@main
|
||||
with:
|
||||
project-url: https://github.com/orgs/element-hq/projects/120
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
|
||||
+2
-1
@@ -25,5 +25,6 @@ out
|
||||
# This file is owned, parsed, and generated by allchange, which doesn't comply with prettier
|
||||
/CHANGELOG.md
|
||||
|
||||
# This file is also autogenerated
|
||||
# These files are also autogenerated
|
||||
/spec/test-utils/test-data/index.ts
|
||||
/spec/test-utils/test_indexeddb_cryptostore_dump/dump.json
|
||||
|
||||
+184
@@ -1,3 +1,186 @@
|
||||
Changes in [32.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v32.2.0) (2024-05-07)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Use a different error code for UTDs when user was not in the room ([#4172](https://github.com/matrix-org/matrix-js-sdk/pull/4172)). Contributed by @uhoreg.
|
||||
* Modernize window.crypto access constants ([#4169](https://github.com/matrix-org/matrix-js-sdk/pull/4169)). Contributed by @turt2live.
|
||||
* Improve compliance with MSC3266 ([#4155](https://github.com/matrix-org/matrix-js-sdk/pull/4155)). Contributed by @AndrewFerr.
|
||||
* Add comment to make clear that RoomStateEvent.Events does not update related objects in the js-sdk ([#4152](https://github.com/matrix-org/matrix-js-sdk/pull/4152)). Contributed by @toger5.
|
||||
* Crypto: use a new error code for UTDs from device-relative historical events ([#4139](https://github.com/matrix-org/matrix-js-sdk/pull/4139)). Contributed by @richvdh.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Element-R: Fix rust migration when ssss secret are stored not encryted in cache (old legacy behavior) ([#4168](https://github.com/matrix-org/matrix-js-sdk/pull/4168)). Contributed by @BillCarsonFr.
|
||||
|
||||
|
||||
Changes in [32.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v32.1.0) (2024-04-23)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Add support for device dehydration v2 (Element R) ([#4062](https://github.com/matrix-org/matrix-js-sdk/pull/4062)). Contributed by @uhoreg.
|
||||
* OIDC improvements in prep of OIDC-QR reciprocation ([#4149](https://github.com/matrix-org/matrix-js-sdk/pull/4149)). Contributed by @t3chguy.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Validate backup private key before migrating it ([#4114](https://github.com/matrix-org/matrix-js-sdk/pull/4114)). Contributed by @BillCarsonFr.
|
||||
* ElementR| Retry query backup until it works during migration to avoid spurious correption error popup ([#4113](https://github.com/matrix-org/matrix-js-sdk/pull/4113)). Contributed by @BillCarsonFr.
|
||||
|
||||
|
||||
Changes in [32.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v32.0.0) (2024-04-09)
|
||||
==================================================================================================
|
||||
## 🚨 BREAKING CHANGES
|
||||
|
||||
* Remove various deprecated methods \& re-exports ([#4125](https://github.com/matrix-org/matrix-js-sdk/pull/4125)). Contributed by @t3chguy.
|
||||
* Remove the logic that throws when the lazy loading options has changed. ([#4124](https://github.com/matrix-org/matrix-js-sdk/pull/4124)). Contributed by @langleyd.
|
||||
* Fix highlights from threads disappearing on new messages ([#4106](https://github.com/matrix-org/matrix-js-sdk/pull/4106)). Contributed by @dbkr.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Add new `decryptExistingEvent` test helper ([#4133](https://github.com/matrix-org/matrix-js-sdk/pull/4133)). Contributed by @richvdh.
|
||||
* Improve types for `sendEvent` ([#4108](https://github.com/matrix-org/matrix-js-sdk/pull/4108)). Contributed by @t3chguy.
|
||||
* Remove various deprecated methods \& re-exports ([#4125](https://github.com/matrix-org/matrix-js-sdk/pull/4125)). Contributed by @t3chguy.
|
||||
* Add new enum for verification methods. ([#4129](https://github.com/matrix-org/matrix-js-sdk/pull/4129)). Contributed by @richvdh.
|
||||
* Add some test utils in a new entrypoint ([#4127](https://github.com/matrix-org/matrix-js-sdk/pull/4127)). Contributed by @richvdh.
|
||||
* Improve types for `sendStateEvent` ([#4105](https://github.com/matrix-org/matrix-js-sdk/pull/4105)). Contributed by @t3chguy.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Improve types for `IPowerLevelsContent` and `hasSufficientPowerLevelFor` ([#4128](https://github.com/matrix-org/matrix-js-sdk/pull/4128)). Contributed by @galash13.
|
||||
* Remove the logic that throws when the lazy loading options has changed. ([#4124](https://github.com/matrix-org/matrix-js-sdk/pull/4124)). Contributed by @langleyd.
|
||||
* Fix highlights from threads disappearing on new messages ([#4106](https://github.com/matrix-org/matrix-js-sdk/pull/4106)). Contributed by @dbkr.
|
||||
* Extend logic for local notification processing to threads ([#4111](https://github.com/matrix-org/matrix-js-sdk/pull/4111)). Contributed by @dbkr.
|
||||
* Fix public rooms post request search params and body ([#4110](https://github.com/matrix-org/matrix-js-sdk/pull/4110)). Contributed by @ajbura.
|
||||
* Fix bugs with the first reply to a thread ([#4104](https://github.com/matrix-org/matrix-js-sdk/pull/4104)). Contributed by @dbkr.
|
||||
|
||||
|
||||
Changes in [31.6.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v31.6.1) (2024-03-28)
|
||||
==================================================================================================
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix merging of default push rules ([#4136](https://github.com/matrix-org/matrix-js-sdk/pull/4136)).
|
||||
|
||||
|
||||
Changes in [31.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v31.6.0) (2024-03-26)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Introduce Membership TS type (take 2) ([#4107](https://github.com/matrix-org/matrix-js-sdk/pull/4107)). Contributed by @andybalaam.
|
||||
* fix automatic DM avatar with functional members ([#4017](https://github.com/matrix-org/matrix-js-sdk/pull/4017)). Contributed by @HarHarLinks.
|
||||
* Export types describing all specced media event formats ([#4092](https://github.com/matrix-org/matrix-js-sdk/pull/4092)). Contributed by @t3chguy.
|
||||
* Add `.m.rule.is_room_mention` push rule to DEFAULT\_OVERRIDE\_RULES ([#4100](https://github.com/matrix-org/matrix-js-sdk/pull/4100)). Contributed by @t3chguy.
|
||||
* Make sending ContentLoaded optional for a widgetClient ([#4086](https://github.com/matrix-org/matrix-js-sdk/pull/4086)). Contributed by @toger5.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Migrate own identity local trust to rust crypto ([#4090](https://github.com/matrix-org/matrix-js-sdk/pull/4090)). Contributed by @BillCarsonFr.
|
||||
* Fix race condition with sliding sync extensions ([#4089](https://github.com/matrix-org/matrix-js-sdk/pull/4089)). Contributed by @zzorba.
|
||||
|
||||
|
||||
Changes in [31.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v31.5.0) (2024-03-12)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Update MSC2965 OIDC Discovery implementation ([#4064](https://github.com/matrix-org/matrix-js-sdk/pull/4064)). Contributed by @t3chguy.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Add basic retry for rust crypto outgoing requests ([#4061](https://github.com/matrix-org/matrix-js-sdk/pull/4061)). Contributed by @BillCarsonFr.
|
||||
|
||||
|
||||
Changes in [31.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v31.4.0) (2024-02-27)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Validate `account_management_uri` and `account_management_actions_supported` from OIDC Issuer well-known ([#4074](https://github.com/matrix-org/matrix-js-sdk/pull/4074)). Contributed by @t3chguy.
|
||||
* Allow specifying OIDC url state parameter for passing data to callback ([#4068](https://github.com/matrix-org/matrix-js-sdk/pull/4068)). Contributed by @t3chguy.
|
||||
* Add getAuthIssuer method for MSC2965 ([#4071](https://github.com/matrix-org/matrix-js-sdk/pull/4071)). Contributed by @t3chguy.
|
||||
* Allow specifying more OIDC client metadata for dynamic registration ([#4070](https://github.com/matrix-org/matrix-js-sdk/pull/4070)). Contributed by @t3chguy.
|
||||
* Add unread marker event type ([#4069](https://github.com/matrix-org/matrix-js-sdk/pull/4069)). Contributed by @dbkr.
|
||||
* Add "AsJson" forms of the key import/export methods ([#4057](https://github.com/matrix-org/matrix-js-sdk/pull/4057)). Contributed by @andybalaam.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Ignore memberships of users that are not in the call ([#4065](https://github.com/matrix-org/matrix-js-sdk/pull/4065)). Contributed by @toger5.
|
||||
* Await encrypted messages ([#4063](https://github.com/matrix-org/matrix-js-sdk/pull/4063)). Contributed by @toger5.
|
||||
* ElementR | Ensure own user and device trust are updated after migration before giving back control to the app. ([#4059](https://github.com/matrix-org/matrix-js-sdk/pull/4059)). Contributed by @BillCarsonFr.
|
||||
* Bump matrix-sdk-crypto-wasm to 4.5.0 ([#4060](https://github.com/matrix-org/matrix-js-sdk/pull/4060)). Contributed by @andybalaam.
|
||||
|
||||
|
||||
Changes in [31.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v31.3.0) (2024-02-13)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Add expire\_ts compatibility to matrixRTC ([#4032](https://github.com/matrix-org/matrix-js-sdk/pull/4032)). Contributed by @toger5.
|
||||
* Element-R: support for migration of the room list from legacy crypto ([#4036](https://github.com/matrix-org/matrix-js-sdk/pull/4036)). Contributed by @richvdh.
|
||||
* Element-R: check persistent room list for encryption config ([#4035](https://github.com/matrix-org/matrix-js-sdk/pull/4035)). Contributed by @richvdh.
|
||||
* Support optional MSC3860 redirects ([#4007](https://github.com/matrix-org/matrix-js-sdk/pull/4007)). Contributed by @turt2live.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* WebR: migrate the megolm session imported flag ([#4037](https://github.com/matrix-org/matrix-js-sdk/pull/4037)). Contributed by @BillCarsonFr.
|
||||
* ElementR: fix emoji verification stalling when both ends hit start at the same time ([#4004](https://github.com/matrix-org/matrix-js-sdk/pull/4004)). Contributed by @uhoreg.
|
||||
* Dependencies: Bump wasm bindings version to 4.3.0 ([#4042](https://github.com/matrix-org/matrix-js-sdk/pull/4042)). Contributed by @BillCarsonFr.
|
||||
* Element R: emit events when devices have changed ([#4019](https://github.com/matrix-org/matrix-js-sdk/pull/4019)). Contributed by @uhoreg.
|
||||
* ElementR: report invalid keys rather than failing to restore from backup ([#4006](https://github.com/matrix-org/matrix-js-sdk/pull/4006)). Contributed by @uhoreg.
|
||||
* Make `timeline` a getter ([#4022](https://github.com/matrix-org/matrix-js-sdk/pull/4022)). Contributed by @florianduros.
|
||||
* Implement getting verification cancellation info in Rust crypto ([#3947](https://github.com/matrix-org/matrix-js-sdk/pull/3947)). Contributed by @uhoreg.
|
||||
* Fix crypto migration for megolm sessions with no sender key ([#4024](https://github.com/matrix-org/matrix-js-sdk/pull/4024)). Contributed by @richvdh.
|
||||
|
||||
|
||||
Changes in [31.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v31.2.0) (2024-01-31)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Emit events during migration from libolm ([#3982](https://github.com/matrix-org/matrix-js-sdk/pull/3982)). Contributed by @richvdh.
|
||||
* Support for migration from from libolm ([#3978](https://github.com/matrix-org/matrix-js-sdk/pull/3978)). Contributed by @richvdh.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* ElementR | backup: call expensive `roomKeyCounts` less often ([#4015](https://github.com/matrix-org/matrix-js-sdk/pull/4015)). Contributed by @BillCarsonFr.
|
||||
* Decrypt and Import full backups in chunk with progress ([#4005](https://github.com/matrix-org/matrix-js-sdk/pull/4005)). Contributed by @BillCarsonFr.
|
||||
* Fix new threads not appearing. ([#4009](https://github.com/matrix-org/matrix-js-sdk/pull/4009)). Contributed by @dbkr.
|
||||
|
||||
|
||||
Changes in [31.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v31.1.0) (2024-01-19)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Broaden spec version support ([#4016](https://github.com/matrix-org/matrix-js-sdk/pull/4016)). Contributed by @RiotRobot.
|
||||
|
||||
|
||||
Changes in [31.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v31.0.0) (2024-01-16)
|
||||
==================================================================================================
|
||||
## 🚨 BREAKING CHANGES
|
||||
|
||||
* Bump minimum spec version to v1.5 ([#3970](https://github.com/matrix-org/matrix-js-sdk/pull/3970)). Contributed by @richvdh.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Bump minimum spec version to v1.5 ([#3970](https://github.com/matrix-org/matrix-js-sdk/pull/3970)). Contributed by @richvdh.
|
||||
* Send authenticated /versions request ([#3968](https://github.com/matrix-org/matrix-js-sdk/pull/3968)). Contributed by @dbkr.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Revert "Bump matrix-sdk-crypto-wasm to 3.6.0" ([#3991](https://github.com/matrix-org/matrix-js-sdk/pull/3991)). Contributed by @andybalaam.
|
||||
* #22606 Fix "Remove" button to users without "m.room.redaction" ([#3981](https://github.com/matrix-org/matrix-js-sdk/pull/3981)). Contributed by @rashmitpankhania.
|
||||
* ElementR: Ensure Encryption order per room ([#3973](https://github.com/matrix-org/matrix-js-sdk/pull/3973)). Contributed by @BillCarsonFr.
|
||||
* Element-R: fix `bootstrapSecretStorage` not resetting key backup when requested ([#3976](https://github.com/matrix-org/matrix-js-sdk/pull/3976)). Contributed by @uhoreg.
|
||||
|
||||
|
||||
Changes in [30.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v30.3.0) (2023-12-19)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Element-R: disable sending room key requests ([#3939](https://github.com/matrix-org/matrix-js-sdk/pull/3939)). Contributed by @richvdh.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix notifications appearing for old events ([#3946](https://github.com/matrix-org/matrix-js-sdk/pull/3946)). Contributed by @dbkr.
|
||||
* Don't back up keys that we got from backup ([#3934](https://github.com/matrix-org/matrix-js-sdk/pull/3934)). Contributed by @uhoreg.
|
||||
* Fix upload with empty Content-Type ([#3918](https://github.com/matrix-org/matrix-js-sdk/pull/3918)). Contributed by @JakubOnderka.
|
||||
* Prevent phantom notifications from events not in a room's timeline ([#3942](https://github.com/matrix-org/matrix-js-sdk/pull/3942)). Contributed by @dbkr.
|
||||
|
||||
|
||||
Changes in [30.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v30.2.0) (2023-12-05)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
@@ -36,6 +219,7 @@ Changes in [30.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v30
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Refactor & make base64 functions browser-safe ([\#3818](https://github.com/matrix-org/matrix-js-sdk/pull/3818)).
|
||||
* `IndexedDBStore.startup()` must be called after using it on `sdk.createClient` now.
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Deprecate `MatrixEvent.toJSON` ([\#3801](https://github.com/matrix-org/matrix-js-sdk/pull/3801)).
|
||||
|
||||
@@ -21,16 +21,6 @@ endpoints from before Matrix 1.1, for example.
|
||||
|
||||
# Quickstart
|
||||
|
||||
## In a browser
|
||||
|
||||
### Note, the browserify build has been removed. Please use a bundler like webpack or vite instead.
|
||||
|
||||
## In Node.js
|
||||
|
||||
Ensure you have the latest LTS version of Node.js installed.
|
||||
This library relies on `fetch` which is available in Node from v18.0.0 - it should work fine also with polyfills.
|
||||
If you wish to use a ponyfill or adapter of some sort then pass it as `fetchFn` to the MatrixClient constructor options.
|
||||
|
||||
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
|
||||
if you do not have it already.
|
||||
|
||||
@@ -47,8 +37,6 @@ 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
|
||||
@@ -58,7 +46,7 @@ await client.startClient({ initialSyncLimit: 10 });
|
||||
You can perform a call to `/sync` to get the current state of the client:
|
||||
|
||||
```javascript
|
||||
client.once("sync", function (state, prevState, res) {
|
||||
client.once(ClientEvent.sync, function (state, prevState, res) {
|
||||
if (state === "PREPARED") {
|
||||
console.log("prepared");
|
||||
} else {
|
||||
@@ -83,7 +71,7 @@ client.sendEvent("roomId", "m.room.message", content, "", (err, res) => {
|
||||
To listen for message events:
|
||||
|
||||
```javascript
|
||||
client.on("Room.timeline", function (event, room, toStartOfTimeline) {
|
||||
client.on(RoomEvent.Timeline, function (event, room, toStartOfTimeline) {
|
||||
if (event.getType() !== "m.room.message") {
|
||||
return; // only use messages
|
||||
}
|
||||
@@ -106,7 +94,7 @@ Object.keys(client.store.rooms).forEach((roomId) => {
|
||||
This SDK provides a full object model around the Matrix Client-Server API and emits
|
||||
events for incoming data and state changes. Aside from wrapping the HTTP API, it:
|
||||
|
||||
- Handles syncing (via `/initialSync` and `/events`)
|
||||
- Handles syncing (via `/sync`)
|
||||
- Handles the generation of "friendly" room and member names.
|
||||
- Handles historical `RoomMember` information (e.g. display names).
|
||||
- Manages room member state across multiple events (e.g. it handles typing, power
|
||||
@@ -127,29 +115,29 @@ events for incoming data and state changes. Aside from wrapping the HTTP API, it
|
||||
- Handles room initial sync on accepting invites.
|
||||
- Handles WebRTC calling.
|
||||
|
||||
Later versions of the SDK will:
|
||||
|
||||
- Expose a `RoomSummary` which would be suitable for a recents page.
|
||||
- Provide different pluggable storage layers (e.g. local storage, database-backed)
|
||||
|
||||
# Usage
|
||||
|
||||
## Conventions
|
||||
## Supported platforms
|
||||
|
||||
### Emitted events
|
||||
`matrix-js-sdk` can be used in either Node.js applications (ensure you have the latest LTS version of Node.js installed),
|
||||
or in browser applications, via a bundler such as Webpack or Vite.
|
||||
|
||||
The SDK will emit events using an `EventEmitter`. It also
|
||||
emits object models (e.g. `Rooms`, `RoomMembers`) when they
|
||||
are updated.
|
||||
You can also use the sdk with [Deno](https://deno.land/) (`import npm:matrix-js-sdk`) but its not officialy supported.
|
||||
|
||||
## Emitted events
|
||||
|
||||
The SDK raises notifications to the application using
|
||||
[`EventEmitter`s](https://nodejs.org/api/events.html#class-eventemitter). The `MatrixClient` itself
|
||||
implements `EventEmitter`, as do many of the high-level abstractions such as `Room` and `RoomMember`.
|
||||
|
||||
```javascript
|
||||
// Listen for low-level MatrixEvents
|
||||
client.on("event", function (event) {
|
||||
client.on(ClientEvent.Event, function (event) {
|
||||
console.log(event.getType());
|
||||
});
|
||||
|
||||
// Listen for typing changes
|
||||
client.on("RoomMember.typing", function (event, member) {
|
||||
client.on(RoomMemberEvent.Typing, function (event, member) {
|
||||
if (member.typing) {
|
||||
console.log(member.name + " is typing...");
|
||||
} else {
|
||||
@@ -161,41 +149,21 @@ client.on("RoomMember.typing", function (event, member) {
|
||||
client.startClient();
|
||||
```
|
||||
|
||||
### Promises and Callbacks
|
||||
## Entry points
|
||||
|
||||
Most of the methods in the SDK are asynchronous: they do not directly return a
|
||||
result, but instead return a [Promise](http://documentup.com/kriskowal/q/)
|
||||
which will be fulfilled in the future.
|
||||
As well as the primary entry point (`matrix-js-sdk`), there are several other entry points which may be useful:
|
||||
|
||||
The typical usage is something like:
|
||||
|
||||
```javascript
|
||||
matrixClient.someMethod(arg1, arg2).then(function(result) {
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
Alternatively, if you have a Node.js-style `callback(err, result)` function,
|
||||
you can pass the result of the promise into it with something like:
|
||||
|
||||
```javascript
|
||||
matrixClient.someMethod(arg1, arg2).nodeify(callback);
|
||||
```
|
||||
|
||||
The main thing to note is that it is problematic to discard the result of a
|
||||
promise-returning function, as that will cause exceptions to go unobserved.
|
||||
|
||||
Methods which return a promise show this in their documentation.
|
||||
|
||||
Many methods in the SDK support _both_ Node.js-style callbacks _and_ Promises,
|
||||
via an optional `callback` argument. The callback support is now deprecated:
|
||||
new methods do not include a `callback` argument, and in the future it may be
|
||||
removed from existing methods.
|
||||
| Entry point | Description |
|
||||
| ------------------------------ | --------------------------------------------------------------------------------------------------- |
|
||||
| `matrix-js-sdk` | Primary entry point. High-level functionality, and lots of historical clutter in need of a cleanup. |
|
||||
| `matrix-js-sdk/lib/crypto-api` | Cryptography functionality. |
|
||||
| `matrix-js-sdk/lib/types` | Low-level types, reflecting data structures defined in the Matrix spec. |
|
||||
| `matrix-js-sdk/lib/testing` | Test utilities, which may be useful in test code but should not be used in production code. |
|
||||
|
||||
## Examples
|
||||
|
||||
This section provides some useful code snippets which demonstrate the
|
||||
core functionality of the SDK. These examples assume the SDK is setup like this:
|
||||
core functionality of the SDK. These examples assume the SDK is set up like this:
|
||||
|
||||
```javascript
|
||||
import * as sdk from "matrix-js-sdk";
|
||||
@@ -211,10 +179,10 @@ const matrixClient = sdk.createClient({
|
||||
### Automatically join rooms when invited
|
||||
|
||||
```javascript
|
||||
matrixClient.on("RoomMember.membership", function (event, member) {
|
||||
if (member.membership === "invite" && member.userId === myUserId) {
|
||||
matrixClient.joinRoom(member.roomId).then(function () {
|
||||
console.log("Auto-joined %s", member.roomId);
|
||||
matrixClient.on(RoomEvent.MyMembership, function (room, membership, prevMembership) {
|
||||
if (membership === KnownMembership.Invite) {
|
||||
matrixClient.joinRoom(room.roomId).then(function () {
|
||||
console.log("Auto-joined %s", room.roomId);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -225,7 +193,7 @@ matrixClient.startClient();
|
||||
### Print out messages for all rooms
|
||||
|
||||
```javascript
|
||||
matrixClient.on("Room.timeline", function (event, room, toStartOfTimeline) {
|
||||
matrixClient.on(RoomEvent.Timeline, function (event, room, toStartOfTimeline) {
|
||||
if (toStartOfTimeline) {
|
||||
return; // don't print paginated results
|
||||
}
|
||||
@@ -257,7 +225,7 @@ Output:
|
||||
### Print out membership lists whenever they are changed
|
||||
|
||||
```javascript
|
||||
matrixClient.on("RoomState.members", function (event, state, member) {
|
||||
matrixClient.on(RoomStateEvent.Members, function (event, state, member) {
|
||||
const room = matrixClient.getRoom(state.roomId);
|
||||
if (!room) {
|
||||
return;
|
||||
@@ -294,7 +262,7 @@ host the API reference from the source files like this:
|
||||
|
||||
```
|
||||
$ yarn gendoc
|
||||
$ cd _docs
|
||||
$ cd docs
|
||||
$ python -m http.server 8005
|
||||
```
|
||||
|
||||
@@ -302,6 +270,9 @@ Then visit `http://localhost:8005` to see the API docs.
|
||||
|
||||
# End-to-end encryption support
|
||||
|
||||
**This section is outdated.** Use of `libolm` is deprecated and we are replacing it with support
|
||||
from the matrix-rust-sdk (https://github.com/element-hq/element-web/issues/21972).
|
||||
|
||||
The SDK supports end-to-end encryption via the Olm and Megolm protocols, using
|
||||
[libolm](https://gitlab.matrix.org/matrix-org/olm). It is left up to the
|
||||
application to make libolm available, via the `Olm` global.
|
||||
|
||||
@@ -4,6 +4,5 @@
|
||||
|
||||
# Deep dive
|
||||
|
||||
- [Release Process](release.md)
|
||||
- [Storage notes](storage-notes.md)
|
||||
- [Unverified devices](warning-on-unverified-devices.md)
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# Release Process
|
||||
|
||||
## Hotfix and off-cycle releases
|
||||
|
||||
1. Prepare the `staging` branch by using the backport automation and manually merging
|
||||
2. Go to [Releasing](#Releasing)
|
||||
|
||||
## Release candidates
|
||||
|
||||
1. Prepare the `staging` branch by running the [branch cut automation](https://github.com/vector-im/element-web/actions/workflows/release_prepare.yml)
|
||||
2. Go to [Releasing](#Releasing)
|
||||
|
||||
## Releasing
|
||||
|
||||
1. Open the [Releases page](https://github.com/matrix-org/matrix-js-sdk/releases) and inspect the draft release there
|
||||
2. Make any modifications to the release notes and tag/version as required
|
||||
3. Run [workflow](https://github.com/matrix-org/matrix-js-sdk/actions/workflows/release.yml) with the type set appropriately
|
||||
|
||||
## Artifacts
|
||||
|
||||
Releasing the Matrix JS SDK has just two artifacts:
|
||||
|
||||
- Package published to [npm](https://github.com/matrix-org/matrix-js-sdk)
|
||||
- Docs published to [Github Pages](https://matrix-org.github.io/matrix-js-sdk/)
|
||||
@@ -115,7 +115,7 @@ rl.on("line", function (line) {
|
||||
if (line.indexOf("/join ") === 0) {
|
||||
var roomIndex = line.split(" ")[1];
|
||||
viewingRoom = roomList[roomIndex];
|
||||
if (viewingRoom.getMember(myUserId).membership === "invite") {
|
||||
if (viewingRoom.getMember(myUserId).membership === KnownMembership.Invite) {
|
||||
// join the room first
|
||||
matrixClient.joinRoom(viewingRoom.roomId).then(
|
||||
function (room) {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { KnipConfig } from "knip";
|
||||
|
||||
export default {
|
||||
entry: [
|
||||
"src/index.ts",
|
||||
"src/types.ts",
|
||||
"src/browser-index.ts",
|
||||
"src/indexeddb-worker.ts",
|
||||
"scripts/**",
|
||||
"spec/**",
|
||||
"release.sh",
|
||||
// For now, we include all source files as entrypoints as we have been bad about gutwrenched imports
|
||||
"src/**",
|
||||
],
|
||||
project: ["**/*.{js,ts}"],
|
||||
ignore: ["examples/**"],
|
||||
ignoreDependencies: [
|
||||
// Required for `action-validator`
|
||||
"@action-validator/*",
|
||||
// Used for git pre-commit hooks
|
||||
"husky",
|
||||
// Used in script which only runs in environment with `@octokit/rest` installed
|
||||
"@octokit/rest",
|
||||
// Used by jest
|
||||
"jest-environment-jsdom",
|
||||
"babel-jest",
|
||||
"ts-node",
|
||||
// Used by `@babel/plugin-transform-runtime`
|
||||
"@babel/runtime",
|
||||
],
|
||||
ignoreBinaries: [
|
||||
// Used when available by reusable workflow `.github/workflows/release-make.yml`
|
||||
"dist",
|
||||
],
|
||||
ignoreExportsUsedInFile: true,
|
||||
} satisfies KnipConfig;
|
||||
+25
-25
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "30.2.0",
|
||||
"version": "32.2.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
"prepack": "yarn build",
|
||||
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||
"clean": "rimraf lib",
|
||||
"build": "yarn build:dev",
|
||||
@@ -16,9 +16,10 @@
|
||||
"gendoc": "typedoc",
|
||||
"lint": "yarn lint:types && yarn lint:js && yarn lint:workflows",
|
||||
"lint:js": "eslint --max-warnings 0 src spec && prettier --check .",
|
||||
"lint:js-fix": "prettier --loglevel=warn --write . && eslint --fix src spec",
|
||||
"lint:js-fix": "prettier --log-level=warn --write . && eslint --fix src spec",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'",
|
||||
"lint:knip": "knip",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"coverage": "yarn test --coverage"
|
||||
@@ -52,23 +53,23 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^3.1.0",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^4.9.0",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^5.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"loglevel": "^1.7.1",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-widget-api": "^1.6.0",
|
||||
"oidc-client-ts": "^2.2.4",
|
||||
"oidc-client-ts": "^3.0.1",
|
||||
"p-retry": "4",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"unhomoglyph": "^1.0.6",
|
||||
"uuid": "9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@action-validator/cli": "^0.5.3",
|
||||
"@action-validator/core": "^0.5.3",
|
||||
"@action-validator/cli": "^0.6.0",
|
||||
"@action-validator/core": "^0.6.0",
|
||||
"@babel/cli": "^7.12.10",
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/eslint-parser": "^7.12.10",
|
||||
@@ -80,9 +81,9 @@
|
||||
"@babel/plugin-transform-runtime": "^7.12.10",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/register": "^7.12.10",
|
||||
"@casualbot/jest-sonar-reporter": "2.2.7",
|
||||
"@matrix-org/olm": "3.2.15",
|
||||
"@peculiar/webcrypto": "^1.4.5",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/debug": "^4.1.7",
|
||||
@@ -91,42 +92,41 @@
|
||||
"@types/node": "18",
|
||||
"@types/sdp-transform": "^2.4.5",
|
||||
"@types/uuid": "9",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"allchange": "^1.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"babel-jest": "^29.0.0",
|
||||
"debug": "^4.3.4",
|
||||
"domexception": "^4.0.0",
|
||||
"eslint": "8.53.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jest": "^27.1.6",
|
||||
"eslint-plugin-jsdoc": "^46.0.0",
|
||||
"eslint-plugin-jest": "^28.0.0",
|
||||
"eslint-plugin-jsdoc": "^48.0.0",
|
||||
"eslint-plugin-matrix-org": "^1.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.2.17",
|
||||
"eslint-plugin-unicorn": "^49.0.0",
|
||||
"fake-indexeddb": "^5.0.0",
|
||||
"eslint-plugin-unicorn": "^52.0.0",
|
||||
"fake-indexeddb": "^5.0.2",
|
||||
"fetch-mock": "9.11.0",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
"husky": "^8.0.3",
|
||||
"husky": "^9.0.0",
|
||||
"jest": "^29.0.0",
|
||||
"jest-environment-jsdom": "^29.0.0",
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jest-mock": "^29.0.0",
|
||||
"knip": "^5.0.0",
|
||||
"lint-staged": "^15.0.2",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"prettier": "2.8.8",
|
||||
"node-fetch": "^2.7.0",
|
||||
"prettier": "3.2.5",
|
||||
"rimraf": "^5.0.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typedoc": "^0.24.0",
|
||||
"typedoc-plugin-coverage": "^2.1.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.25.10",
|
||||
"typedoc-plugin-coverage": "^3.0.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"
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"@casualbot/jest-sonar-reporter": {
|
||||
"outputDirectory": "coverage",
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Script to perform a post-release steps of matrix-js-sdk.
|
||||
#
|
||||
# Requires:
|
||||
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
|
||||
|
||||
set -e
|
||||
|
||||
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
|
||||
|
||||
if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then
|
||||
"$(dirname "$0")/scripts/release/post-merge-master.sh"
|
||||
git push origin develop
|
||||
fi
|
||||
-346
@@ -1,346 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Script to perform a release of matrix-js-sdk and downstream projects.
|
||||
#
|
||||
# Requires:
|
||||
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
|
||||
# hub; install via brew (macOS) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9
|
||||
# yarn; install via brew (macOS) or similar (https://yarnpkg.com/docs/install/)
|
||||
#
|
||||
# Note: this script is also used to release matrix-react-sdk, element-web, and element-desktop.
|
||||
|
||||
set -e
|
||||
|
||||
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
|
||||
if [[ $(command -v hub) ]] && [[ $(hub --version) =~ hub[[:space:]]version[[:space:]]([0-9]*).([0-9]*) ]]; then
|
||||
HUB_VERSION_MAJOR=${BASH_REMATCH[1]}
|
||||
HUB_VERSION_MINOR=${BASH_REMATCH[2]}
|
||||
if [[ $HUB_VERSION_MAJOR -lt 2 ]] || [[ $HUB_VERSION_MAJOR -eq 2 && $HUB_VERSION_MINOR -lt 5 ]]; then
|
||||
echo "hub version 2.5 is required, you have $HUB_VERSION_MAJOR.$HUB_VERSION_MINOR installed"
|
||||
exit
|
||||
fi
|
||||
else
|
||||
echo "hub is required: please install it"
|
||||
exit
|
||||
fi
|
||||
yarn --version > /dev/null || (echo "yarn is required: please install it"; kill $$)
|
||||
|
||||
USAGE="$0 [-x] [-c changelog_file] vX.Y.Z"
|
||||
|
||||
help() {
|
||||
cat <<EOF
|
||||
$USAGE
|
||||
|
||||
-c changelog_file: specify name of file containing changelog
|
||||
-x: skip updating the changelog
|
||||
EOF
|
||||
}
|
||||
|
||||
if ! git diff-index --quiet --cached HEAD; then
|
||||
echo "this git checkout has staged (uncommitted) changes. Refusing to release."
|
||||
exit
|
||||
fi
|
||||
|
||||
if ! git diff-files --quiet; then
|
||||
echo "this git checkout has uncommitted changes. Refusing to release."
|
||||
exit
|
||||
fi
|
||||
|
||||
skip_changelog=
|
||||
changelog_file="CHANGELOG.md"
|
||||
while getopts hc:x f; do
|
||||
case $f in
|
||||
h)
|
||||
help
|
||||
exit 0
|
||||
;;
|
||||
c)
|
||||
changelog_file="$OPTARG"
|
||||
;;
|
||||
x)
|
||||
skip_changelog=1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
shift $(expr $OPTIND - 1)
|
||||
|
||||
if [ $# -ne 1 ]; then
|
||||
echo "Usage: $USAGE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
function check_dependency {
|
||||
local depver=$(cat package.json | jq -r .dependencies[\"$1\"])
|
||||
if [ "$depver" == "null" ]; then return 0; fi
|
||||
|
||||
echo "Checking version of $1..."
|
||||
local latestver=$(yarn info -s "$1" dist-tags.next)
|
||||
if [ "$depver" != "$latestver" ]
|
||||
then
|
||||
echo "The latest version of $1 is $latestver but package.json depends on $depver."
|
||||
echo -n "Type 'u' to auto-upgrade, 'c' to continue anyway, or 'a' to abort:"
|
||||
read resp
|
||||
if [ "$resp" != "u" ] && [ "$resp" != "c" ]
|
||||
then
|
||||
echo "Aborting."
|
||||
exit 1
|
||||
fi
|
||||
if [ "$resp" == "u" ]
|
||||
then
|
||||
echo "Upgrading $1 to $latestver..."
|
||||
yarn add -E "$1@$latestver"
|
||||
git add -u
|
||||
git commit -m "Upgrade $1 to $latestver"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
function reset_dependency {
|
||||
local depver=$(cat package.json | jq -r .dependencies[\"$1\"])
|
||||
if [ "$depver" == "null" ]; then return 0; fi
|
||||
|
||||
echo "Resetting $1 to develop branch..."
|
||||
yarn add "github:matrix-org/$1#develop"
|
||||
git add -u
|
||||
git commit -m "Reset $1 back to develop branch"
|
||||
}
|
||||
|
||||
has_subprojects=0
|
||||
if [ -f release_config.yaml ]; then
|
||||
subprojects=$(cat release_config.yaml | python -c "import yaml; import sys; print(' '.join(list(yaml.load(sys.stdin)['subprojects'].keys())))" 2> /dev/null)
|
||||
if [ "$?" -eq 0 ]; then
|
||||
has_subprojects=1
|
||||
echo "Checking subprojects for upgrades"
|
||||
for proj in $subprojects; do
|
||||
check_dependency "$proj"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
ret=0
|
||||
cat package.json | jq '.dependencies[]' | grep -q '#develop' || ret=$?
|
||||
if [ "$ret" -eq 0 ]; then
|
||||
echo "package.json contains develop dependencies. Refusing to release."
|
||||
exit
|
||||
fi
|
||||
|
||||
# We use Git branch / commit dependencies for some packages, and Yarn seems
|
||||
# to have a hard time getting that right. See also
|
||||
# https://github.com/yarnpkg/yarn/issues/4734. As a workaround, we clean the
|
||||
# global cache here to ensure we get the right thing.
|
||||
yarn cache clean
|
||||
# Ensure all dependencies are updated
|
||||
yarn install --ignore-scripts --frozen-lockfile
|
||||
|
||||
# ignore leading v on release
|
||||
release="${1#v}"
|
||||
tag="v${release}"
|
||||
|
||||
prerelease=0
|
||||
# We check if this build is a prerelease by looking to
|
||||
# see if the version has a hyphen in it. Crude,
|
||||
# but semver doesn't support postreleases so anything
|
||||
# with a hyphen is a prerelease.
|
||||
echo $release | grep -q '-' && prerelease=1
|
||||
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
echo Making a PRE-RELEASE
|
||||
else
|
||||
read -p "Making a FINAL RELEASE, press enter to continue " REPLY
|
||||
fi
|
||||
|
||||
rel_branch=$(git symbolic-ref --short HEAD)
|
||||
|
||||
if [ -z "$skip_changelog" ]; then
|
||||
echo "Generating changelog"
|
||||
yarn run allchange "$release"
|
||||
read -p "Edit $changelog_file manually, or press enter to continue " REPLY
|
||||
|
||||
if [ -n "$(git ls-files --modified $changelog_file)" ]; then
|
||||
echo "Committing updated changelog"
|
||||
git commit "$changelog_file" -m "Prepare changelog for $tag"
|
||||
fi
|
||||
fi
|
||||
latest_changes=$(mktemp)
|
||||
cat "${changelog_file}" | "$(dirname "$0")/scripts/changelog_head.py" > "${latest_changes}"
|
||||
|
||||
set -x
|
||||
|
||||
# Bump package.json and build the dist
|
||||
echo "yarn version"
|
||||
# yarn version will automatically commit its modification
|
||||
# and make a release tag. We don't want it to create the tag
|
||||
# because it can only sign with the default key, but we can
|
||||
# only turn off both of these behaviours, so we have to
|
||||
# manually commit the result.
|
||||
yarn version --no-git-tag-version --new-version "$release"
|
||||
|
||||
"$(dirname "$0")/scripts/release/pre-release.sh"
|
||||
|
||||
# commit yarn.lock if it exists, is versioned, and is modified
|
||||
if [[ -f yarn.lock && $(git status --porcelain yarn.lock | grep '^ M') ]];
|
||||
then
|
||||
pkglock='yarn.lock'
|
||||
else
|
||||
pkglock=''
|
||||
fi
|
||||
git commit package.json $pkglock -m "$tag"
|
||||
|
||||
|
||||
# figure out if we should be signing this release
|
||||
signing_id=
|
||||
if [ -f release_config.yaml ]; then
|
||||
result=$(cat release_config.yaml | python -c "import yaml; import sys; print(yaml.load(sys.stdin)['signing_id'])" 2> /dev/null || true)
|
||||
if [ "$?" -eq 0 ]; then
|
||||
signing_id=$result
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
# If there is a 'dist' script in the package.json,
|
||||
# run it in a separate checkout of the project, then
|
||||
# upload any files in the 'dist' directory as release
|
||||
# assets.
|
||||
# We make a completely separate checkout to be sure
|
||||
# we're using released versions of the dependencies
|
||||
# (rather than whatever we're pulling in from yarn link)
|
||||
assets=''
|
||||
dodist=0
|
||||
jq -e .scripts.dist package.json 2> /dev/null || dodist=$?
|
||||
if [ $dodist -eq 0 ]; then
|
||||
projdir=$(pwd)
|
||||
builddir=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')
|
||||
echo "Building distribution copy in $builddir"
|
||||
pushd "$builddir"
|
||||
git clone "$projdir" .
|
||||
git checkout "$rel_branch"
|
||||
yarn install --frozen-lockfile
|
||||
# We haven't tagged yet, so tell the dist script what version
|
||||
# it's building
|
||||
DIST_VERSION="$tag" yarn dist
|
||||
|
||||
popd
|
||||
|
||||
for i in "$builddir"/dist/*; do
|
||||
assets="$assets -a $i"
|
||||
if [ -n "$signing_id" ]
|
||||
then
|
||||
gpg -u "$signing_id" --armor --output "$i".asc --detach-sig "$i"
|
||||
assets="$assets -a $i.asc"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -n "$signing_id" ]; then
|
||||
# make a signed tag
|
||||
# gnupg seems to fail to get the right tty device unless we set it here
|
||||
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=$(tty) git tag -u "$signing_id" -F "${latest_changes}" "$tag"
|
||||
else
|
||||
git tag -a -F "${latest_changes}" "$tag"
|
||||
fi
|
||||
|
||||
# push the tag and the release branch
|
||||
git push origin "$rel_branch" "$tag"
|
||||
|
||||
if [ -n "$signing_id" ]; then
|
||||
# make a signature for the source tarball.
|
||||
#
|
||||
# github will make us a tarball from the tag - we want to create a
|
||||
# signature for it, which means that first of all we need to check that
|
||||
# it's correct.
|
||||
#
|
||||
# we can't deterministically build exactly the same tarball, due to
|
||||
# differences in gzip implementation - but we *can* build the same tar - so
|
||||
# the easiest way to check the validity of the tarball from git is to unzip
|
||||
# it and compare it with our own idea of what the tar should look like.
|
||||
|
||||
# This uses git archive which seems to be what github uses. Specifically,
|
||||
# the header fields are set in the same way: same file mode, uid & gid
|
||||
# both zero and mtime set to the timestamp of the commit that the tag
|
||||
# references. Also note that this puts the commit into the tar headers
|
||||
# and can be extracted with gunzip -c foo.tar.gz | git get-tar-commit-id
|
||||
|
||||
# the name of the sig file we want to create
|
||||
source_sigfile="${tag}-src.tar.gz.asc"
|
||||
|
||||
tarfile="$tag.tar.gz"
|
||||
gh_project_url=$(git remote get-url origin |
|
||||
sed -e 's#^git@github\.com:#https://github.com/#' \
|
||||
-e 's#^git\+ssh://git@github\.com/#https://github.com/#' \
|
||||
-e 's/\.git$//')
|
||||
project_name="${gh_project_url##*/}"
|
||||
curl -L "${gh_project_url}/archive/${tarfile}" -o "${tarfile}"
|
||||
|
||||
# unzip it and compare it with the tar we would generate
|
||||
if ! cmp --silent <(gunzip -c $tarfile) \
|
||||
<(git archive --format tar --prefix="${project_name}-${release}/" "$tag"); then
|
||||
|
||||
# we don't bail out here, because really it's more likely that our comparison
|
||||
# screwed up and it's super annoying to abort the script at this point.
|
||||
cat >&2 <<EOF
|
||||
!!!!!!!!!!!!!!!!!
|
||||
!!!! WARNING !!!!
|
||||
|
||||
Mismatch between our own tarfile and that generated by github: not signing
|
||||
source tarball.
|
||||
|
||||
To resolve, determine if $tarfile is correct, and if so sign it with gpg and
|
||||
attach it to the release as $source_sigfile.
|
||||
|
||||
!!!!!!!!!!!!!!!!!
|
||||
EOF
|
||||
else
|
||||
gpg -u "$signing_id" --armor --output "$source_sigfile" --detach-sig "$tarfile"
|
||||
assets="$assets -a $source_sigfile"
|
||||
fi
|
||||
fi
|
||||
|
||||
hubflags=''
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
hubflags='-p'
|
||||
fi
|
||||
|
||||
release_text=$(mktemp)
|
||||
echo "$tag" > "${release_text}"
|
||||
echo >> "${release_text}"
|
||||
cat "${latest_changes}" >> "${release_text}"
|
||||
hub release create $hubflags $assets -F "${release_text}" "$tag"
|
||||
|
||||
if [ $dodist -eq 0 ]; then
|
||||
rm -rf "$builddir"
|
||||
fi
|
||||
rm "${release_text}"
|
||||
rm "${latest_changes}"
|
||||
|
||||
# if it is a pre-release, leave it on the release branch for now.
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
git checkout "$rel_branch"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# merge release branch to master
|
||||
echo "updating master branch"
|
||||
git checkout master
|
||||
git pull
|
||||
git merge "$rel_branch" --no-edit
|
||||
|
||||
# push master to github
|
||||
git push origin master
|
||||
|
||||
# finally, merge master back onto develop (if it exists)
|
||||
if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then
|
||||
git checkout develop
|
||||
git pull
|
||||
git merge master --no-edit
|
||||
git push origin develop
|
||||
fi
|
||||
|
||||
[ -x ./post-release.sh ] && ./post-release.sh
|
||||
|
||||
if [ $has_subprojects -eq 1 ] && [ $prerelease -eq 0 ]; then
|
||||
echo "Resetting subprojects to develop"
|
||||
for proj in $subprojects; do
|
||||
reset_dependency "$proj"
|
||||
done
|
||||
git push origin develop
|
||||
fi
|
||||
@@ -2,16 +2,70 @@
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
async function listReleases(github, owner, repo) {
|
||||
const response = await github.rest.repos.listReleases({
|
||||
owner,
|
||||
repo,
|
||||
per_page: 100,
|
||||
});
|
||||
// Filters out draft releases
|
||||
return response.data.filter((release) => !release.draft);
|
||||
}
|
||||
|
||||
// Dependency can be a tuple of dependency, from version, to version, in which case a list of releases in that range (to inclusive) will be returned
|
||||
// Or it can be a string in the form accepted by `getRelease`
|
||||
async function getReleases(github, dependency) {
|
||||
if (Array.isArray(dependency)) {
|
||||
const [dep, fromVersion, toVersion] = dependency;
|
||||
const upstreamPackageJson = getDependencyPackageJson(dep);
|
||||
const [owner, repo] = upstreamPackageJson.repository.url.split("/").slice(-2);
|
||||
|
||||
const unfilteredReleases = await listReleases(github, owner, repo);
|
||||
// Only include non-draft & non-prerelease releases, unless the to-release is a pre-release, include that one
|
||||
const releases = unfilteredReleases.filter(
|
||||
(release) => !release.prerelease || release.tag_name === `v${toVersion}`,
|
||||
);
|
||||
|
||||
const fromVersionIndex = releases.findIndex((release) => release.tag_name === `v${fromVersion}`);
|
||||
const toVersionIndex = releases.findIndex((release) => release.tag_name === `v${toVersion}`);
|
||||
|
||||
return releases.slice(toVersionIndex, fromVersionIndex);
|
||||
}
|
||||
|
||||
return [await getRelease(github, dependency)];
|
||||
}
|
||||
|
||||
// Dependency can be the name of an entry in package.json, in which case the owner, repo & version will be looked up in its own package.json
|
||||
// Or it can be a string in the form owner/repo@tag - in this case the tag is used exactly to find the release
|
||||
// Or it can be a string in the form owner/repo~tag - in this case the latest tag in the same major.minor.patch set is used to find the release
|
||||
async function getRelease(github, dependency) {
|
||||
let owner;
|
||||
let repo;
|
||||
let tag;
|
||||
if (dependency.includes("/") && dependency.includes("@")) {
|
||||
owner = dependency.split("/")[0];
|
||||
repo = dependency.split("/")[1].split("@")[0];
|
||||
tag = dependency.split("@")[1];
|
||||
|
||||
if (dependency.includes("/")) {
|
||||
let rest;
|
||||
[owner, rest] = dependency.split("/");
|
||||
|
||||
if (dependency.includes("@")) {
|
||||
[repo, tag] = rest.split("@");
|
||||
} else if (dependency.includes("~")) {
|
||||
[repo, tag] = rest.split("~");
|
||||
|
||||
if (tag.includes("-rc.")) {
|
||||
// If the tag is an RC, find the latest matching RC in the set
|
||||
try {
|
||||
const releases = await listReleases(github, owner, repo);
|
||||
const baseVersion = tag.split("-rc.")[0];
|
||||
const release = releases.find((release) => release.tag_name.startsWith(baseVersion));
|
||||
if (release) return release;
|
||||
} catch (e) {
|
||||
// Fall back to getReleaseByTag
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const upstreamPackageJson = JSON.parse(fs.readFileSync(`./node_modules/${dependency}/package.json`, "utf8"));
|
||||
const upstreamPackageJson = getDependencyPackageJson(dependency);
|
||||
[owner, repo] = upstreamPackageJson.repository.url.split("/").slice(-2);
|
||||
tag = `v${upstreamPackageJson.version}`;
|
||||
}
|
||||
@@ -24,25 +78,45 @@ async function getRelease(github, dependency) {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
function getDependencyPackageJson(dependency) {
|
||||
return JSON.parse(fs.readFileSync(`./node_modules/${dependency}/package.json`, "utf8"));
|
||||
}
|
||||
|
||||
const HEADING_PREFIX = "## ";
|
||||
|
||||
const categories = [
|
||||
"🔒 SECURITY FIXES",
|
||||
"🚨 BREAKING CHANGESd",
|
||||
"🦖 Deprecations",
|
||||
"✨ Features",
|
||||
"🐛 Bug Fixes",
|
||||
"🧰 Maintenance",
|
||||
];
|
||||
|
||||
const parseReleaseNotes = (body, sections) => {
|
||||
let heading = null;
|
||||
for (const line of body.split("\n")) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.startsWith(HEADING_PREFIX)) {
|
||||
heading = trimmed.slice(HEADING_PREFIX.length);
|
||||
if (!categories.includes(heading)) heading = null;
|
||||
continue;
|
||||
}
|
||||
if (heading && trimmed) {
|
||||
sections[heading].push(trimmed);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const main = async ({ github, releaseId, dependencies }) => {
|
||||
const { GITHUB_REPOSITORY } = process.env;
|
||||
const [owner, repo] = GITHUB_REPOSITORY.split("/");
|
||||
|
||||
const sections = new Map();
|
||||
let heading = null;
|
||||
const sections = Object.fromEntries(categories.map((cat) => [cat, []]));
|
||||
for (const dependency of dependencies) {
|
||||
const release = await getRelease(github, dependency);
|
||||
for (const line of release.body.split("\n")) {
|
||||
if (line.startsWith(HEADING_PREFIX)) {
|
||||
heading = line.trim();
|
||||
sections.set(heading, []);
|
||||
continue;
|
||||
}
|
||||
if (heading && line) {
|
||||
sections.get(heading).push(line.trim());
|
||||
}
|
||||
const releases = await getReleases(github, dependency);
|
||||
for (const release of releases) {
|
||||
parseReleaseNotes(release.body, sections);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,36 +126,22 @@ const main = async ({ github, releaseId, dependencies }) => {
|
||||
release_id: releaseId,
|
||||
});
|
||||
|
||||
const headings = ["🚨 BREAKING CHANGES", "🦖 Deprecations", "✨ Features", "🐛 Bug Fixes", "🧰 Maintenance"].map(
|
||||
(h) => HEADING_PREFIX + h,
|
||||
);
|
||||
const intro = release.body.split(HEADING_PREFIX, 2)[0].trim();
|
||||
|
||||
heading = null;
|
||||
const output = [];
|
||||
for (const line of [...release.body.split("\n"), null]) {
|
||||
if (line === null || line.startsWith(HEADING_PREFIX)) {
|
||||
// If we have a heading, and it's not the first in the list of pending headings, output the section.
|
||||
// If we're processing the last line (null) then output all remaining sections.
|
||||
while (headings.length > 0 && (line === null || (heading && headings[0] !== heading))) {
|
||||
const heading = headings.shift();
|
||||
if (sections.has(heading)) {
|
||||
output.push(heading);
|
||||
output.push(...sections.get(heading));
|
||||
}
|
||||
}
|
||||
|
||||
if (heading && sections.has(heading)) {
|
||||
const lastIsBlank = !output.at(-1)?.trim();
|
||||
if (lastIsBlank) output.pop();
|
||||
output.push(...sections.get(heading));
|
||||
if (lastIsBlank) output.push("");
|
||||
}
|
||||
heading = line;
|
||||
}
|
||||
output.push(line);
|
||||
let output = "";
|
||||
if (intro) {
|
||||
output = intro + "\n\n";
|
||||
}
|
||||
|
||||
return output.join("\n");
|
||||
for (const section in sections) {
|
||||
const lines = sections[section];
|
||||
if (!lines.length) continue;
|
||||
output += HEADING_PREFIX + section + "\n\n";
|
||||
output += lines.join("\n");
|
||||
output += "\n\n";
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
// This is just for testing locally
|
||||
|
||||
@@ -31,9 +31,12 @@ import {
|
||||
SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64,
|
||||
SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64,
|
||||
SIGNED_CROSS_SIGNING_KEYS_DATA,
|
||||
SIGNED_TEST_DEVICE_DATA,
|
||||
USER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
|
||||
} from "../../test-utils/test-data";
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
import { AccountDataAccumulator } from "../../test-utils/AccountDataAccumulator";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
@@ -78,27 +81,37 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
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,
|
||||
cryptoCallbacks: createCryptoCallbacks(),
|
||||
});
|
||||
const homeserverUrl = "https://alice-server.com";
|
||||
aliceClient = createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
userId: TEST_USER_ID,
|
||||
accessToken: "akjgkrgjs",
|
||||
deviceId: TEST_DEVICE_ID,
|
||||
cryptoCallbacks: createCryptoCallbacks(),
|
||||
});
|
||||
|
||||
syncResponder = new SyncResponder(homeserverUrl);
|
||||
e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
/** an object which intercepts `/keys/upload` requests on the test homeserver */
|
||||
new E2EKeyReceiver(homeserverUrl);
|
||||
syncResponder = new SyncResponder(homeserverUrl);
|
||||
e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
/** an object which intercepts `/keys/upload` requests on the test homeserver */
|
||||
new E2EKeyReceiver(homeserverUrl);
|
||||
|
||||
await initCrypto(aliceClient);
|
||||
});
|
||||
// Silence warnings from the backup manager
|
||||
fetchMock.getOnce(new URL("/_matrix/client/v3/room_keys/version", homeserverUrl).toString(), {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND" },
|
||||
});
|
||||
|
||||
await initCrypto(aliceClient);
|
||||
},
|
||||
/* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */
|
||||
10000,
|
||||
);
|
||||
|
||||
afterEach(async () => {
|
||||
await aliceClient.stopClient();
|
||||
@@ -236,6 +249,53 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
|
||||
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[ed25519:${SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64}]`,
|
||||
);
|
||||
});
|
||||
|
||||
it("can bootstrapCrossSigning twice", async () => {
|
||||
mockSetupCrossSigningRequests();
|
||||
|
||||
const authDict = { type: "test" };
|
||||
await bootstrapCrossSigning(authDict);
|
||||
|
||||
// a second call should do nothing except GET requests
|
||||
fetchMock.mockClear();
|
||||
await bootstrapCrossSigning(authDict);
|
||||
const calls = fetchMock.calls((url, opts) => opts.method != "GET");
|
||||
expect(calls.length).toEqual(0);
|
||||
});
|
||||
|
||||
newBackendOnly("will upload existing cross-signing keys to an established secret storage", async () => {
|
||||
// This rather obscure codepath covers the case that:
|
||||
// - 4S is set up and working
|
||||
// - our device has private cross-signing keys, but has not published them to 4S
|
||||
//
|
||||
// To arrange that, we call `bootstrapCrossSigning` on our main device, and then (pretend to) set up 4S from
|
||||
// a *different* device. Then, when we call `bootstrapCrossSigning` again, it should do the honours.
|
||||
|
||||
mockSetupCrossSigningRequests();
|
||||
const accountDataAccumulator = new AccountDataAccumulator();
|
||||
accountDataAccumulator.interceptGetAccountData();
|
||||
|
||||
const authDict = { type: "test" };
|
||||
await bootstrapCrossSigning(authDict);
|
||||
|
||||
// Pretend that another device has uploaded a 4S key
|
||||
accountDataAccumulator.accountDataEvents.set("m.secret_storage.default_key", { key: "key_id" });
|
||||
accountDataAccumulator.accountDataEvents.set("m.secret_storage.key.key_id", {
|
||||
key: "keykeykey",
|
||||
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
});
|
||||
|
||||
// Prepare for the cross-signing keys
|
||||
const p = accountDataAccumulator.interceptSetAccountData(":type(m.cross_signing..*)");
|
||||
|
||||
await bootstrapCrossSigning(authDict);
|
||||
await p;
|
||||
|
||||
// The cross-signing keys should have been uploaded
|
||||
expect(accountDataAccumulator.accountDataEvents.has("m.cross_signing.master")).toBeTruthy();
|
||||
expect(accountDataAccumulator.accountDataEvents.has("m.cross_signing.self_signing")).toBeTruthy();
|
||||
expect(accountDataAccumulator.accountDataEvents.has("m.cross_signing.user_signing")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCrossSigningStatus()", () => {
|
||||
@@ -287,6 +347,67 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
|
||||
|
||||
expect(isCrossSigningReady).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return false if identity is not trusted, even if the secrets are in 4S", async () => {
|
||||
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
|
||||
|
||||
// Complete initial sync, to get the 4S account_data events stored
|
||||
mockInitialApiRequests(aliceClient.getHomeserverUrl());
|
||||
|
||||
// For this test we need to have a well-formed 4S setup.
|
||||
const mockSecretInfo = {
|
||||
encrypted: {
|
||||
// Don't care about the actual values here, just need to be present for validation
|
||||
KeyId: {
|
||||
iv: "IVIVIVIVIVIVIV",
|
||||
ciphertext: "CIPHERTEXTB64",
|
||||
mac: "MACMACMAC",
|
||||
},
|
||||
},
|
||||
};
|
||||
syncResponder.sendOrQueueSyncResponse({
|
||||
next_batch: 1,
|
||||
account_data: {
|
||||
events: [
|
||||
{
|
||||
type: "m.secret_storage.key.KeyId",
|
||||
content: {
|
||||
algorithm: "m.secret_storage.v1.aes-hmac-sha2",
|
||||
// iv and mac not relevant for this test
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "m.secret_storage.default_key",
|
||||
content: {
|
||||
key: "KeyId",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "m.cross_signing.master",
|
||||
content: mockSecretInfo,
|
||||
},
|
||||
{
|
||||
type: "m.cross_signing.user_signing",
|
||||
content: mockSecretInfo,
|
||||
},
|
||||
{
|
||||
type: "m.cross_signing.self_signing",
|
||||
content: mockSecretInfo,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await aliceClient.startClient();
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// Sanity: ensure that the secrets are in 4S
|
||||
const status = await aliceClient.getCrypto()!.getCrossSigningStatus();
|
||||
expect(status.privateKeysInSecretStorage).toBeTruthy();
|
||||
|
||||
const isCrossSigningReady = await aliceClient.getCrypto()!.isCrossSigningReady();
|
||||
|
||||
expect(isCrossSigningReady).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCrossSigningKeyId", () => {
|
||||
@@ -339,4 +460,49 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
|
||||
expect(userSigningKeyId).toBe(getPubKey(crossSigningKeys.user_signing_key));
|
||||
});
|
||||
});
|
||||
|
||||
describe("crossSignDevice", () => {
|
||||
beforeEach(async () => {
|
||||
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
// make sure that there is another device which we can sign
|
||||
e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
|
||||
|
||||
// Complete initialsync, to get the outgoing requests going
|
||||
mockInitialApiRequests(aliceClient.getHomeserverUrl());
|
||||
syncResponder.sendOrQueueSyncResponse({ next_batch: 1 });
|
||||
await aliceClient.startClient();
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// Wait for legacy crypto to find the device
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
|
||||
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([aliceClient.getSafeUserId()]);
|
||||
expect(devices.get(aliceClient.getSafeUserId())!.has(testData.TEST_DEVICE_ID)).toBeTruthy();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("fails for an unknown device", async () => {
|
||||
await expect(aliceClient.getCrypto()!.crossSignDevice("unknown")).rejects.toThrow("Unknown device");
|
||||
});
|
||||
|
||||
it("cross-signs the device", async () => {
|
||||
mockSetupCrossSigningRequests();
|
||||
await aliceClient.getCrypto()!.bootstrapCrossSigning({});
|
||||
|
||||
fetchMock.mockClear();
|
||||
await aliceClient.getCrypto()!.crossSignDevice(testData.TEST_DEVICE_ID);
|
||||
|
||||
// check that a sig for the device was uploaded
|
||||
const calls = fetchMock.calls("upload-sigs");
|
||||
expect(calls.length).toEqual(1);
|
||||
const body = JSON.parse(calls[0][1]!.body as string);
|
||||
const deviceSig = body[aliceClient.getSafeUserId()][testData.TEST_DEVICE_ID];
|
||||
expect(deviceSig).toHaveProperty("signatures");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+473
-171
@@ -26,9 +26,11 @@ import * as testUtils from "../../test-utils/test-utils";
|
||||
import {
|
||||
advanceTimersUntil,
|
||||
CRYPTO_BACKENDS,
|
||||
emitPromise,
|
||||
getSyncResponse,
|
||||
InitCrypto,
|
||||
mkEventCustom,
|
||||
mkMembershipCustom,
|
||||
syncPromise,
|
||||
} from "../../test-utils/test-utils";
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
@@ -38,6 +40,7 @@ import {
|
||||
BOB_TEST_USER_ID,
|
||||
SIGNED_CROSS_SIGNING_KEYS_DATA,
|
||||
SIGNED_TEST_DEVICE_DATA,
|
||||
TEST_ROOM_ID,
|
||||
TEST_ROOM_ID as ROOM_ID,
|
||||
TEST_USER_ID,
|
||||
} from "../../test-utils/test-data";
|
||||
@@ -48,6 +51,7 @@ import {
|
||||
ClientEvent,
|
||||
createClient,
|
||||
CryptoEvent,
|
||||
HistoryVisibility,
|
||||
IClaimOTKsResult,
|
||||
IContent,
|
||||
IDownloadKeyResult,
|
||||
@@ -57,11 +61,11 @@ import {
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
MsgType,
|
||||
PendingEventOrdering,
|
||||
Room,
|
||||
RoomMember,
|
||||
RoomStateEvent,
|
||||
HistoryVisibility,
|
||||
} from "../../../src/matrix";
|
||||
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
|
||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
@@ -74,16 +78,16 @@ import {
|
||||
mockSetupCrossSigningRequests,
|
||||
mockSetupMegolmBackupRequests,
|
||||
} from "../../test-utils/mockEndpoints";
|
||||
import { AddSecretStorageKeyOpts } from "../../../src/secret-storage";
|
||||
import { SecretStorageKeyDescription } from "../../../src/secret-storage";
|
||||
import {
|
||||
CrossSigningKey,
|
||||
CryptoCallbacks,
|
||||
DecryptionFailureCode,
|
||||
EventShieldColour,
|
||||
EventShieldReason,
|
||||
KeyBackupInfo,
|
||||
} from "../../../src/crypto-api";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
import { DecryptionError } from "../../../src/crypto/algorithms";
|
||||
import { IKeyBackup } from "../../../src/crypto/backup";
|
||||
import {
|
||||
createOlmAccount,
|
||||
@@ -96,12 +100,17 @@ import {
|
||||
getTestOlmAccountKeys,
|
||||
} from "./olm-utils";
|
||||
import { ToDevicePayload } from "../../../src/models/ToDeviceMessage";
|
||||
import { AccountDataAccumulator } from "../../test-utils/AccountDataAccumulator";
|
||||
import { UNSIGNED_MEMBERSHIP_FIELD } from "../../../src/@types/event";
|
||||
import { KnownMembership } from "../../../src/@types/membership";
|
||||
|
||||
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();
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -229,9 +238,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
/** an object which intercepts `/keys/upload` requests from {@link #aliceClient} to catch the uploaded keys */
|
||||
let keyReceiver: E2EKeyReceiver;
|
||||
|
||||
/** an object which intercepts `/keys/query` requests on the test homeserver */
|
||||
let keyResponder: E2EKeyResponder;
|
||||
|
||||
/** an object which intercepts `/sync` requests from {@link #aliceClient} */
|
||||
let syncResponder: ISyncResponder;
|
||||
|
||||
@@ -339,7 +345,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
function createCryptoCallbacks(): CryptoCallbacks {
|
||||
// Store the cached secret storage key and return it when `getSecretStorageKey` is called
|
||||
let cachedKey: { keyId: string; key: Uint8Array };
|
||||
const cacheSecretStorageKey = (keyId: string, keyInfo: AddSecretStorageKeyOpts, key: Uint8Array) => {
|
||||
const cacheSecretStorageKey = (keyId: string, keyInfo: SecretStorageKeyDescription, key: Uint8Array) => {
|
||||
cachedKey = {
|
||||
keyId,
|
||||
key,
|
||||
@@ -367,6 +373,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
accessToken: "akjgkrgjs",
|
||||
deviceId: "xzcvb",
|
||||
cryptoCallbacks: createCryptoCallbacks(),
|
||||
logger: logger.getChild("aliceClient"),
|
||||
});
|
||||
|
||||
/* set up listeners for /keys/upload and /sync */
|
||||
@@ -397,6 +404,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
expect(aliceClient.getCrypto()).toHaveProperty("globalBlacklistUnverifiedDevices");
|
||||
});
|
||||
|
||||
it("CryptoAPI.getOwnDeviceKeys returns plausible values", async () => {
|
||||
const deviceKeys = await aliceClient.getCrypto()!.getOwnDeviceKeys();
|
||||
// We just check for a 43-character base64 string
|
||||
expect(deviceKeys.curve25519).toMatch(/^[A-Za-z0-9+/]{43}$/);
|
||||
expect(deviceKeys.ed25519).toMatch(/^[A-Za-z0-9+/]{43}$/);
|
||||
});
|
||||
|
||||
it("Alice receives a megolm message", async () => {
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
@@ -456,56 +470,58 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
});
|
||||
|
||||
describe("Unable to decrypt error codes", function () {
|
||||
it("Encryption fails with expected UISI error", async () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
});
|
||||
|
||||
it("Decryption fails with UISI error", async () => {
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
const awaitUISI = new Promise<void>((resolve) => {
|
||||
aliceClient.on(MatrixEventEvent.Decrypted, (ev, err) => {
|
||||
const error = err as DecryptionError;
|
||||
if (error.code == "MEGOLM_UNKNOWN_INBOUND_SESSION_ID") {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
// A promise which resolves, with the MatrixEvent which wraps the event, once the decryption fails.
|
||||
const awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted);
|
||||
|
||||
// Ensure that the timestamp post-dates the creation of our device
|
||||
const encryptedEvent = {
|
||||
...testData.ENCRYPTED_EVENT,
|
||||
origin_server_ts: Date.now(),
|
||||
};
|
||||
|
||||
// Alice gets both the events in a single sync
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: {
|
||||
join: {
|
||||
[testData.TEST_ROOM_ID]: { timeline: { events: [testData.ENCRYPTED_EVENT] } },
|
||||
[testData.TEST_ROOM_ID]: { timeline: { events: [encryptedEvent] } },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
await awaitUISI;
|
||||
const ev = await awaitDecryption;
|
||||
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID);
|
||||
});
|
||||
|
||||
it("Encryption fails with expected Unknown Index error", async () => {
|
||||
it("Decryption fails with Unknown Index error", async () => {
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
const awaitUnknownIndex = new Promise<void>((resolve) => {
|
||||
aliceClient.on(MatrixEventEvent.Decrypted, (ev, err) => {
|
||||
const error = err as DecryptionError;
|
||||
if (error.code == "OLM_UNKNOWN_MESSAGE_INDEX") {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
// A promise which resolves, with the MatrixEvent which wraps the event, once the decryption fails.
|
||||
const awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted);
|
||||
|
||||
await aliceClient.getCrypto()!.importRoomKeys([testData.RATCHTED_MEGOLM_SESSION_DATA]);
|
||||
|
||||
// Alice gets both the events in a single sync
|
||||
// Ensure that the timestamp post-dates the creation of our device
|
||||
const encryptedEvent = {
|
||||
...testData.ENCRYPTED_EVENT,
|
||||
origin_server_ts: Date.now(),
|
||||
};
|
||||
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: {
|
||||
join: {
|
||||
[testData.TEST_ROOM_ID]: { timeline: { events: [testData.ENCRYPTED_EVENT] } },
|
||||
[testData.TEST_ROOM_ID]: { timeline: { events: [encryptedEvent] } },
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -513,23 +529,168 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
syncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
await awaitUnknownIndex;
|
||||
const ev = await awaitDecryption;
|
||||
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX);
|
||||
});
|
||||
|
||||
it("Encryption fails with Unable to decrypt for other errors", async () => {
|
||||
describe("Historical events", () => {
|
||||
async function sendEventAndAwaitDecryption(props: Partial<IEvent> = {}): Promise<MatrixEvent> {
|
||||
// A promise which resolves, with the MatrixEvent which wraps the event, once the decryption fails.
|
||||
const awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted);
|
||||
|
||||
// Ensure that the timestamp pre-dates the creation of our device: set it to 24 hours ago
|
||||
const encryptedEvent = {
|
||||
...testData.ENCRYPTED_EVENT,
|
||||
origin_server_ts: Date.now() - 24 * 3600 * 1000,
|
||||
...props,
|
||||
};
|
||||
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: {
|
||||
join: {
|
||||
[testData.TEST_ROOM_ID]: { timeline: { events: [encryptedEvent] } },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
return await awaitDecryption;
|
||||
}
|
||||
|
||||
newBackendOnly("fails with HISTORICAL_MESSAGE_BACKUP_NO_BACKUP when there is no backup", async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
|
||||
});
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
const ev = await sendEventAndAwaitDecryption();
|
||||
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP);
|
||||
});
|
||||
|
||||
newBackendOnly("fails with HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED when the backup is broken", async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {});
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
const ev = await sendEventAndAwaitDecryption();
|
||||
expect(ev.decryptionFailureReason).toEqual(
|
||||
DecryptionFailureCode.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED,
|
||||
);
|
||||
});
|
||||
|
||||
newBackendOnly("fails with HISTORICAL_MESSAGE_WORKING_BACKUP when backup is working", async () => {
|
||||
// The test backup data is signed by a dummy device. We'll need to tell Alice about the device, and
|
||||
// later, tell her to trust it, so that she trusts the backup.
|
||||
const e2eResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
|
||||
e2eResponder.addDeviceKeys(testData.SIGNED_TEST_DEVICE_DATA);
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
await aliceClient
|
||||
.getCrypto()!
|
||||
.storeSessionBackupPrivateKey(
|
||||
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
|
||||
testData.SIGNED_BACKUP_DATA.version!,
|
||||
);
|
||||
|
||||
// Tell Alice to trust the dummy device that signed the backup
|
||||
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
|
||||
expect(devices.get(TEST_USER_ID)!.keys()).toContain(testData.TEST_DEVICE_ID);
|
||||
await aliceClient.getCrypto()!.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
// Tell Alice to check and enable backup
|
||||
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
|
||||
|
||||
// Sanity: Alice should now have working backup.
|
||||
expect(await aliceClient.getCrypto()!.getActiveSessionBackupVersion()).toEqual(
|
||||
testData.SIGNED_BACKUP_DATA.version,
|
||||
);
|
||||
|
||||
// Finally! we can check what happens when we get an event.
|
||||
const ev = await sendEventAndAwaitDecryption();
|
||||
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP);
|
||||
});
|
||||
|
||||
newBackendOnly("fails with NOT_JOINED if user is not member of room", async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
|
||||
});
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
const ev = await sendEventAndAwaitDecryption({
|
||||
unsigned: {
|
||||
[UNSIGNED_MEMBERSHIP_FIELD.name]: "leave",
|
||||
},
|
||||
});
|
||||
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED);
|
||||
});
|
||||
|
||||
newBackendOnly(
|
||||
"fails with another error when the server reports user was a member of the room",
|
||||
async () => {
|
||||
// This tests that when the server reports that the user
|
||||
// was invited at the time the event was sent, then we
|
||||
// don't get a HISTORICAL_MESSAGE_USER_NOT_JOINED error,
|
||||
// and instead get some other error, since the user should
|
||||
// have gotten the key for the event.
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
|
||||
});
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
const ev = await sendEventAndAwaitDecryption({
|
||||
unsigned: {
|
||||
[UNSIGNED_MEMBERSHIP_FIELD.name]: "invite",
|
||||
},
|
||||
});
|
||||
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP);
|
||||
},
|
||||
);
|
||||
|
||||
newBackendOnly(
|
||||
"fails with another error when the server reports user was a member of the room",
|
||||
async () => {
|
||||
// This tests that when the server reports the user's
|
||||
// membership, and reports that the user was joined, then we
|
||||
// don't get a HISTORICAL_MESSAGE_USER_NOT_JOINED error, and
|
||||
// instead get some other error.
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
|
||||
});
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
const ev = await sendEventAndAwaitDecryption({
|
||||
unsigned: {
|
||||
[UNSIGNED_MEMBERSHIP_FIELD.name]: "join",
|
||||
},
|
||||
});
|
||||
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("Decryption fails with Unable to decrypt for other errors", async () => {
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
await aliceClient.getCrypto()!.importRoomKeys([testData.MEGOLM_SESSION_DATA]);
|
||||
|
||||
const awaitDecryptionError = new Promise<void>((resolve) => {
|
||||
aliceClient.on(MatrixEventEvent.Decrypted, (ev, err) => {
|
||||
const error = err as DecryptionError;
|
||||
aliceClient.on(MatrixEventEvent.Decrypted, (ev) => {
|
||||
// rust and libolm can't have an exact 1:1 mapping for all errors,
|
||||
// but some errors are part of API and should match
|
||||
if (
|
||||
error.code != "MEGOLM_UNKNOWN_INBOUND_SESSION_ID" &&
|
||||
error.code != "OLM_UNKNOWN_MESSAGE_INDEX"
|
||||
ev.decryptionFailureReason !== DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID &&
|
||||
ev.decryptionFailureReason !== DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX
|
||||
) {
|
||||
resolve();
|
||||
}
|
||||
@@ -693,7 +854,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
|
||||
it("prepareToEncrypt", async () => {
|
||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||
keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
keyResponder.addKeyReceiver("@alice:localhost", keyReceiver);
|
||||
|
||||
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
|
||||
@@ -724,7 +885,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
it("Alice sends a megolm message with GlobalErrorOnUnknownDevices=false", async () => {
|
||||
aliceClient.setGlobalErrorOnUnknownDevices(false);
|
||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||
keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
keyResponder.addKeyReceiver("@alice:localhost", keyReceiver);
|
||||
|
||||
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
|
||||
@@ -752,7 +913,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
it("We should start a new megolm session after forceDiscardSession", async () => {
|
||||
aliceClient.setGlobalErrorOnUnknownDevices(false);
|
||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||
keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
keyResponder.addKeyReceiver("@alice:localhost", keyReceiver);
|
||||
|
||||
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
|
||||
@@ -999,10 +1160,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
return encryptedMessage;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
newBackendOnly("should rotate the session after 2 messages", async () => {
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
@@ -1055,8 +1212,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
await startClientAndAwaitFirstSync();
|
||||
const p2pSession = await establishOlmSession(aliceClient, keyReceiver, syncResponder, testOlmAccount);
|
||||
|
||||
// We need to fake the timers to advance the time
|
||||
jest.useFakeTimers();
|
||||
// We need to fake the timers to advance the time, but the wasm bindings of matrix-sdk-crypto rely on a
|
||||
// working `queueMicrotask`
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
const syncResponse = getSyncResponse(["@bob:xyz"]);
|
||||
|
||||
@@ -1233,7 +1391,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
content: { algorithm: "m.megolm.v1.aes-sha2" },
|
||||
}),
|
||||
testUtils.mkMembership({
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
sender: aliceClient.getUserId()!,
|
||||
}),
|
||||
],
|
||||
@@ -1370,7 +1528,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
});
|
||||
expect(decryptedEvent.getContent().body).toEqual("42");
|
||||
|
||||
const exported = await aliceClient.exportRoomKeys();
|
||||
const exported = await aliceClient.getCrypto()!.exportRoomKeysAsJson();
|
||||
|
||||
// start a new client
|
||||
await aliceClient.stopClient();
|
||||
@@ -1386,7 +1544,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
keyReceiver = new E2EKeyReceiver(homeserverUrl);
|
||||
syncResponder = new SyncResponder(homeserverUrl);
|
||||
await initCrypto(aliceClient);
|
||||
await aliceClient.importRoomKeys(exported);
|
||||
await aliceClient.getCrypto()!.importRoomKeysAsJson(exported);
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
@@ -1652,7 +1810,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
type: "m.room.member",
|
||||
state_key: "@alice:localhost",
|
||||
content: {
|
||||
membership: "invite",
|
||||
membership: KnownMembership.Invite,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -1801,7 +1959,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
type: "m.room.member",
|
||||
state_key: "@alice:localhost",
|
||||
content: {
|
||||
membership: "invite",
|
||||
membership: KnownMembership.Invite,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -1877,7 +2035,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
{
|
||||
type: "m.room.member",
|
||||
state_key: aliceClient.getUserId(),
|
||||
content: { membership: "join" },
|
||||
content: { membership: KnownMembership.Join },
|
||||
event_id: "$alijoin",
|
||||
},
|
||||
],
|
||||
@@ -1904,7 +2062,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
{
|
||||
type: "m.room.member",
|
||||
state_key: "@other:user",
|
||||
content: { membership: "invite" },
|
||||
content: { membership: KnownMembership.Invite },
|
||||
event_id: "$otherinvite",
|
||||
},
|
||||
],
|
||||
@@ -1918,7 +2076,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
expectAliceKeyQuery({ device_keys: { "@other:user": {} }, failures: {} });
|
||||
aliceClient.on(RoomStateEvent.NewMember, (_e, _s, member: RoomMember) => {
|
||||
if (member.userId == "@other:user") {
|
||||
aliceClient.sendMessage(testRoomId, { msgtype: "m.text", body: "Hello, World" });
|
||||
aliceClient.sendMessage(testRoomId, { msgtype: MsgType.Text, body: "Hello, World" });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2052,7 +2210,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
fetchMock.getOnce(new RegExp(membersPath), {
|
||||
chunk: [
|
||||
testUtils.mkMembershipCustom({
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
sender: "@bob:xyz",
|
||||
}),
|
||||
],
|
||||
@@ -2061,7 +2219,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
|
||||
it("Sending an event initiates a member list sync", async () => {
|
||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||
keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
keyResponder.addKeyReceiver("@alice:localhost", keyReceiver);
|
||||
|
||||
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
|
||||
@@ -2084,7 +2242,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
|
||||
it("loading the membership list inhibits a later load", async () => {
|
||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||
keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
keyResponder.addKeyReceiver("@alice:localhost", keyReceiver);
|
||||
|
||||
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
|
||||
@@ -2181,11 +2339,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
|
||||
describe("key upload request", () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
});
|
||||
|
||||
function awaitKeyUploadRequest(): Promise<{ keysCount: number; fallbackKeysCount: number }> {
|
||||
@@ -2250,10 +2405,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
});
|
||||
|
||||
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 = {
|
||||
@@ -2381,8 +2532,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
expect(devicesInfo.get(user)?.size).toBeFalsy();
|
||||
});
|
||||
|
||||
it("Get devices from tacked users", async () => {
|
||||
jest.useFakeTimers();
|
||||
it("Get devices from tracked users", async () => {
|
||||
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
@@ -2425,12 +2577,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
});
|
||||
|
||||
describe("Secret Storage and Key Backup", () => {
|
||||
/**
|
||||
* The account data events to be returned by the sync.
|
||||
* Will be updated when fecthMock intercepts calls to PUT `/_matrix/client/v3/user/:userId/account_data/`.
|
||||
* Will be used by `sendSyncResponseWithUpdatedAccountData`
|
||||
*/
|
||||
let accountDataEvents: Map<String, any>;
|
||||
let accountDataAccumulator: AccountDataAccumulator;
|
||||
|
||||
/**
|
||||
* Create a fake secret storage key
|
||||
@@ -2443,76 +2590,19 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
|
||||
beforeEach(async () => {
|
||||
createSecretStorageKey.mockClear();
|
||||
accountDataEvents = new Map();
|
||||
accountDataAccumulator = new AccountDataAccumulator();
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
});
|
||||
|
||||
function mockGetAccountData() {
|
||||
fetchMock.get(
|
||||
`path:/_matrix/client/v3/user/:userId/account_data/:type`,
|
||||
(url) => {
|
||||
const type = url.split("/").pop();
|
||||
const existing = accountDataEvents.get(type!);
|
||||
if (existing) {
|
||||
// return it
|
||||
return {
|
||||
status: 200,
|
||||
body: existing.content,
|
||||
};
|
||||
} else {
|
||||
// 404
|
||||
return {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
|
||||
};
|
||||
}
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mock to respond to the PUT request `/_matrix/client/v3/user/:userId/account_data/m.cross_signing.${key}`
|
||||
* Resolved when the cross signing key is uploaded
|
||||
* https://spec.matrix.org/v1.6/client-server-api/#put_matrixclientv3useruseridaccount_datatype
|
||||
*/
|
||||
function awaitCrossSigningKeyUpload(key: string): Promise<Record<string, {}>> {
|
||||
return new Promise((resolve) => {
|
||||
// Called when the cross signing key is uploaded
|
||||
fetchMock.put(
|
||||
`express:/_matrix/client/v3/user/:userId/account_data/m.cross_signing.${key}`,
|
||||
(url: string, options: RequestInit) => {
|
||||
const content = JSON.parse(options.body as string);
|
||||
const type = url.split("/").pop();
|
||||
// update account data for sync response
|
||||
accountDataEvents.set(type!, content);
|
||||
resolve(content.encrypted);
|
||||
return {};
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send in the sync response the current account data events, as stored by `accountDataEvents`.
|
||||
*/
|
||||
function sendSyncResponseWithUpdatedAccountData() {
|
||||
try {
|
||||
syncResponder.sendOrQueueSyncResponse({
|
||||
next_batch: 1,
|
||||
account_data: {
|
||||
events: Array.from(accountDataEvents, ([type, content]) => ({
|
||||
type: type,
|
||||
content: content,
|
||||
})),
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
// Might fail with "Cannot queue more than one /sync response" if called too often.
|
||||
// It's ok if it fails here, the sync response is cumulative and will contain
|
||||
// the latest account data.
|
||||
}
|
||||
async function awaitCrossSigningKeyUpload(key: string): Promise<Record<string, {}>> {
|
||||
const content = await accountDataAccumulator.interceptSetAccountData(`m.cross_signing.${key}`);
|
||||
return content.encrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2520,28 +2610,18 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
* Resolved when a key is uploaded (ie in `body.content.key`)
|
||||
* https://spec.matrix.org/v1.6/client-server-api/#put_matrixclientv3useruseridaccount_datatype
|
||||
*/
|
||||
function awaitSecretStorageKeyStoredInAccountData(): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
// This url is called multiple times during the secret storage bootstrap process
|
||||
// When we received the newly generated key, we return it
|
||||
fetchMock.put(
|
||||
"express:/_matrix/client/v3/user/:userId/account_data/:type(m.secret_storage.*)",
|
||||
(url: string, options: RequestInit) => {
|
||||
const type = url.split("/").pop();
|
||||
const content = JSON.parse(options.body as string);
|
||||
|
||||
// update account data for sync response
|
||||
accountDataEvents.set(type!, content);
|
||||
|
||||
if (content.key) {
|
||||
resolve(content.key);
|
||||
}
|
||||
sendSyncResponseWithUpdatedAccountData();
|
||||
return {};
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
});
|
||||
async function awaitSecretStorageKeyStoredInAccountData(): Promise<string> {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const content = await accountDataAccumulator.interceptSetAccountData(":type(m.secret_storage.*)", {
|
||||
repeat: 1,
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
accountDataAccumulator.sendSyncResponseWithUpdatedAccountData(syncResponder);
|
||||
if (content.key) {
|
||||
return content.key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function awaitMegolmBackupKeyUpload(): Promise<Record<string, {}>> {
|
||||
@@ -2552,7 +2632,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
(url: string, options: RequestInit) => {
|
||||
const content = JSON.parse(options.body as string);
|
||||
// update account data for sync response
|
||||
accountDataEvents.set("m.megolm_backup.v1", content);
|
||||
accountDataAccumulator.accountDataEvents.set("m.megolm_backup.v1", content);
|
||||
resolve(content.encrypted);
|
||||
return {};
|
||||
},
|
||||
@@ -2617,7 +2697,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
await bootstrapPromise;
|
||||
|
||||
// Return the newly created key in the sync response
|
||||
sendSyncResponseWithUpdatedAccountData();
|
||||
accountDataAccumulator.sendSyncResponseWithUpdatedAccountData(syncResponder);
|
||||
|
||||
// Finally ensure backup is working
|
||||
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
|
||||
@@ -2625,6 +2705,30 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
await backupStatusUpdate;
|
||||
}
|
||||
|
||||
describe("Generate 4S recovery keys", () => {
|
||||
it("should create a random recovery key", async () => {
|
||||
const generatedKey = await aliceClient.getCrypto()!.createRecoveryKeyFromPassphrase();
|
||||
expect(generatedKey.privateKey).toBeDefined();
|
||||
expect(generatedKey.privateKey).toBeInstanceOf(Uint8Array);
|
||||
expect(generatedKey.privateKey.length).toBe(32);
|
||||
expect(generatedKey.keyInfo?.passphrase).toBeUndefined();
|
||||
expect(generatedKey.encodedPrivateKey).toBeDefined();
|
||||
expect(generatedKey.encodedPrivateKey!.indexOf("Es")).toBe(0);
|
||||
});
|
||||
|
||||
it("should create a recovery key from passphrase", async () => {
|
||||
const generatedKey = await aliceClient.getCrypto()!.createRecoveryKeyFromPassphrase("mypassphrase");
|
||||
expect(generatedKey.privateKey).toBeDefined();
|
||||
expect(generatedKey.privateKey).toBeInstanceOf(Uint8Array);
|
||||
expect(generatedKey.privateKey.length).toBe(32);
|
||||
expect(generatedKey.keyInfo?.passphrase?.algorithm).toBe("m.pbkdf2");
|
||||
expect(generatedKey.keyInfo?.passphrase?.iterations).toBe(500000);
|
||||
|
||||
expect(generatedKey.encodedPrivateKey).toBeDefined();
|
||||
expect(generatedKey.encodedPrivateKey!.indexOf("Es")).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bootstrapSecretStorage", () => {
|
||||
// Doesn't work with legacy crypto, which will try to bootstrap even without private key, which is buggy.
|
||||
newBackendOnly(
|
||||
@@ -2639,7 +2743,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
);
|
||||
|
||||
it("Should create a 4S key", async () => {
|
||||
mockGetAccountData();
|
||||
accountDataAccumulator.interceptGetAccountData();
|
||||
|
||||
const awaitAccountData = awaitAccountDataUpdate("m.secret_storage.default_key");
|
||||
|
||||
@@ -2650,8 +2754,16 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
// Wait for the key to be uploaded in the account data
|
||||
const secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
|
||||
|
||||
// check that the key content contains the key check info
|
||||
const keyContent = accountDataAccumulator.accountDataEvents.get(
|
||||
`m.secret_storage.key.${secretStorageKey}`,
|
||||
)!;
|
||||
// In order to verify if the key is valid, a zero secret is encrypted with the key
|
||||
expect(keyContent.iv).toBeDefined();
|
||||
expect(keyContent.mac).toBeDefined();
|
||||
|
||||
// Return the newly created key in the sync response
|
||||
sendSyncResponseWithUpdatedAccountData();
|
||||
accountDataAccumulator.sendSyncResponseWithUpdatedAccountData(syncResponder);
|
||||
|
||||
// Finally, wait for bootstrapSecretStorage to finished
|
||||
await bootstrapPromise;
|
||||
@@ -2675,7 +2787,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
await awaitSecretStorageKeyStoredInAccountData();
|
||||
|
||||
// Return the newly created key in the sync response
|
||||
sendSyncResponseWithUpdatedAccountData();
|
||||
accountDataAccumulator.sendSyncResponseWithUpdatedAccountData(syncResponder);
|
||||
|
||||
// Wait for bootstrapSecretStorage to finished
|
||||
await bootstrapPromise;
|
||||
@@ -2699,7 +2811,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
await awaitSecretStorageKeyStoredInAccountData();
|
||||
|
||||
// Return the newly created key in the sync response
|
||||
sendSyncResponseWithUpdatedAccountData();
|
||||
accountDataAccumulator.sendSyncResponseWithUpdatedAccountData(syncResponder);
|
||||
|
||||
// Wait for bootstrapSecretStorage to finished
|
||||
await bootstrapPromise;
|
||||
@@ -2713,7 +2825,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
await awaitSecretStorageKeyStoredInAccountData();
|
||||
|
||||
// Return the newly created key in the sync response
|
||||
sendSyncResponseWithUpdatedAccountData();
|
||||
accountDataAccumulator.sendSyncResponseWithUpdatedAccountData(syncResponder);
|
||||
|
||||
// Wait for bootstrapSecretStorage to finished
|
||||
await bootstrapPromise;
|
||||
@@ -2737,7 +2849,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
const secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
|
||||
|
||||
// Return the newly created key in the sync response
|
||||
sendSyncResponseWithUpdatedAccountData();
|
||||
accountDataAccumulator.sendSyncResponseWithUpdatedAccountData(syncResponder);
|
||||
|
||||
// Wait for the cross signing keys to be uploaded
|
||||
const [masterKey, userSigningKey, selfSigningKey] = await Promise.all([
|
||||
@@ -2777,11 +2889,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
|
||||
describe("Manage Key Backup", () => {
|
||||
beforeEach(async () => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
});
|
||||
|
||||
it("Should be able to restore from 4S after bootstrap", async () => {
|
||||
@@ -2890,6 +2999,19 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
|
||||
const newBackupUploadPromise = awaitMegolmBackupKeyUpload();
|
||||
|
||||
// Track calls to scheduleAllGroupSessionsForBackup. This is
|
||||
// only relevant on legacy encryption.
|
||||
const scheduleAllGroupSessionsForBackup = jest.fn();
|
||||
if (backend === "libolm") {
|
||||
aliceClient.crypto!.backupManager.scheduleAllGroupSessionsForBackup =
|
||||
scheduleAllGroupSessionsForBackup;
|
||||
} else {
|
||||
// With Rust crypto, we don't need to call this function, so
|
||||
// we call the dummy value here so we pass our later
|
||||
// expectation.
|
||||
scheduleAllGroupSessionsForBackup();
|
||||
}
|
||||
|
||||
await aliceClient.getCrypto()!.resetKeyBackup();
|
||||
await awaitDeleteCalled;
|
||||
await newBackupStatusUpdate;
|
||||
@@ -2901,6 +3023,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
expect(nextVersion).toBeDefined();
|
||||
expect(nextVersion).not.toEqual(currentVersion);
|
||||
expect(nextKey).not.toEqual(currentBackupKey);
|
||||
expect(scheduleAllGroupSessionsForBackup).toHaveBeenCalled();
|
||||
|
||||
// The `deleteKeyBackupVersion` API is deprecated but has been modified to work with both crypto backend
|
||||
// ensure that it works anyhow
|
||||
@@ -2917,7 +3040,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
|
||||
keyResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
|
||||
const keyResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
|
||||
keyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
|
||||
keyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
|
||||
keyResponder.addKeyReceiver(BOB_TEST_USER_ID, keyReceiver);
|
||||
@@ -2953,4 +3076,183 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
expect(hasCrossSigningKeysForUser).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
/** Guards against downgrade attacks from servers hiding or manipulating the crypto settings. */
|
||||
describe("Persistent encryption settings", () => {
|
||||
let persistentStoreClient: MatrixClient;
|
||||
let client2: MatrixClient;
|
||||
|
||||
beforeEach(async () => {
|
||||
const homeserverurl = "https://alice-server.com";
|
||||
const userId = "@alice:localhost";
|
||||
|
||||
const keyResponder = new E2EKeyResponder(homeserverurl);
|
||||
keyResponder.addKeyReceiver(userId, keyReceiver);
|
||||
|
||||
// For legacy crypto, these tests only work properly with a proper (indexeddb-based) CryptoStore, so
|
||||
// rather than using the existing `aliceClient`, create a new client. Once we drop legacy crypto, we can
|
||||
// just use `aliceClient` here.
|
||||
persistentStoreClient = await makeNewClient(homeserverurl, userId, "persistentStoreClient");
|
||||
await persistentStoreClient.startClient({});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
persistentStoreClient.stopClient();
|
||||
client2?.stopClient();
|
||||
});
|
||||
|
||||
test("Sending a message in a room where the server is hiding the state event does not send a plaintext event", async () => {
|
||||
// Alice is in an encrypted room
|
||||
const encryptionState = mkEncryptionEvent({ algorithm: "m.megolm.v1.aes-sha2" });
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponseWithState([encryptionState]));
|
||||
await syncPromise(persistentStoreClient);
|
||||
|
||||
// Send a message, and expect to get an `m.room.encrypted` event.
|
||||
await Promise.all([persistentStoreClient.sendTextMessage(ROOM_ID, "test"), expectEncryptedSendMessage()]);
|
||||
|
||||
// We now replace the client, and allow the new one to resync, *without* the encryption event.
|
||||
client2 = await replaceClient(persistentStoreClient);
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponseWithState([]));
|
||||
await client2.startClient({});
|
||||
await syncPromise(client2);
|
||||
logger.log(client2.getUserId() + ": restarted");
|
||||
|
||||
await expectSendMessageToFail(client2);
|
||||
});
|
||||
|
||||
test("Changes to the rotation period should be ignored", async () => {
|
||||
// Alice is in an encrypted room, where the rotation period is set to 2 messages
|
||||
const encryptionState = mkEncryptionEvent({ algorithm: "m.megolm.v1.aes-sha2", rotation_period_msgs: 2 });
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponseWithState([encryptionState]));
|
||||
await syncPromise(persistentStoreClient);
|
||||
|
||||
// Send a message, and expect to get an `m.room.encrypted` event.
|
||||
const [, msg1Content] = await Promise.all([
|
||||
persistentStoreClient.sendTextMessage(ROOM_ID, "test1"),
|
||||
expectEncryptedSendMessage(),
|
||||
]);
|
||||
|
||||
// Replace the state with one which bumps the rotation period. This should be ignored, though it's not
|
||||
// clear that is correct behaviour (see https://github.com/element-hq/element-meta/issues/69)
|
||||
const encryptionState2 = mkEncryptionEvent({
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
rotation_period_msgs: 100,
|
||||
});
|
||||
syncResponder.sendOrQueueSyncResponse({
|
||||
next_batch: "1",
|
||||
rooms: { join: { [TEST_ROOM_ID]: { timeline: { events: [encryptionState2], prev_batch: "" } } } },
|
||||
});
|
||||
await syncPromise(persistentStoreClient);
|
||||
|
||||
// Send two more messages. The first should use the same megolm session as the first; the second should
|
||||
// use a different one.
|
||||
const [, msg2Content] = await Promise.all([
|
||||
persistentStoreClient.sendTextMessage(ROOM_ID, "test2"),
|
||||
expectEncryptedSendMessage(),
|
||||
]);
|
||||
expect(msg2Content.session_id).toEqual(msg1Content.session_id);
|
||||
const [, msg3Content] = await Promise.all([
|
||||
persistentStoreClient.sendTextMessage(ROOM_ID, "test3"),
|
||||
expectEncryptedSendMessage(),
|
||||
]);
|
||||
expect(msg3Content.session_id).not.toEqual(msg1Content.session_id);
|
||||
});
|
||||
|
||||
test("Changes to the rotation period should be ignored after a client restart", async () => {
|
||||
// Alice is in an encrypted room, where the rotation period is set to 2 messages
|
||||
const encryptionState = mkEncryptionEvent({ algorithm: "m.megolm.v1.aes-sha2", rotation_period_msgs: 2 });
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponseWithState([encryptionState]));
|
||||
await syncPromise(persistentStoreClient);
|
||||
|
||||
// Send a message, and expect to get an `m.room.encrypted` event.
|
||||
await Promise.all([persistentStoreClient.sendTextMessage(ROOM_ID, "test1"), expectEncryptedSendMessage()]);
|
||||
|
||||
// We now replace the client, and allow the new one to resync with a *different* encryption event.
|
||||
client2 = await replaceClient(persistentStoreClient);
|
||||
const encryptionState2 = mkEncryptionEvent({
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
rotation_period_msgs: 100,
|
||||
});
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponseWithState([encryptionState2]));
|
||||
await client2.startClient({});
|
||||
await syncPromise(client2);
|
||||
logger.log(client2.getUserId() + ": restarted");
|
||||
|
||||
// Now send another message, which should (for now) be rejected.
|
||||
await expectSendMessageToFail(client2);
|
||||
});
|
||||
|
||||
/** Shut down `oldClient`, and build a new MatrixClient for the same user. */
|
||||
async function replaceClient(oldClient: MatrixClient) {
|
||||
oldClient.stopClient();
|
||||
syncResponder.sendOrQueueSyncResponse({}); // flush pending request from old client
|
||||
return makeNewClient(oldClient.getHomeserverUrl(), oldClient.getSafeUserId(), "client2");
|
||||
}
|
||||
|
||||
async function makeNewClient(
|
||||
homeserverUrl: string,
|
||||
userId: string,
|
||||
loggerPrefix: string,
|
||||
): Promise<MatrixClient> {
|
||||
const client = createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
userId: userId,
|
||||
accessToken: "akjgkrgjs",
|
||||
deviceId: "xzcvb",
|
||||
cryptoCallbacks: createCryptoCallbacks(),
|
||||
logger: logger.getChild(loggerPrefix),
|
||||
|
||||
// For legacy crypto, these tests only work with a proper persistent cryptoStore.
|
||||
cryptoStore: new IndexedDBCryptoStore(indexedDB, "test"),
|
||||
});
|
||||
await initCrypto(client);
|
||||
mockInitialApiRequests(client.getHomeserverUrl());
|
||||
return client;
|
||||
}
|
||||
|
||||
function mkEncryptionEvent(content: Object) {
|
||||
return mkEventCustom({
|
||||
sender: persistentStoreClient.getSafeUserId(),
|
||||
type: "m.room.encryption",
|
||||
state_key: "",
|
||||
content: content,
|
||||
});
|
||||
}
|
||||
|
||||
/** Sync response which includes `TEST_ROOM_ID`, where alice is a member
|
||||
*
|
||||
* @param stateEvents - Additional state events for the test room
|
||||
*/
|
||||
function getSyncResponseWithState(stateEvents: Array<Object>) {
|
||||
const roomResponse = {
|
||||
state: {
|
||||
events: [
|
||||
mkMembershipCustom({
|
||||
membership: KnownMembership.Join,
|
||||
sender: persistentStoreClient.getSafeUserId(),
|
||||
}),
|
||||
...stateEvents,
|
||||
],
|
||||
},
|
||||
timeline: {
|
||||
events: [],
|
||||
prev_batch: "",
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
next_batch: "1",
|
||||
rooms: { join: { [TEST_ROOM_ID]: roomResponse } },
|
||||
};
|
||||
}
|
||||
|
||||
/** Send a message with the given client, and check that it is not sent in plaintext */
|
||||
async function expectSendMessageToFail(aliceClient2: MatrixClient) {
|
||||
// The precise failure mode here is somewhat up for debate (https://github.com/element-hq/element-meta/issues/69).
|
||||
// For now, the attempt to send is rejected with an exception. The text is different between old and new stacks.
|
||||
await expect(aliceClient2.sendTextMessage(ROOM_ID, "test")).rejects.toThrow(
|
||||
/unconfigured room !room:id|Room !room:id was previously configured to use encryption/,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
Copyright 2024 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 fetchMock from "fetch-mock-jest";
|
||||
|
||||
import { createClient, ClientEvent, MatrixClient, MatrixEvent } from "../../../src";
|
||||
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
|
||||
import { AddSecretStorageKeyOpts } from "../../../src/secret-storage";
|
||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
|
||||
describe("Device dehydration", () => {
|
||||
it("should rehydrate and dehydrate a device", async () => {
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
cryptoCallbacks: {
|
||||
getSecretStorageKey: async (keys: any, name: string) => {
|
||||
return [[...Object.keys(keys.keys)][0], new Uint8Array(32)];
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await initializeSecretStorage(matrixClient, "@alice:localhost", "http://test.server");
|
||||
|
||||
// count the number of times the dehydration key gets set
|
||||
let setDehydrationCount = 0;
|
||||
matrixClient.on(ClientEvent.AccountData, (event: MatrixEvent) => {
|
||||
if (event.getType() === "org.matrix.msc3814") {
|
||||
setDehydrationCount++;
|
||||
}
|
||||
});
|
||||
|
||||
const crypto = matrixClient.getCrypto()!;
|
||||
fetchMock.config.overwriteRoutes = true;
|
||||
|
||||
// start dehydration -- we start with no dehydrated device, and we
|
||||
// store the dehydrated device that we create
|
||||
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "Not found",
|
||||
},
|
||||
});
|
||||
let dehydratedDeviceBody: any;
|
||||
let dehydrationCount = 0;
|
||||
let resolveDehydrationPromise: () => void;
|
||||
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", (_, opts) => {
|
||||
dehydratedDeviceBody = JSON.parse(opts.body as string);
|
||||
dehydrationCount++;
|
||||
if (resolveDehydrationPromise) {
|
||||
resolveDehydrationPromise();
|
||||
}
|
||||
return {};
|
||||
});
|
||||
await crypto.startDehydration();
|
||||
|
||||
expect(dehydrationCount).toEqual(1);
|
||||
|
||||
// a week later, we should have created another dehydrated device
|
||||
const dehydrationPromise = new Promise<void>((resolve, reject) => {
|
||||
resolveDehydrationPromise = resolve;
|
||||
});
|
||||
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
|
||||
await dehydrationPromise;
|
||||
expect(dehydrationCount).toEqual(2);
|
||||
|
||||
// restart dehydration -- rehydrate the device that we created above,
|
||||
// and create a new dehydrated device. We also set `createNewKey`, so
|
||||
// a new dehydration key will be set
|
||||
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
|
||||
device_id: dehydratedDeviceBody.device_id,
|
||||
device_data: dehydratedDeviceBody.device_data,
|
||||
});
|
||||
const eventsResponse = jest.fn((url, opts) => {
|
||||
// rehydrating should make two calls to the /events endpoint.
|
||||
// The first time will return a single event, and the second
|
||||
// time will return no events (which will signal to the
|
||||
// rehydration function that it can stop)
|
||||
const body = JSON.parse(opts.body as string);
|
||||
const nextBatch = body.next_batch ?? "0";
|
||||
const events = nextBatch === "0" ? [{ sender: "@alice:localhost", type: "m.dummy", content: {} }] : [];
|
||||
return {
|
||||
events,
|
||||
next_batch: nextBatch + "1",
|
||||
};
|
||||
});
|
||||
fetchMock.post(
|
||||
`path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device/${encodeURIComponent(dehydratedDeviceBody.device_id)}/events`,
|
||||
eventsResponse,
|
||||
);
|
||||
await crypto.startDehydration(true);
|
||||
expect(dehydrationCount).toEqual(3);
|
||||
|
||||
expect(setDehydrationCount).toEqual(2);
|
||||
expect(eventsResponse.mock.calls).toHaveLength(2);
|
||||
|
||||
matrixClient.stopClient();
|
||||
});
|
||||
});
|
||||
|
||||
/** create a new secret storage and cross-signing keys */
|
||||
async function initializeSecretStorage(
|
||||
matrixClient: MatrixClient,
|
||||
userId: string,
|
||||
homeserverUrl: string,
|
||||
): Promise<void> {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "Not found",
|
||||
},
|
||||
});
|
||||
const e2eKeyReceiver = new E2EKeyReceiver(homeserverUrl);
|
||||
const e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
e2eKeyResponder.addKeyReceiver(userId, e2eKeyReceiver);
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/device_signing/upload", {});
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/signatures/upload", {});
|
||||
const accountData: Map<string, object> = new Map();
|
||||
fetchMock.get("glob:http://*/_matrix/client/v3/user/*/account_data/*", (url, opts) => {
|
||||
const name = url.split("/").pop()!;
|
||||
const value = accountData.get(name);
|
||||
if (value) {
|
||||
return value;
|
||||
} else {
|
||||
return {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "Not found",
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
fetchMock.put("glob:http://*/_matrix/client/v3/user/*/account_data/*", (url, opts) => {
|
||||
const name = url.split("/").pop()!;
|
||||
const value = JSON.parse(opts.body as string);
|
||||
accountData.set(name, value);
|
||||
matrixClient.emit(ClientEvent.AccountData, new MatrixEvent({ type: name, content: value }));
|
||||
return {};
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
const crypto = matrixClient.getCrypto()! as RustCrypto;
|
||||
// we need to process a sync so that the OlmMachine will upload keys
|
||||
await crypto.preprocessToDeviceMessages([]);
|
||||
await crypto.onSyncCompleted({});
|
||||
|
||||
// create initial secret storage
|
||||
async function createSecretStorageKey() {
|
||||
return {
|
||||
keyInfo: {} as AddSecretStorageKeyOpts,
|
||||
privateKey: new Uint8Array(32),
|
||||
};
|
||||
}
|
||||
await matrixClient.bootstrapCrossSigning({ setupNewCrossSigning: true });
|
||||
await matrixClient.bootstrapSecretStorage({
|
||||
createSecretStorageKey,
|
||||
setupNewSecretStorage: true,
|
||||
setupNewKeyBackup: false,
|
||||
});
|
||||
}
|
||||
@@ -17,8 +17,18 @@ limitations under the License.
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import { Mocked } from "jest-mock";
|
||||
|
||||
import { createClient, CryptoEvent, ICreateClientOpts, MatrixClient, TypedEventEmitter } from "../../../src";
|
||||
import {
|
||||
createClient,
|
||||
CryptoApi,
|
||||
CryptoEvent,
|
||||
ICreateClientOpts,
|
||||
IEvent,
|
||||
IMegolmSessionData,
|
||||
MatrixClient,
|
||||
TypedEventEmitter,
|
||||
} from "../../../src";
|
||||
import { SyncResponder } from "../../test-utils/SyncResponder";
|
||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
@@ -31,9 +41,11 @@ import {
|
||||
syncPromise,
|
||||
} from "../../test-utils/test-utils";
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
import { KeyBackupInfo } from "../../../src/crypto-api/keybackup";
|
||||
import { KeyBackupInfo, KeyBackupSession } from "../../../src/crypto-api/keybackup";
|
||||
import { IKeyBackup } from "../../../src/crypto/backup";
|
||||
import { flushPromises } from "../../test-utils/flushPromises";
|
||||
import { defer, IDeferred } from "../../../src/utils";
|
||||
import { DecryptionFailureCode } from "../../../src/crypto-api";
|
||||
|
||||
const ROOM_ID = testData.TEST_ROOM_ID;
|
||||
|
||||
@@ -118,7 +130,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
let e2eKeyResponder: E2EKeyResponder;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.useFakeTimers();
|
||||
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
@@ -180,28 +193,31 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
beforeEach(
|
||||
async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
// ignore requests to send room key requests
|
||||
fetchMock.put("express:/_matrix/client/v3/sendToDevice/m.room_key_request/:request_id", {});
|
||||
// ignore requests to send room key requests
|
||||
fetchMock.put("express:/_matrix/client/v3/sendToDevice/m.room_key_request/:request_id", {});
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
|
||||
testData.SIGNED_BACKUP_DATA.version!,
|
||||
);
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
|
||||
testData.SIGNED_BACKUP_DATA.version!,
|
||||
);
|
||||
|
||||
// start after saving the private key
|
||||
await aliceClient.startClient();
|
||||
// start after saving the private key
|
||||
await aliceClient.startClient();
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup, and re-check the backup.
|
||||
// XXX: should we automatically re-check after a device becomes verified?
|
||||
await waitForDeviceList();
|
||||
await aliceClient.getCrypto()!.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
|
||||
});
|
||||
// tell Alice to trust the dummy device that signed the backup, and re-check the backup.
|
||||
// XXX: should we automatically re-check after a device becomes verified?
|
||||
await waitForDeviceList();
|
||||
await aliceClient.getCrypto()!.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
|
||||
} /* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */,
|
||||
10000,
|
||||
);
|
||||
|
||||
it("Alice checks key backups when receiving a message she can't decrypt", async () => {
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id", (url, request) => {
|
||||
@@ -227,8 +243,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
|
||||
const room = aliceClient.getRoom(ROOM_ID)!;
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
await advanceTimersUntil(awaitDecryption(event, { waitOnDecryptionFailure: true }));
|
||||
|
||||
// On the first decryption attempt, decryption fails.
|
||||
await awaitDecryption(event);
|
||||
expect(event.decryptionFailureReason).toEqual(
|
||||
backend === "libolm"
|
||||
? DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID
|
||||
: DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP,
|
||||
);
|
||||
|
||||
// Eventually, decryption succeeds.
|
||||
await awaitDecryption(event, { waitOnDecryptionFailure: true });
|
||||
expect(event.getContent()).toEqual(testData.CLEAR_EVENT.content);
|
||||
});
|
||||
|
||||
@@ -285,17 +310,21 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
});
|
||||
|
||||
describe("recover from backup", () => {
|
||||
it("can restore from backup (Curve25519 version)", async function () {
|
||||
let aliceCrypto: CryptoApi;
|
||||
|
||||
beforeEach(async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
aliceCrypto = aliceClient.getCrypto()!;
|
||||
await aliceClient.startClient();
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
});
|
||||
|
||||
it("can restore from backup (Curve25519 version)", async function () {
|
||||
const fullBackup = {
|
||||
rooms: {
|
||||
[ROOM_ID]: {
|
||||
@@ -339,17 +368,179 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
expect(afterCache.imported).toStrictEqual(1);
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a mock backup response of a GET `room_keys/keys` with a given number of keys per room.
|
||||
* @param keysPerRoom The number of keys per room
|
||||
*/
|
||||
function createBackupDownloadResponse(keysPerRoom: number[]) {
|
||||
const response: {
|
||||
rooms: {
|
||||
[roomId: string]: {
|
||||
sessions: {
|
||||
[sessionId: string]: KeyBackupSession;
|
||||
};
|
||||
};
|
||||
};
|
||||
} = { rooms: {} };
|
||||
|
||||
const expectedTotal = keysPerRoom.reduce((a, b) => a + b, 0);
|
||||
for (let i = 0; i < keysPerRoom.length; i++) {
|
||||
const roomId = `!room${i}:example.com`;
|
||||
response.rooms[roomId] = { sessions: {} };
|
||||
for (let j = 0; j < keysPerRoom[i]; j++) {
|
||||
const sessionId = `session${j}`;
|
||||
// Put the same fake session data, not important for that test
|
||||
response.rooms[roomId].sessions[sessionId] = testData.CURVE25519_KEY_BACKUP_DATA;
|
||||
}
|
||||
}
|
||||
return { response, expectedTotal };
|
||||
}
|
||||
|
||||
it("Should import full backup in chunks", async function () {
|
||||
const importMockImpl = jest.fn();
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
aliceCrypto.importBackedUpRoomKeys = importMockImpl;
|
||||
|
||||
// We need several rooms with several sessions to test chunking
|
||||
const { response, expectedTotal } = createBackupDownloadResponse([45, 300, 345, 12, 130]);
|
||||
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", response);
|
||||
|
||||
const check = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
|
||||
const progressCallback = jest.fn();
|
||||
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
||||
undefined,
|
||||
undefined,
|
||||
check!.backupInfo!,
|
||||
{
|
||||
progressCallback,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.imported).toStrictEqual(expectedTotal);
|
||||
// Should be called 5 times: 200*4 plus one chunk with the remaining 32
|
||||
expect(importMockImpl).toHaveBeenCalledTimes(5);
|
||||
for (let i = 0; i < 4; i++) {
|
||||
expect(importMockImpl.mock.calls[i][0].length).toEqual(200);
|
||||
}
|
||||
expect(importMockImpl.mock.calls[4][0].length).toEqual(32);
|
||||
|
||||
expect(progressCallback).toHaveBeenCalledWith({
|
||||
stage: "fetch",
|
||||
});
|
||||
|
||||
// Should be called 4 times and report 200/400/600/800
|
||||
for (let i = 0; i < 4; i++) {
|
||||
expect(progressCallback).toHaveBeenCalledWith({
|
||||
total: expectedTotal,
|
||||
successes: (i + 1) * 200,
|
||||
stage: "load_keys",
|
||||
failures: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// The last chunk
|
||||
expect(progressCallback).toHaveBeenCalledWith({
|
||||
total: expectedTotal,
|
||||
successes: 832,
|
||||
stage: "load_keys",
|
||||
failures: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("Should continue to process backup if a chunk import fails and report failures", async function () {
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
aliceCrypto.importBackedUpRoomKeys = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
// Fail to import first chunk
|
||||
throw new Error("test error");
|
||||
})
|
||||
// Ok for other chunks
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
const { response, expectedTotal } = createBackupDownloadResponse([100, 300]);
|
||||
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", response);
|
||||
|
||||
const check = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
|
||||
const progressCallback = jest.fn();
|
||||
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
||||
undefined,
|
||||
undefined,
|
||||
check!.backupInfo!,
|
||||
{
|
||||
progressCallback,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.total).toStrictEqual(expectedTotal);
|
||||
// A chunk failed to import
|
||||
expect(result.imported).toStrictEqual(200);
|
||||
|
||||
expect(progressCallback).toHaveBeenCalledWith({
|
||||
total: expectedTotal,
|
||||
successes: 0,
|
||||
stage: "load_keys",
|
||||
failures: 200,
|
||||
});
|
||||
|
||||
expect(progressCallback).toHaveBeenCalledWith({
|
||||
total: expectedTotal,
|
||||
successes: 200,
|
||||
stage: "load_keys",
|
||||
failures: 200,
|
||||
});
|
||||
});
|
||||
|
||||
it("Should continue if some keys fails to decrypt", async function () {
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
aliceCrypto.importBackedUpRoomKeys = jest.fn();
|
||||
|
||||
const decryptionFailureCount = 2;
|
||||
|
||||
const mockDecryptor = {
|
||||
// DecryptSessions does not reject on decryption failure, but just skip the key
|
||||
decryptSessions: jest.fn().mockImplementation((sessions) => {
|
||||
// simulate fail to decrypt 2 keys out of all
|
||||
const decrypted = [];
|
||||
const keys = Object.keys(sessions);
|
||||
for (let i = 0; i < keys.length - decryptionFailureCount; i++) {
|
||||
decrypted.push({
|
||||
session_id: keys[i],
|
||||
} as unknown as Mocked<IMegolmSessionData>);
|
||||
}
|
||||
return decrypted;
|
||||
}),
|
||||
free: jest.fn(),
|
||||
};
|
||||
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
aliceCrypto.getBackupDecryptor = jest.fn().mockResolvedValue(mockDecryptor);
|
||||
|
||||
const { response, expectedTotal } = createBackupDownloadResponse([100]);
|
||||
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", response);
|
||||
|
||||
const check = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
|
||||
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
||||
undefined,
|
||||
undefined,
|
||||
check!.backupInfo!,
|
||||
);
|
||||
|
||||
expect(result.total).toStrictEqual(expectedTotal);
|
||||
// A chunk failed to import
|
||||
expect(result.imported).toStrictEqual(expectedTotal - decryptionFailureCount);
|
||||
});
|
||||
|
||||
it("recover specific session from backup", async function () {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
await aliceClient.startClient();
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
fetchMock.get(
|
||||
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
|
||||
testData.CURVE25519_KEY_BACKUP_DATA,
|
||||
@@ -370,16 +561,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
});
|
||||
|
||||
it("Fails on bad recovery key", async function () {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
await aliceClient.startClient();
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
const fullBackup = {
|
||||
rooms: {
|
||||
[ROOM_ID]: {
|
||||
@@ -888,6 +1069,146 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
});
|
||||
});
|
||||
|
||||
describe("Backup Changed from other sessions", () => {
|
||||
beforeEach(async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
// ignore requests to send room key requests
|
||||
fetchMock.put("express:/_matrix/client/v3/sendToDevice/m.room_key_request/:request_id", {});
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
|
||||
testData.SIGNED_BACKUP_DATA.version!,
|
||||
);
|
||||
|
||||
// start after saving the private key
|
||||
await aliceClient.startClient();
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup, and re-check the backup.
|
||||
// XXX: should we automatically re-check after a device becomes verified?
|
||||
await waitForDeviceList();
|
||||
await aliceClient.getCrypto()!.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
|
||||
});
|
||||
|
||||
// let aliceClient: MatrixClient;
|
||||
|
||||
const SYNC_RESPONSE = {
|
||||
next_batch: 1,
|
||||
rooms: { join: { [ROOM_ID]: { timeline: { events: [testData.ENCRYPTED_EVENT] } } } },
|
||||
};
|
||||
|
||||
it("If current backup has changed, the manager should switch to the new one on UTD", async () => {
|
||||
// =====
|
||||
// First ensure that the client checks for keys using the backup version 1
|
||||
/// =====
|
||||
|
||||
fetchMock.get(
|
||||
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
|
||||
(url, request) => {
|
||||
// check that the version is correct
|
||||
const version = new URLSearchParams(new URL(url).search).get("version");
|
||||
if (version == "1") {
|
||||
return testData.CURVE25519_KEY_BACKUP_DATA;
|
||||
} else {
|
||||
return {
|
||||
status: 403,
|
||||
body: {
|
||||
current_version: "1",
|
||||
errcode: "M_WRONG_ROOM_KEYS_VERSION",
|
||||
error: "Wrong backup version.",
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
|
||||
// Send Alice a message that she won't be able to decrypt, and check that she fetches the key from the backup.
|
||||
syncResponder.sendOrQueueSyncResponse(SYNC_RESPONSE);
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const room = aliceClient.getRoom(ROOM_ID)!;
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
await advanceTimersUntil(awaitDecryption(event, { waitOnDecryptionFailure: true }));
|
||||
|
||||
expect(event.getContent()).toEqual(testData.CLEAR_EVENT.content);
|
||||
|
||||
// =====
|
||||
// Second suppose now that the backup has changed to version 2
|
||||
/// =====
|
||||
|
||||
const newBackup = {
|
||||
...testData.SIGNED_BACKUP_DATA,
|
||||
version: "2",
|
||||
};
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, { overwriteRoutes: true });
|
||||
// suppose the new key is now known
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
|
||||
newBackup.version,
|
||||
);
|
||||
|
||||
// A check backup should happen at some point
|
||||
await aliceCrypto.checkKeyBackupAndEnable();
|
||||
|
||||
const awaitHasQueriedNewBackup: IDeferred<void> = defer<void>();
|
||||
|
||||
fetchMock.get(
|
||||
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
|
||||
(url, request) => {
|
||||
// check that the version is correct
|
||||
const version = new URLSearchParams(new URL(url).search).get("version");
|
||||
if (version == newBackup.version) {
|
||||
awaitHasQueriedNewBackup.resolve();
|
||||
return testData.CURVE25519_KEY_BACKUP_DATA;
|
||||
} else {
|
||||
// awaitHasQueriedOldBackup.resolve();
|
||||
return {
|
||||
status: 403,
|
||||
body: {
|
||||
current_version: "2",
|
||||
errcode: "M_WRONG_ROOM_KEYS_VERSION",
|
||||
error: "Wrong backup version.",
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
|
||||
// Send Alice a message that she won't be able to decrypt, and check that she fetches the key from the new backup.
|
||||
const newMessage: Partial<IEvent> = {
|
||||
type: "m.room.encrypted",
|
||||
room_id: "!room:id",
|
||||
sender: "@alice:localhost",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
ciphertext:
|
||||
"AwgAEpABKvf9FqPW52zeHfeVTn90a3jlBLlx7g6VDEkc2089RQUJoWpSJRiK13E83rN41wgGFJccyfoCr7ZDGJeuGYMGETTrgnLQhLs6JmyPf37JYkzxW8uS8rGUKEqTFQriKhibHVLvVacOlSIObUiKU/V3r176XuixqZF/4eyK9A22JNpInbgI10ZUT6LnApH9LR3FpZbE2zImf1uNPuvp7r0xQbW7CcJjqpH+qTPBD5zFdFnMkc2SnbXCsIOaX11Dm0krWfQz7iA26ZnI1nyZnyh7XPrCnJCRsuQH",
|
||||
device_id: "WVMJGTSSVB",
|
||||
sender_key: "E5RiY/YCIrHWaF4u416CqvblC6udK2jt9SJ/h1QeLS0",
|
||||
session_id: "ybnW+LGdUhoS4fHm1DAEphukO3sZ1GCqZD7UQz7L+GA",
|
||||
},
|
||||
event_id: "$event2",
|
||||
origin_server_ts: 1507753887000,
|
||||
};
|
||||
|
||||
const nextSyncResponse = {
|
||||
next_batch: 2,
|
||||
rooms: { join: { [ROOM_ID]: { timeline: { events: [newMessage] } } } },
|
||||
};
|
||||
syncResponder.sendOrQueueSyncResponse(nextSyncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
await awaitHasQueriedNewBackup.promise;
|
||||
});
|
||||
});
|
||||
|
||||
/** make sure that the client knows about the dummy device */
|
||||
async function waitForDeviceList(): Promise<void> {
|
||||
// Completing the initial sync will make the device list download outdated device lists (of which our own
|
||||
|
||||
@@ -34,8 +34,9 @@ import { logger } from "../../../src/logger";
|
||||
import * as testUtils from "../../test-utils/test-utils";
|
||||
import { TestClient } from "../../TestClient";
|
||||
import { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, IUploadKeysRequest } from "../../../src/client";
|
||||
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../../src/matrix";
|
||||
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent, MsgType } from "../../../src/matrix";
|
||||
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
|
||||
import { KnownMembership } from "../../../src/@types/membership";
|
||||
|
||||
let aliTestClient: TestClient;
|
||||
const roomId = "!room:localhost";
|
||||
@@ -216,7 +217,7 @@ async function expectBobSendMessageRequest(): Promise<OlmPayload> {
|
||||
}
|
||||
|
||||
function sendMessage(client: MatrixClient): Promise<ISendEventResponse> {
|
||||
return client.sendMessage(roomId, { msgtype: "m.text", body: "Hello, World" });
|
||||
return client.sendMessage(roomId, { msgtype: MsgType.Text, body: "Hello, World" });
|
||||
}
|
||||
|
||||
async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]): Promise<IContent> {
|
||||
@@ -316,11 +317,11 @@ function firstSync(testClient: TestClient): Promise<void> {
|
||||
state: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: aliUserId,
|
||||
}),
|
||||
testUtils.mkMembership({
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: bobUserId,
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -16,8 +16,15 @@ limitations under the License.
|
||||
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import { createClient } from "../../../src";
|
||||
import { createClient, CryptoEvent, IndexedDBCryptoStore } from "../../../src";
|
||||
import { populateStore } from "../../test-utils/test_indexeddb_cryptostore_dump";
|
||||
import { MSK_NOT_CACHED_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/no_cached_msk_dump";
|
||||
import { IDENTITY_NOT_TRUSTED_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/unverified";
|
||||
import { FULL_ACCOUNT_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/full_account";
|
||||
|
||||
jest.setTimeout(15000);
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
@@ -88,6 +95,302 @@ describe("MatrixClient.initRustCrypto", () => {
|
||||
await matrixClient.initRustCrypto();
|
||||
await matrixClient.initRustCrypto();
|
||||
});
|
||||
|
||||
describe("Libolm Migration", () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.reset();
|
||||
});
|
||||
|
||||
it("should migrate from libolm", async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", FULL_ACCOUNT_DATASET.backupResponse);
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", FULL_ACCOUNT_DATASET.keyQueryResponse);
|
||||
|
||||
const testStoreName = "test-store";
|
||||
await populateStore(testStoreName, FULL_ACCOUNT_DATASET.dumpPath);
|
||||
const cryptoStore = new IndexedDBCryptoStore(indexedDB, testStoreName);
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: FULL_ACCOUNT_DATASET.userId,
|
||||
deviceId: FULL_ACCOUNT_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: FULL_ACCOUNT_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
const progressListener = jest.fn();
|
||||
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
const verificationStatus = await matrixClient
|
||||
.getCrypto()!
|
||||
.getDeviceVerificationStatus(FULL_ACCOUNT_DATASET.userId, FULL_ACCOUNT_DATASET.deviceId);
|
||||
|
||||
// Check that the current device and identity trust is migrated correctly just after migration
|
||||
expect(verificationStatus).toBeDefined();
|
||||
expect(verificationStatus!.crossSigningVerified).toEqual(true);
|
||||
expect(verificationStatus!.signedByOwner).toEqual(true);
|
||||
|
||||
// Do some basic checks on the imported data
|
||||
const deviceKeys = await matrixClient.getCrypto()!.getOwnDeviceKeys();
|
||||
expect(deviceKeys.curve25519).toEqual("LKv0bKbc0EC4h0jknbemv3QalEkeYvuNeUXVRgVVTTU");
|
||||
expect(deviceKeys.ed25519).toEqual("qK70DEqIXq7T+UU3v/al47Ab4JkMEBLpNrTBMbS5rrw");
|
||||
|
||||
expect(await matrixClient.getCrypto()!.getActiveSessionBackupVersion()).toEqual("7");
|
||||
|
||||
expect(await matrixClient.getCrypto()!.isEncryptionEnabledInRoom("!CWLUCoEWXSFyTCOtfL:matrix.org")).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
// check the progress callback
|
||||
expect(progressListener.mock.calls.length).toBeGreaterThan(50);
|
||||
|
||||
// The first call should have progress == 0
|
||||
const [firstProgress, totalSteps] = progressListener.mock.calls[0];
|
||||
expect(totalSteps).toBeGreaterThan(3000);
|
||||
expect(firstProgress).toEqual(0);
|
||||
|
||||
for (let i = 1; i < progressListener.mock.calls.length - 1; i++) {
|
||||
const [progress, total] = progressListener.mock.calls[i];
|
||||
expect(total).toEqual(totalSteps);
|
||||
expect(progress).toBeGreaterThan(progressListener.mock.calls[i - 1][0]);
|
||||
expect(progress).toBeLessThanOrEqual(totalSteps);
|
||||
}
|
||||
|
||||
// The final call should have progress == total == -1
|
||||
expect(progressListener).toHaveBeenLastCalledWith(-1, -1);
|
||||
}, 60000);
|
||||
|
||||
describe("Private key backup migration", () => {
|
||||
it("should not migrate the backup private key if backup has changed", async () => {
|
||||
// Here we have a new backup server side, and the migrated account has the previous backup key.
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", MSK_NOT_CACHED_DATASET.newBackupResponse);
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
|
||||
|
||||
await populateStore("test-store", MSK_NOT_CACHED_DATASET.dumpPath);
|
||||
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "test-store");
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: MSK_NOT_CACHED_DATASET.userId,
|
||||
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
const privateBackupKey = await matrixClient.getCrypto()?.getSessionBackupPrivateKey();
|
||||
expect(privateBackupKey).toBeNull();
|
||||
});
|
||||
|
||||
it("should not migrate the backup private key if backup has unknown algorithm", async () => {
|
||||
// Here we have a new backup server side, and the migrated account has the previous backup key.
|
||||
const backupResponse = {
|
||||
...MSK_NOT_CACHED_DATASET.backupResponse,
|
||||
algorithm: "m.megolm_backup.v8",
|
||||
};
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupResponse);
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
|
||||
|
||||
await populateStore("test-store", MSK_NOT_CACHED_DATASET.dumpPath);
|
||||
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "test-store");
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: MSK_NOT_CACHED_DATASET.userId,
|
||||
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
const privateBackupKey = await matrixClient.getCrypto()?.getSessionBackupPrivateKey();
|
||||
expect(privateBackupKey).toBeNull();
|
||||
});
|
||||
|
||||
it("should not migrate the backup private key if the backup has been deleted", async () => {
|
||||
// The old backup has been deleted server side.
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "No backup found",
|
||||
},
|
||||
});
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
|
||||
|
||||
await populateStore("test-store", MSK_NOT_CACHED_DATASET.dumpPath);
|
||||
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "test-store");
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: MSK_NOT_CACHED_DATASET.userId,
|
||||
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
const privateBackupKey = await matrixClient.getCrypto()?.getSessionBackupPrivateKey();
|
||||
expect(privateBackupKey).toBeNull();
|
||||
});
|
||||
|
||||
it("should migrate the backup private key if the backup matches", async () => {
|
||||
// The old backup has been deleted server side.
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", MSK_NOT_CACHED_DATASET.backupResponse);
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
|
||||
|
||||
await populateStore("test-store", MSK_NOT_CACHED_DATASET.dumpPath);
|
||||
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "test-store");
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: MSK_NOT_CACHED_DATASET.userId,
|
||||
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
const privateBackupKey = await matrixClient.getCrypto()?.getSessionBackupPrivateKey();
|
||||
expect(privateBackupKey).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Legacy trust migration", () => {
|
||||
async function populateAndStartLegacyCryptoStore(dumpPath: string): Promise<IndexedDBCryptoStore> {
|
||||
const testStoreName = "test-store";
|
||||
await populateStore(testStoreName, dumpPath);
|
||||
const cryptoStore = new IndexedDBCryptoStore(indexedDB, testStoreName);
|
||||
await cryptoStore.startup();
|
||||
return cryptoStore;
|
||||
}
|
||||
|
||||
it("should not revert to untrusted if legacy was trusted but msk not in cache, big account", async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "No backup found",
|
||||
},
|
||||
});
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", FULL_ACCOUNT_DATASET.keyQueryResponse);
|
||||
|
||||
const cryptoStore = await populateAndStartLegacyCryptoStore(FULL_ACCOUNT_DATASET.dumpPath);
|
||||
|
||||
// Remove the master key from the cache
|
||||
await cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
|
||||
const objectStore = txn.objectStore("account");
|
||||
objectStore.delete(`ssss_cache:master`);
|
||||
});
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: FULL_ACCOUNT_DATASET.userId,
|
||||
deviceId: FULL_ACCOUNT_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: FULL_ACCOUNT_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
const verificationStatus = await matrixClient
|
||||
.getCrypto()!
|
||||
.getUserVerificationStatus(FULL_ACCOUNT_DATASET.userId);
|
||||
|
||||
expect(verificationStatus.isCrossSigningVerified()).toBe(true);
|
||||
}, 60000);
|
||||
|
||||
it("should not revert to untrusted if legacy was trusted but msk not in cache", async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", MSK_NOT_CACHED_DATASET.backupResponse);
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
|
||||
|
||||
const cryptoStore = await populateAndStartLegacyCryptoStore(MSK_NOT_CACHED_DATASET.dumpPath);
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: MSK_NOT_CACHED_DATASET.userId,
|
||||
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
const verificationStatus = await matrixClient
|
||||
.getCrypto()!
|
||||
.getUserVerificationStatus("@migration:localhost");
|
||||
|
||||
expect(verificationStatus.isCrossSigningVerified()).toBe(true);
|
||||
});
|
||||
|
||||
it("should not migrate local trust if key has changed", async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", MSK_NOT_CACHED_DATASET.backupResponse);
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.rotatedKeyQueryResponse);
|
||||
|
||||
const cryptoStore = await populateAndStartLegacyCryptoStore(MSK_NOT_CACHED_DATASET.dumpPath);
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: MSK_NOT_CACHED_DATASET.userId,
|
||||
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
const verificationStatus = await matrixClient
|
||||
.getCrypto()!
|
||||
.getUserVerificationStatus("@migration:localhost");
|
||||
|
||||
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
||||
});
|
||||
|
||||
it("should not migrate local trust if was not trusted in legacy", async () => {
|
||||
// Just 404 here for the test
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "No backup found",
|
||||
},
|
||||
});
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", IDENTITY_NOT_TRUSTED_DATASET.keyQueryResponse);
|
||||
|
||||
const cryptoStore = await populateAndStartLegacyCryptoStore(IDENTITY_NOT_TRUSTED_DATASET.dumpPath);
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: IDENTITY_NOT_TRUSTED_DATASET.userId,
|
||||
deviceId: IDENTITY_NOT_TRUSTED_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: IDENTITY_NOT_TRUSTED_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
const verificationStatus = await matrixClient
|
||||
.getCrypto()!
|
||||
.getUserVerificationStatus("@untrusted:localhost");
|
||||
|
||||
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("MatrixClient.clearStores", () => {
|
||||
|
||||
@@ -85,7 +85,8 @@ import { encodeBase64 } from "../../../src/base64";
|
||||
|
||||
// The verification flows use javascript timers to set timeouts. We tell jest to use mock timer implementations
|
||||
// to ensure that we don't end up with dangling timeouts.
|
||||
jest.useFakeTimers();
|
||||
// But the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
beforeAll(async () => {
|
||||
// we use the libolm primitives in the test, so init the Olm library
|
||||
@@ -743,6 +744,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||
expect(toDeviceMessage.code).toEqual("m.user");
|
||||
expect(request.phase).toEqual(VerificationPhase.Cancelled);
|
||||
expect(request.cancellationCode).toEqual("m.user");
|
||||
expect(request.cancellingUserId).toEqual("@alice:localhost");
|
||||
});
|
||||
|
||||
it("can cancel during the SAS phase", async () => {
|
||||
@@ -1259,14 +1262,11 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
|
||||
const requestId = await requestPromises.get("m.megolm_backup.v1");
|
||||
|
||||
const keyBackupIsCached = emitPromise(aliceClient, CryptoEvent.KeyBackupDecryptionKeyCached);
|
||||
|
||||
await sendBackupGossipAndExpectVersion(requestId!, BACKUP_DECRYPTION_KEY_BASE64, matchingBackupInfo);
|
||||
|
||||
// We are lacking a way to signal that the secret has been received, so we wait a bit..
|
||||
jest.useRealTimers();
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
jest.useFakeTimers();
|
||||
await keyBackupIsCached;
|
||||
|
||||
// the backup secret should be cached
|
||||
const cachedKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
|
||||
@@ -1288,7 +1288,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
jest.useFakeTimers();
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
// the backup secret should not be cached
|
||||
const cachedKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
|
||||
@@ -1312,7 +1312,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
jest.useFakeTimers();
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
// the backup secret should not be cached
|
||||
const cachedKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
|
||||
@@ -1337,7 +1337,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
jest.useFakeTimers();
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
// the backup secret should not be cached
|
||||
const cachedKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
|
||||
@@ -1358,7 +1358,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
jest.useFakeTimers();
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
// the backup secret should not be cached
|
||||
const cachedKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
|
||||
|
||||
@@ -19,6 +19,7 @@ limitations under the License.
|
||||
import { TestClient } from "../TestClient";
|
||||
import * as testUtils from "../test-utils/test-utils";
|
||||
import { logger } from "../../src/logger";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
|
||||
@@ -43,7 +44,7 @@ function getSyncResponse(roomMembers: string[]) {
|
||||
stateEvents,
|
||||
roomMembers.map((m) =>
|
||||
testUtils.mkMembership({
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
sender: m,
|
||||
}),
|
||||
),
|
||||
@@ -323,7 +324,7 @@ describe("DeviceList management:", function () {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: "leave",
|
||||
mship: KnownMembership.Leave,
|
||||
sender: "@bob:xyz",
|
||||
}),
|
||||
],
|
||||
@@ -357,7 +358,7 @@ describe("DeviceList management:", function () {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: "leave",
|
||||
mship: KnownMembership.Leave,
|
||||
sender: "@bob:xyz",
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} from "../../src";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
describe("MatrixClient events", function () {
|
||||
const selfUserId = "@alice:localhost";
|
||||
@@ -85,7 +86,7 @@ describe("MatrixClient events", function () {
|
||||
events: [
|
||||
utils.mkMembership({
|
||||
room: "!erufh:bar",
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: "@foo:bar",
|
||||
}),
|
||||
utils.mkEvent({
|
||||
@@ -272,7 +273,7 @@ describe("MatrixClient events", function () {
|
||||
membersInvokeCount++;
|
||||
expect(member.roomId).toEqual("!erufh:bar");
|
||||
expect(member.userId).toEqual("@foo:bar");
|
||||
expect(member.membership).toEqual("join");
|
||||
expect(member.membership).toEqual(KnownMembership.Join);
|
||||
});
|
||||
client!.on(RoomStateEvent.NewMember, function (event, state, member) {
|
||||
newMemberInvokeCount++;
|
||||
@@ -310,7 +311,7 @@ describe("MatrixClient events", function () {
|
||||
});
|
||||
client!.on(RoomMemberEvent.Membership, function (event, member) {
|
||||
membershipInvokeCount++;
|
||||
expect(member.membership).toEqual("join");
|
||||
expect(member.membership).toEqual(KnownMembership.Join);
|
||||
});
|
||||
|
||||
client!.startClient();
|
||||
|
||||
@@ -33,9 +33,10 @@ import {
|
||||
import { logger } from "../../src/logger";
|
||||
import { encodeParams, encodeUri, QueryDict, replaceParam } from "../../src/utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { FeatureSupport, Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../src/models/thread";
|
||||
import { FeatureSupport, Thread, ThreadEvent } from "../../src/models/thread";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
import { Feature, ServerSupport } from "../../src/feature";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
const userId = "@alice:localhost";
|
||||
const userName = "Alice";
|
||||
@@ -63,7 +64,7 @@ const buildRelationPaginationQuery = (params: QueryDict): string => {
|
||||
|
||||
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: userId,
|
||||
name: userName,
|
||||
event: false,
|
||||
@@ -98,7 +99,7 @@ const INITIAL_SYNC_DATA = {
|
||||
events: [
|
||||
withoutRoomId(ROOM_NAME_EVENT),
|
||||
utils.mkMembership({
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: otherUserId,
|
||||
name: "Bob",
|
||||
event: false,
|
||||
@@ -607,11 +608,6 @@ describe("MatrixClient event timelines", function () {
|
||||
await client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId)!;
|
||||
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
@@ -623,9 +619,7 @@ describe("MatrixClient event timelines", function () {
|
||||
"GET",
|
||||
"/rooms/!foo%3Abar/relations/" +
|
||||
encodeURIComponent(THREAD_ROOT.event_id!) +
|
||||
"/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) +
|
||||
buildRelationPaginationQuery({ dir: Direction.Backward, limit: 1 }),
|
||||
buildRelationPaginationQuery({ dir: Direction.Backward }),
|
||||
)
|
||||
.respond(200, function () {
|
||||
return {
|
||||
@@ -1154,10 +1148,7 @@ describe("MatrixClient event timelines", function () {
|
||||
httpBackend
|
||||
.when(
|
||||
"GET",
|
||||
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
|
||||
encodeURIComponent(THREAD_ROOT_UPDATED.event_id!) +
|
||||
"/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name),
|
||||
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" + encodeURIComponent(THREAD_ROOT_UPDATED.event_id!),
|
||||
)
|
||||
.respond(200, {
|
||||
chunk: [THREAD_REPLY3.event, THREAD_REPLY2.event, THREAD_REPLY],
|
||||
@@ -1262,11 +1253,8 @@ describe("MatrixClient event timelines", function () {
|
||||
"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,
|
||||
}),
|
||||
)
|
||||
@@ -1321,11 +1309,7 @@ describe("MatrixClient event timelines", function () {
|
||||
function respondToThread(root: Partial<IEvent>, replies: Partial<IEvent>[]): ExpectedHttpRequest {
|
||||
const request = httpBackend.when(
|
||||
"GET",
|
||||
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
|
||||
encodeURIComponent(root.event_id!) +
|
||||
"/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) +
|
||||
"?dir=b&limit=1",
|
||||
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" + encodeURIComponent(root.event_id!) + "?dir=b",
|
||||
);
|
||||
request.respond(200, function () {
|
||||
return {
|
||||
@@ -1557,9 +1541,7 @@ describe("MatrixClient event timelines", function () {
|
||||
expect(timelineSets).not.toBeNull();
|
||||
respondToThreads(threadsResponse);
|
||||
respondToThreads(threadsResponse);
|
||||
respondToEvent(THREAD_ROOT);
|
||||
respondToEvent(THREAD2_ROOT);
|
||||
respondToThread(THREAD_ROOT, [THREAD_REPLY]);
|
||||
respondToThread(THREAD2_ROOT, [THREAD2_REPLY]);
|
||||
await flushHttp(room.fetchRoomThreads());
|
||||
const threadIds = room.getThreads().map((thread) => thread.id);
|
||||
@@ -1567,7 +1549,7 @@ describe("MatrixClient event timelines", function () {
|
||||
expect(threadIds).toContain(THREAD2_ROOT.event_id);
|
||||
const [allThreads] = timelineSets!;
|
||||
const timeline = allThreads.getLiveTimeline()!;
|
||||
// Test threads are in chronological order
|
||||
// Test threads are in chronological order (first thread should be first because it has a more recent reply)
|
||||
expect(timeline.getEvents().map((it) => it.event.event_id)).toEqual([
|
||||
THREAD_ROOT.event_id,
|
||||
THREAD2_ROOT.event_id,
|
||||
@@ -1578,7 +1560,6 @@ describe("MatrixClient event timelines", function () {
|
||||
thread.initialEventsFetched = true;
|
||||
const prom = emitPromise(room, ThreadEvent.NewReply);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD2_ROOT);
|
||||
await room.addLiveEvents([THREAD_REPLY2]);
|
||||
await httpBackend.flushAllExpected();
|
||||
await prom;
|
||||
@@ -1655,7 +1636,7 @@ describe("MatrixClient event timelines", function () {
|
||||
...THREAD_ROOT.unsigned!["m.relations"],
|
||||
"io.element.thread": {
|
||||
...THREAD_ROOT.unsigned!["m.relations"]!["io.element.thread"],
|
||||
count: 2,
|
||||
count: 1,
|
||||
latest_event: THREAD_REPLY,
|
||||
},
|
||||
},
|
||||
@@ -1704,7 +1685,6 @@ describe("MatrixClient event timelines", function () {
|
||||
thread.initialEventsFetched = true;
|
||||
const prom = emitPromise(room, ThreadEvent.Update);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD2_ROOT);
|
||||
await room.addLiveEvents([THREAD_REPLY_REACTION]);
|
||||
await httpBackend.flushAllExpected();
|
||||
await prom;
|
||||
@@ -1942,7 +1922,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
// a state event, followed by a redaction thereof
|
||||
const event = utils.mkMembership({
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: otherUserId,
|
||||
});
|
||||
const redaction = utils.mkEvent({
|
||||
@@ -2019,11 +1999,6 @@ describe("MatrixClient event timelines", function () {
|
||||
},
|
||||
},
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
@@ -2034,9 +2009,7 @@ describe("MatrixClient event timelines", function () {
|
||||
"GET",
|
||||
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
|
||||
encodeURIComponent(THREAD_ROOT.event_id!) +
|
||||
"/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) +
|
||||
buildRelationPaginationQuery({ dir: Direction.Backward, limit: 1 }),
|
||||
buildRelationPaginationQuery({ dir: Direction.Backward }),
|
||||
)
|
||||
.respond(200, function () {
|
||||
return {
|
||||
|
||||
@@ -19,7 +19,16 @@ import { Mocked } from "jest-mock";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { CRYPTO_ENABLED, IStoredClientOpts, MatrixClient } from "../../src/client";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { Filter, KnockRoomOpts, MemoryStore, Method, Room, SERVICE_TYPES } from "../../src/matrix";
|
||||
import {
|
||||
Filter,
|
||||
JoinRule,
|
||||
KnockRoomOpts,
|
||||
MemoryStore,
|
||||
Method,
|
||||
Room,
|
||||
RoomSummary,
|
||||
SERVICE_TYPES,
|
||||
} from "../../src/matrix";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { THREAD_RELATION_TYPE } from "../../src/models/thread";
|
||||
import { IFilterDefinition } from "../../src/filter";
|
||||
@@ -27,6 +36,7 @@ import { ISearchResults } from "../../src/@types/search";
|
||||
import { IStore } from "../../src/store";
|
||||
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
|
||||
import { SetPresence } from "../../src/sync";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
describe("MatrixClient", function () {
|
||||
const userId = "@alice:localhost";
|
||||
@@ -162,7 +172,7 @@ describe("MatrixClient", function () {
|
||||
utils.mkMembership({
|
||||
user: userId,
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
event: true,
|
||||
}),
|
||||
]);
|
||||
@@ -182,7 +192,7 @@ describe("MatrixClient", function () {
|
||||
utils.mkMembership({
|
||||
user: userId,
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
event: true,
|
||||
}),
|
||||
]);
|
||||
@@ -269,7 +279,7 @@ describe("MatrixClient", function () {
|
||||
utils.mkMembership({
|
||||
user: userId,
|
||||
room: roomId,
|
||||
mship: "knock",
|
||||
mship: KnownMembership.Knock,
|
||||
event: true,
|
||||
}),
|
||||
]);
|
||||
@@ -1709,6 +1719,102 @@ describe("MatrixClient", function () {
|
||||
await Promise.all([client.unbindThreePid("email", "alice@server.com"), httpBackend.flushAllExpected()]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRoomSummary", () => {
|
||||
const roomId = "!foo:bar";
|
||||
const encodedRoomId = encodeURIComponent(roomId);
|
||||
|
||||
const roomSummary: RoomSummary = {
|
||||
"room_id": roomId,
|
||||
"name": "My Room",
|
||||
"avatar_url": "",
|
||||
"topic": "My room topic",
|
||||
"world_readable": false,
|
||||
"guest_can_join": false,
|
||||
"num_joined_members": 1,
|
||||
"room_type": "",
|
||||
"join_rule": JoinRule.Public,
|
||||
"membership": "leave",
|
||||
"im.nheko.summary.room_version": "6",
|
||||
"im.nheko.summary.encryption": "algo",
|
||||
};
|
||||
|
||||
const prefix = "/_matrix/client/unstable/im.nheko.summary/";
|
||||
const suffix = `summary/${encodedRoomId}`;
|
||||
const deprecatedSuffix = `rooms/${encodedRoomId}/summary`;
|
||||
|
||||
const errorUnrecogStatus = 404;
|
||||
const errorUnrecogBody = {
|
||||
errcode: "M_UNRECOGNIZED",
|
||||
error: "Unsupported endpoint",
|
||||
};
|
||||
|
||||
const errorBadreqStatus = 400;
|
||||
const errorBadreqBody = {
|
||||
errcode: "M_UNKNOWN",
|
||||
error: "Invalid request",
|
||||
};
|
||||
|
||||
it("should respond with a valid room summary object", () => {
|
||||
httpBackend.when("GET", prefix + suffix).respond(200, roomSummary);
|
||||
|
||||
const prom = client.getRoomSummary(roomId).then((response) => {
|
||||
expect(response).toEqual(roomSummary);
|
||||
});
|
||||
|
||||
httpBackend.flush("");
|
||||
return prom;
|
||||
});
|
||||
|
||||
it("should allow fallback to the deprecated endpoint", () => {
|
||||
httpBackend.when("GET", prefix + suffix).respond(errorUnrecogStatus, errorUnrecogBody);
|
||||
httpBackend.when("GET", prefix + deprecatedSuffix).respond(200, roomSummary);
|
||||
|
||||
const prom = client.getRoomSummary(roomId).then((response) => {
|
||||
expect(response).toEqual(roomSummary);
|
||||
});
|
||||
|
||||
httpBackend.flush("");
|
||||
return prom;
|
||||
});
|
||||
|
||||
it("should respond to unsupported path with error", () => {
|
||||
httpBackend.when("GET", prefix + suffix).respond(errorUnrecogStatus, errorUnrecogBody);
|
||||
httpBackend.when("GET", prefix + deprecatedSuffix).respond(errorUnrecogStatus, errorUnrecogBody);
|
||||
|
||||
const prom = client.getRoomSummary(roomId).then(
|
||||
function (response) {
|
||||
throw Error("request not failed");
|
||||
},
|
||||
function (error) {
|
||||
expect(error.httpStatus).toEqual(errorUnrecogStatus);
|
||||
expect(error.errcode).toEqual(errorUnrecogBody.errcode);
|
||||
expect(error.message).toEqual(`MatrixError: [${errorUnrecogStatus}] ${errorUnrecogBody.error}`);
|
||||
},
|
||||
);
|
||||
|
||||
httpBackend.flush("");
|
||||
return prom;
|
||||
});
|
||||
|
||||
it("should respond to invalid path arguments with error", () => {
|
||||
httpBackend.when("GET", prefix).respond(errorBadreqStatus, errorBadreqBody);
|
||||
|
||||
const prom = client.getRoomSummary("notAroom").then(
|
||||
function (response) {
|
||||
throw Error("request not failed");
|
||||
},
|
||||
function (error) {
|
||||
expect(error.httpStatus).toEqual(errorBadreqStatus);
|
||||
expect(error.errcode).toEqual(errorBadreqBody.errcode);
|
||||
expect(error.message).toEqual(`MatrixError: [${errorBadreqStatus}] ${errorBadreqBody.error}`);
|
||||
},
|
||||
);
|
||||
|
||||
httpBackend.flush("");
|
||||
return prom;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function withThreadId(event: MatrixEvent, newThreadId: string): MatrixEvent {
|
||||
@@ -1912,7 +2018,7 @@ const buildEventJoinRules = () =>
|
||||
new MatrixEvent({
|
||||
age: 80123696,
|
||||
content: {
|
||||
join_rule: "invite",
|
||||
join_rule: KnownMembership.Invite,
|
||||
},
|
||||
event_id: "$6JDDeDp7fEc0F6YnTWMruNcKWFltR3e9wk7wWDDJrAU",
|
||||
origin_server_ts: 1643815441191,
|
||||
@@ -1966,7 +2072,7 @@ const buildEventMember = () =>
|
||||
content: {
|
||||
avatar_url: "mxc://matrix.org/aNtbVcFfwotudypZcHsIcPOc",
|
||||
displayname: "andybalaam-test1",
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
},
|
||||
event_id: "$Ex5eVmMs_ti784mo8bgddynbwLvy6231lCycJr7Cl9M",
|
||||
origin_server_ts: 1643815439608,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { MatrixScheduler } from "../../src/scheduler";
|
||||
import { MemoryStore } from "../../src/store/memory";
|
||||
import { MatrixError } from "../../src/http-api";
|
||||
import { IStore } from "../../src/store";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
describe("MatrixClient opts", function () {
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
@@ -43,13 +44,13 @@ describe("MatrixClient opts", function () {
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: userB,
|
||||
name: "Bob",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: userId,
|
||||
name: "Alice",
|
||||
}),
|
||||
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
|
||||
import { EventStatus, RoomEvent, MatrixClient, MatrixScheduler } from "../../src/matrix";
|
||||
import { EventStatus, MatrixClient, MatrixScheduler, MsgType, RoomEvent } from "../../src/matrix";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
@@ -60,7 +60,7 @@ describe("MatrixClient retrying", function () {
|
||||
// send a couple of events; the second will be queued
|
||||
const p1 = client!
|
||||
.sendMessage(roomId, {
|
||||
msgtype: "m.text",
|
||||
msgtype: MsgType.Text,
|
||||
body: "m1",
|
||||
})
|
||||
.then(
|
||||
@@ -77,7 +77,7 @@ describe("MatrixClient retrying", function () {
|
||||
// never gets resolved.
|
||||
// https://github.com/matrix-org/matrix-js-sdk/issues/496
|
||||
client!.sendMessage(roomId, {
|
||||
msgtype: "m.text",
|
||||
msgtype: MsgType.Text,
|
||||
body: "m2",
|
||||
});
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
Room,
|
||||
} from "../../src";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
describe("MatrixClient room timelines", function () {
|
||||
const userId = "@alice:localhost";
|
||||
@@ -42,7 +43,7 @@ describe("MatrixClient room timelines", function () {
|
||||
|
||||
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: userId,
|
||||
name: userName,
|
||||
});
|
||||
@@ -76,7 +77,7 @@ describe("MatrixClient room timelines", function () {
|
||||
ROOM_NAME_EVENT,
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: otherUserId,
|
||||
name: "Bob",
|
||||
}),
|
||||
@@ -316,7 +317,7 @@ describe("MatrixClient room timelines", function () {
|
||||
|
||||
// make an m.room.member event for alice's join
|
||||
const joinMshipEvent = utils.mkMembership({
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: userId,
|
||||
room: roomId,
|
||||
name: "Old Alice",
|
||||
@@ -326,7 +327,7 @@ describe("MatrixClient room timelines", function () {
|
||||
// make an m.room.member event with prev_content for alice's nick
|
||||
// change
|
||||
const oldMshipEvent = utils.mkMembership({
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: userId,
|
||||
room: roomId,
|
||||
name: userName,
|
||||
@@ -335,7 +336,7 @@ describe("MatrixClient room timelines", function () {
|
||||
oldMshipEvent.prev_content = {
|
||||
displayname: "Old Alice",
|
||||
avatar_url: undefined,
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
};
|
||||
|
||||
// set the list of events to return on scrollback (/messages)
|
||||
@@ -487,7 +488,7 @@ describe("MatrixClient room timelines", function () {
|
||||
utils.mkMembership({
|
||||
user: userId,
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
name: "New Name",
|
||||
}),
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
@@ -554,13 +555,13 @@ describe("MatrixClient room timelines", function () {
|
||||
utils.mkMembership({
|
||||
user: userC,
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
name: "C",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userC,
|
||||
room: roomId,
|
||||
mship: "invite",
|
||||
mship: KnownMembership.Invite,
|
||||
skey: userD,
|
||||
}),
|
||||
];
|
||||
@@ -571,9 +572,9 @@ describe("MatrixClient room timelines", function () {
|
||||
return Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]).then(function () {
|
||||
expect(room.currentState.getMembers().length).toEqual(4);
|
||||
expect(room.currentState.getMember(userC)!.name).toEqual("C");
|
||||
expect(room.currentState.getMember(userC)!.membership).toEqual("join");
|
||||
expect(room.currentState.getMember(userC)!.membership).toEqual(KnownMembership.Join);
|
||||
expect(room.currentState.getMember(userD)!.name).toEqual(userD);
|
||||
expect(room.currentState.getMember(userD)!.membership).toEqual("invite");
|
||||
expect(room.currentState.getMember(userD)!.membership).toEqual(KnownMembership.Invite);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -598,9 +599,9 @@ describe("MatrixClient room timelines", function () {
|
||||
expect(room.timeline[0].event).toEqual(eventData[0]);
|
||||
expect(room.currentState.getMembers().length).toEqual(2);
|
||||
expect(room.currentState.getMember(userId)!.name).toEqual(userName);
|
||||
expect(room.currentState.getMember(userId)!.membership).toEqual("join");
|
||||
expect(room.currentState.getMember(userId)!.membership).toEqual(KnownMembership.Join);
|
||||
expect(room.currentState.getMember(otherUserId)!.name).toEqual("Bob");
|
||||
expect(room.currentState.getMember(otherUserId)!.membership).toEqual("join");
|
||||
expect(room.currentState.getMember(otherUserId)!.membership).toEqual(KnownMembership.Join);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
IndexedDBStore,
|
||||
RelationType,
|
||||
EventType,
|
||||
MatrixEventEvent,
|
||||
} from "../../src";
|
||||
import { ReceiptType } from "../../src/@types/read_receipts";
|
||||
import { UNREAD_THREAD_NOTIFICATIONS } from "../../src/@types/sync";
|
||||
@@ -46,6 +47,8 @@ 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";
|
||||
import { IActionsObject } from "../../src/pushprocessor";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
describe("MatrixClient syncing", () => {
|
||||
const selfUserId = "@alice:localhost";
|
||||
@@ -123,7 +126,7 @@ describe("MatrixClient syncing", () => {
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "invite",
|
||||
membership: KnownMembership.Invite,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -151,10 +154,10 @@ describe("MatrixClient syncing", () => {
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "leave",
|
||||
membership: KnownMembership.Leave,
|
||||
},
|
||||
prev_content: {
|
||||
membership: "invite",
|
||||
membership: KnownMembership.Invite,
|
||||
},
|
||||
// XXX: And other fields required on an event
|
||||
},
|
||||
@@ -167,10 +170,10 @@ describe("MatrixClient syncing", () => {
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "leave",
|
||||
membership: KnownMembership.Leave,
|
||||
},
|
||||
prev_content: {
|
||||
membership: "invite",
|
||||
membership: KnownMembership.Invite,
|
||||
},
|
||||
// XXX: And other fields required on an event
|
||||
},
|
||||
@@ -193,22 +196,22 @@ describe("MatrixClient syncing", () => {
|
||||
// Room, string, string
|
||||
fires++;
|
||||
expect(room.roomId).toBe(roomId);
|
||||
expect(membership).toBe("invite");
|
||||
expect(membership).toBe(KnownMembership.Invite);
|
||||
expect(oldMembership).toBeFalsy();
|
||||
|
||||
// Second fire: a leave
|
||||
client!.once(RoomEvent.MyMembership, (room, membership, oldMembership) => {
|
||||
fires++;
|
||||
expect(room.roomId).toBe(roomId);
|
||||
expect(membership).toBe("leave");
|
||||
expect(oldMembership).toBe("invite");
|
||||
expect(membership).toBe(KnownMembership.Leave);
|
||||
expect(oldMembership).toBe(KnownMembership.Invite);
|
||||
|
||||
// Third/final fire: a second invite
|
||||
client!.once(RoomEvent.MyMembership, (room, membership, oldMembership) => {
|
||||
fires++;
|
||||
expect(room.roomId).toBe(roomId);
|
||||
expect(membership).toBe("invite");
|
||||
expect(oldMembership).toBe("leave");
|
||||
expect(membership).toBe(KnownMembership.Invite);
|
||||
expect(oldMembership).toBe(KnownMembership.Leave);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -238,7 +241,7 @@ describe("MatrixClient syncing", () => {
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "knock",
|
||||
membership: KnownMembership.Knock,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -266,10 +269,10 @@ describe("MatrixClient syncing", () => {
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "leave",
|
||||
membership: KnownMembership.Leave,
|
||||
},
|
||||
prev_content: {
|
||||
membership: "knock",
|
||||
membership: KnownMembership.Knock,
|
||||
},
|
||||
// XXX: And other fields required on an event
|
||||
},
|
||||
@@ -282,10 +285,10 @@ describe("MatrixClient syncing", () => {
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "leave",
|
||||
membership: KnownMembership.Leave,
|
||||
},
|
||||
prev_content: {
|
||||
membership: "knock",
|
||||
membership: KnownMembership.Knock,
|
||||
},
|
||||
// XXX: And other fields required on an event
|
||||
},
|
||||
@@ -308,22 +311,22 @@ describe("MatrixClient syncing", () => {
|
||||
// Room, string, string
|
||||
fires++;
|
||||
expect(room.roomId).toBe(roomId);
|
||||
expect(membership).toBe("knock");
|
||||
expect(membership).toBe(KnownMembership.Knock);
|
||||
expect(oldMembership).toBeFalsy();
|
||||
|
||||
// Second fire: a leave
|
||||
client!.once(RoomEvent.MyMembership, (room, membership, oldMembership) => {
|
||||
fires++;
|
||||
expect(room.roomId).toBe(roomId);
|
||||
expect(membership).toBe("leave");
|
||||
expect(oldMembership).toBe("knock");
|
||||
expect(membership).toBe(KnownMembership.Leave);
|
||||
expect(oldMembership).toBe(KnownMembership.Knock);
|
||||
|
||||
// Third/final fire: a second knock
|
||||
client!.once(RoomEvent.MyMembership, (room, membership, oldMembership) => {
|
||||
fires++;
|
||||
expect(room.roomId).toBe(roomId);
|
||||
expect(membership).toBe("knock");
|
||||
expect(oldMembership).toBe("leave");
|
||||
expect(membership).toBe(KnownMembership.Knock);
|
||||
expect(oldMembership).toBe(KnownMembership.Leave);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -381,7 +384,7 @@ describe("MatrixClient syncing", () => {
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "invite",
|
||||
membership: KnownMembership.Invite,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -421,7 +424,7 @@ describe("MatrixClient syncing", () => {
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "knock",
|
||||
membership: KnownMembership.Knock,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -533,12 +536,12 @@ describe("MatrixClient syncing", () => {
|
||||
events: [
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: otherUserId,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: selfUserId,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
@@ -556,7 +559,7 @@ describe("MatrixClient syncing", () => {
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: "invite",
|
||||
mship: KnownMembership.Invite,
|
||||
user: userC,
|
||||
}) as IStateEvent,
|
||||
);
|
||||
@@ -589,7 +592,7 @@ describe("MatrixClient syncing", () => {
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: "invite",
|
||||
mship: KnownMembership.Invite,
|
||||
user: userC,
|
||||
}) as IStateEvent,
|
||||
);
|
||||
@@ -617,7 +620,7 @@ describe("MatrixClient syncing", () => {
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: "invite",
|
||||
mship: KnownMembership.Invite,
|
||||
user: userC,
|
||||
}) as IStateEvent,
|
||||
);
|
||||
@@ -644,7 +647,7 @@ describe("MatrixClient syncing", () => {
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: "invite",
|
||||
mship: KnownMembership.Invite,
|
||||
user: userC,
|
||||
}) as IStateEvent,
|
||||
);
|
||||
@@ -719,12 +722,12 @@ describe("MatrixClient syncing", () => {
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: otherUserId,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: selfUserId,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
@@ -750,13 +753,13 @@ describe("MatrixClient syncing", () => {
|
||||
events: [
|
||||
utils.mkMembership({
|
||||
room: roomTwo,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: otherUserId,
|
||||
name: otherDisplayName,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomTwo,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: selfUserId,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
@@ -1247,7 +1250,7 @@ describe("MatrixClient syncing", () => {
|
||||
|
||||
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: userA,
|
||||
});
|
||||
|
||||
@@ -1508,12 +1511,12 @@ describe("MatrixClient syncing", () => {
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: otherUserId,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: selfUserId,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
@@ -1605,12 +1608,12 @@ describe("MatrixClient syncing", () => {
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: otherUserId,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: selfUserId,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
@@ -1645,6 +1648,99 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should zero total notifications for threads when absent from the notifications object", async () => {
|
||||
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
|
||||
[THREAD_ID]: {
|
||||
highlight_count: 2,
|
||||
notification_count: 5,
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client!.startClient();
|
||||
|
||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||
|
||||
const room = client!.getRoom(roomOne);
|
||||
|
||||
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(5);
|
||||
|
||||
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {};
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||
|
||||
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(0);
|
||||
});
|
||||
|
||||
it("should zero highlight notifications for threads in encrypted rooms", async () => {
|
||||
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
|
||||
[THREAD_ID]: {
|
||||
highlight_count: 2,
|
||||
notification_count: 5,
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client!.startClient();
|
||||
|
||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||
|
||||
const room = client!.getRoom(roomOne);
|
||||
|
||||
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(5);
|
||||
|
||||
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
|
||||
[THREAD_ID]: {
|
||||
highlight_count: 0,
|
||||
notification_count: 0,
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||
|
||||
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0);
|
||||
});
|
||||
|
||||
it("should not zero highlight notifications for threads in encrypted rooms", async () => {
|
||||
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
|
||||
[THREAD_ID]: {
|
||||
highlight_count: 2,
|
||||
notification_count: 5,
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client!.startClient();
|
||||
|
||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||
|
||||
const room = client!.getRoom(roomOne);
|
||||
room!.hasEncryptionStateEvent = jest.fn().mockReturnValue(true);
|
||||
|
||||
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(5);
|
||||
|
||||
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
|
||||
[THREAD_ID]: {
|
||||
highlight_count: 0,
|
||||
notification_count: 0,
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||
|
||||
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(0);
|
||||
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(2);
|
||||
});
|
||||
|
||||
it("caches unknown threads receipts and replay them when the thread is created", async () => {
|
||||
const THREAD_ID = "$unknownthread:localhost";
|
||||
|
||||
@@ -1732,64 +1828,351 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should apply encrypted notification logic for events within the same sync blob", async () => {
|
||||
const roomId = "!room123:server";
|
||||
const syncData = {
|
||||
rooms: {
|
||||
join: {
|
||||
[roomId]: {
|
||||
ephemeral: {
|
||||
events: [],
|
||||
},
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
room: roomId,
|
||||
event: true,
|
||||
skey: "",
|
||||
type: EventType.RoomEncryption,
|
||||
content: {},
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId,
|
||||
user: otherUserId,
|
||||
msg: "hello",
|
||||
}),
|
||||
],
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
user: otherUserId,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
user: selfUserId,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create",
|
||||
room: roomId,
|
||||
user: selfUserId,
|
||||
content: {},
|
||||
}),
|
||||
],
|
||||
describe("encrypted notification logic", () => {
|
||||
let roomId: string;
|
||||
let syncData: ISyncResponse;
|
||||
|
||||
beforeEach(() => {
|
||||
roomId = "!room123:server";
|
||||
syncData = {
|
||||
rooms: {
|
||||
join: {
|
||||
[roomId]: {
|
||||
ephemeral: {
|
||||
events: [],
|
||||
},
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
room: roomId,
|
||||
event: true,
|
||||
skey: "",
|
||||
type: EventType.RoomEncryption,
|
||||
content: {},
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId,
|
||||
user: otherUserId,
|
||||
msg: "hello",
|
||||
}),
|
||||
],
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: KnownMembership.Join,
|
||||
user: otherUserId,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: KnownMembership.Join,
|
||||
user: selfUserId,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create",
|
||||
room: roomId,
|
||||
user: selfUserId,
|
||||
content: {},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as ISyncResponse;
|
||||
} as unknown as ISyncResponse;
|
||||
});
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
client!.startClient();
|
||||
it("should apply encrypted notification logic for events within the same sync blob", async () => {
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
client!.startClient();
|
||||
|
||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeInstanceOf(Room);
|
||||
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeInstanceOf(Room);
|
||||
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
|
||||
});
|
||||
|
||||
it("should recalculate highlights on unthreaded receipt for encrypted rooms", async () => {
|
||||
const myUserId = client!.getUserId()!;
|
||||
|
||||
const firstEventId = syncData.rooms.join[roomId].timeline.events[1].event_id;
|
||||
|
||||
// add a receipt for the first event in the room (let's say the user has already read that one)
|
||||
syncData.rooms.join[roomId].ephemeral.events = [
|
||||
{
|
||||
content: {
|
||||
[firstEventId]: {
|
||||
"m.read": {
|
||||
[myUserId]: { ts: 1 },
|
||||
},
|
||||
},
|
||||
},
|
||||
type: "m.receipt",
|
||||
},
|
||||
];
|
||||
|
||||
// Now add a highlighting event after that receipt
|
||||
const pingEvent = utils.mkMessage({
|
||||
room: roomId,
|
||||
user: otherUserId,
|
||||
msg: client?.getUserId() + " ping",
|
||||
}) as IRoomEvent;
|
||||
syncData.rooms.join[roomId].timeline.events.push(pingEvent);
|
||||
|
||||
// fudge this to make it a highlight
|
||||
client!.getPushActionsForEvent = (ev: MatrixEvent): IActionsObject | null => {
|
||||
if (ev.getId() === pingEvent.event_id) {
|
||||
return {
|
||||
notify: true,
|
||||
tweaks: {
|
||||
highlight: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
client!.startClient();
|
||||
|
||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeInstanceOf(Room);
|
||||
// the room should now have one highlight since our receipt was before the ping message
|
||||
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1);
|
||||
});
|
||||
|
||||
it("should recalculate highlights on main thread receipt for encrypted rooms", async () => {
|
||||
const myUserId = client!.getUserId()!;
|
||||
|
||||
const firstEventId = syncData.rooms.join[roomId].timeline.events[1].event_id;
|
||||
|
||||
// add a receipt for the first event in the room (let's say the user has already read that one)
|
||||
syncData.rooms.join[roomId].ephemeral.events = [
|
||||
{
|
||||
content: {
|
||||
[firstEventId]: {
|
||||
"m.read": {
|
||||
[myUserId]: { ts: 1, thread_id: "main" },
|
||||
},
|
||||
},
|
||||
},
|
||||
type: "m.receipt",
|
||||
},
|
||||
];
|
||||
|
||||
// Now add a highlighting event after that receipt
|
||||
const pingEvent = utils.mkMessage({
|
||||
room: roomId,
|
||||
user: otherUserId,
|
||||
msg: client?.getUserId() + " ping",
|
||||
}) as IRoomEvent;
|
||||
syncData.rooms.join[roomId].timeline.events.push(pingEvent);
|
||||
|
||||
// fudge this to make it a highlight
|
||||
client!.getPushActionsForEvent = (ev: MatrixEvent): IActionsObject | null => {
|
||||
if (ev.getId() === pingEvent.event_id) {
|
||||
return {
|
||||
notify: true,
|
||||
tweaks: {
|
||||
highlight: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
client!.startClient();
|
||||
|
||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeInstanceOf(Room);
|
||||
// the room should now have one highlight since our receipt was before the ping message
|
||||
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1);
|
||||
});
|
||||
|
||||
describe("notification processing in threads", () => {
|
||||
let threadEvent1: IRoomEvent;
|
||||
let threadEvent2: IRoomEvent;
|
||||
let firstEventId: string;
|
||||
|
||||
beforeEach(() => {
|
||||
firstEventId = syncData.rooms.join[roomId].timeline.events[1].event_id;
|
||||
|
||||
// Add a threaded event off of the first event
|
||||
threadEvent1 = utils.mkEvent({
|
||||
type: EventType.RoomMessage,
|
||||
user: otherUserId,
|
||||
room: roomId,
|
||||
ts: 500,
|
||||
content: {
|
||||
"body": "first thread response",
|
||||
"m.relates_to": {
|
||||
"event_id": firstEventId,
|
||||
"m.in_reply_to": {
|
||||
event_id: firstEventId,
|
||||
},
|
||||
"rel_type": "io.element.thread",
|
||||
},
|
||||
},
|
||||
}) as IRoomEvent;
|
||||
syncData.rooms.join[roomId].timeline.events.push(threadEvent1);
|
||||
|
||||
// ...and another
|
||||
threadEvent2 = utils.mkEvent({
|
||||
type: EventType.RoomMessage,
|
||||
user: otherUserId,
|
||||
room: roomId,
|
||||
ts: 1500,
|
||||
content: {
|
||||
"body": "second thread response",
|
||||
"m.relates_to": {
|
||||
"event_id": firstEventId,
|
||||
"m.in_reply_to": {
|
||||
event_id: firstEventId,
|
||||
},
|
||||
"rel_type": "io.element.thread",
|
||||
},
|
||||
},
|
||||
}) as IRoomEvent;
|
||||
syncData.rooms.join[roomId].timeline.events.push(threadEvent2);
|
||||
|
||||
// fudge to make these highlights
|
||||
client!.getPushActionsForEvent = (ev: MatrixEvent): IActionsObject | null => {
|
||||
if ([threadEvent1.event_id, threadEvent2.event_id].includes(ev.getId()!)) {
|
||||
return {
|
||||
notify: true,
|
||||
tweaks: {
|
||||
highlight: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
});
|
||||
|
||||
it("checks threads with notifications on unthreaded receipts", async () => {
|
||||
const myUserId = client!.getUserId()!;
|
||||
|
||||
// add a receipt for a random, ficticious thread, otherwise the client will
|
||||
// think that the thread is before any threaded receipts and ignore it.
|
||||
syncData.rooms.join[roomId].ephemeral.events = [
|
||||
{
|
||||
content: {
|
||||
[firstEventId]: {
|
||||
"m.read": {
|
||||
[myUserId]: { ts: 1, thread_id: "some_other_thread" },
|
||||
},
|
||||
},
|
||||
},
|
||||
type: "m.receipt",
|
||||
},
|
||||
];
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
client!.startClient({ threadSupport: true });
|
||||
|
||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||
|
||||
const room = client!.getRoom(roomId)!;
|
||||
|
||||
// pretend that the client has decrypted an event to trigger it to compute
|
||||
// local notifications
|
||||
client?.emit(MatrixEventEvent.Decrypted, room.findEventById(firstEventId)!);
|
||||
client?.emit(MatrixEventEvent.Decrypted, room.findEventById(threadEvent1.event_id)!);
|
||||
client?.emit(MatrixEventEvent.Decrypted, room.findEventById(threadEvent2.event_id)!);
|
||||
|
||||
expect(room).toBeInstanceOf(Room);
|
||||
|
||||
// we should now have one highlight: the unread message that pings
|
||||
expect(
|
||||
room.getThreadUnreadNotificationCount(firstEventId, NotificationCountType.Highlight),
|
||||
).toEqual(2);
|
||||
|
||||
const syncData2 = {
|
||||
rooms: {
|
||||
join: {
|
||||
[roomId]: {
|
||||
ephemeral: {
|
||||
events: [
|
||||
{
|
||||
content: {
|
||||
[firstEventId]: {
|
||||
"m.read": {
|
||||
[myUserId]: { ts: 1 },
|
||||
},
|
||||
},
|
||||
},
|
||||
type: "m.receipt",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as ISyncResponse;
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData2);
|
||||
|
||||
await Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]);
|
||||
|
||||
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Highlight)).toBe(0);
|
||||
});
|
||||
|
||||
it("should recalculate highlights on threaded receipt for encrypted rooms", async () => {
|
||||
const myUserId = client!.getUserId()!;
|
||||
|
||||
// add a receipt for the first message in the threadm leaving the second one unread
|
||||
syncData.rooms.join[roomId].ephemeral.events = [
|
||||
{
|
||||
content: {
|
||||
[threadEvent1.event_id]: {
|
||||
"m.read": {
|
||||
[myUserId]: { ts: 1, thread_id: firstEventId },
|
||||
},
|
||||
},
|
||||
},
|
||||
type: "m.receipt",
|
||||
},
|
||||
];
|
||||
|
||||
// fudge to make both thread replies highlights
|
||||
client!.getPushActionsForEvent = (ev: MatrixEvent): IActionsObject | null => {
|
||||
if ([threadEvent1.event_id, threadEvent2.event_id].includes(ev.getId()!)) {
|
||||
return {
|
||||
notify: true,
|
||||
tweaks: {
|
||||
highlight: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
client!.startClient({ threadSupport: true });
|
||||
|
||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeInstanceOf(Room);
|
||||
|
||||
// pretend that the client has decrypted an event to trigger it to compute
|
||||
// local notifications
|
||||
client?.emit(MatrixEventEvent.Decrypted, room.findEventById(firstEventId)!);
|
||||
|
||||
// the room should now have one highlight: the second thread message
|
||||
|
||||
expect(room.getThreadUnreadNotificationCount(firstEventId, NotificationCountType.Highlight)).toBe(
|
||||
1,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1901,7 +2284,7 @@ describe("MatrixClient syncing", () => {
|
||||
it("should return a room based on the room initialSync API", async () => {
|
||||
httpBackend!.when("GET", `/rooms/${encodeURIComponent(roomOne)}/initialSync`).respond(200, {
|
||||
room_id: roomOne,
|
||||
membership: "leave",
|
||||
membership: KnownMembership.Leave,
|
||||
messages: {
|
||||
start: "start",
|
||||
end: "end",
|
||||
@@ -1950,7 +2333,7 @@ describe("MatrixClient syncing", () => {
|
||||
const room = await prom;
|
||||
|
||||
expect(room.roomId).toBe(roomOne);
|
||||
expect(room.getMyMembership()).toBe("leave");
|
||||
expect(room.getMyMembership()).toBe(KnownMembership.Leave);
|
||||
expect(room.name).toBe("Room Name");
|
||||
expect(room.currentState.getStateEvents("m.room.name", "")?.getId()).toBe("$eventId");
|
||||
expect(room.timeline[0].getContent().body).toBe("Message 1");
|
||||
@@ -2042,7 +2425,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "invite",
|
||||
membership: KnownMembership.Invite,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -28,32 +28,71 @@ import {
|
||||
NotificationCountType,
|
||||
RelationType,
|
||||
Room,
|
||||
fixNotificationCountOnDecryption,
|
||||
} from "../../src";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { ReceiptType } from "../../src/@types/read_receipts";
|
||||
import { mkThread } from "../test-utils/thread";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
const userA = "@alice:localhost";
|
||||
const userB = "@bob:localhost";
|
||||
const selfUserId = userA;
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
|
||||
function setupTestClient(): [MatrixClient, HttpBackend] {
|
||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
const httpBackend = testClient.httpBackend;
|
||||
const client = testClient.client;
|
||||
httpBackend!.when("GET", "/versions").respond(200, {});
|
||||
httpBackend!.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
return [client, httpBackend];
|
||||
}
|
||||
|
||||
describe("Notification count fixing", () => {
|
||||
let client: MatrixClient | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
[client] = setupTestClient();
|
||||
});
|
||||
|
||||
it("doesn't increment notification count for events that can't be found in a room", async () => {
|
||||
const roomId = "!room:localhost";
|
||||
|
||||
client!.startClient({ threadSupport: true });
|
||||
const room = new Room(roomId, client!, selfUserId);
|
||||
jest.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
|
||||
|
||||
const event = new MatrixEvent({
|
||||
room_id: roomId,
|
||||
type: "m.reaction",
|
||||
event_id: "$foo",
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Annotation,
|
||||
event_id: "$foo",
|
||||
key: "x",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
jest.spyOn(event, "getPushActions").mockReturnValue({
|
||||
notify: true,
|
||||
tweaks: {},
|
||||
});
|
||||
|
||||
fixNotificationCountOnDecryption(client!, event);
|
||||
|
||||
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MatrixClient syncing", () => {
|
||||
const userA = "@alice:localhost";
|
||||
const userB = "@bob:localhost";
|
||||
|
||||
const selfUserId = userA;
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
|
||||
let client: MatrixClient | undefined;
|
||||
let httpBackend: HttpBackend | undefined;
|
||||
|
||||
const setupTestClient = (): [MatrixClient, HttpBackend] => {
|
||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
const httpBackend = testClient.httpBackend;
|
||||
const client = testClient.client;
|
||||
httpBackend!.when("GET", "/versions").respond(200, {});
|
||||
httpBackend!.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
return [client, httpBackend];
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
[client, httpBackend] = setupTestClient();
|
||||
});
|
||||
@@ -113,7 +152,7 @@ describe("MatrixClient syncing", () => {
|
||||
await client!.sendEvent(roomId, EventType.Reaction, {
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Annotation,
|
||||
event_id: threadReply.getId(),
|
||||
event_id: threadReply.getId()!,
|
||||
key: "",
|
||||
},
|
||||
});
|
||||
@@ -191,7 +230,7 @@ describe("MatrixClient syncing", () => {
|
||||
content: {
|
||||
avatar_url: "",
|
||||
displayname: userB,
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
},
|
||||
origin_server_ts: 2,
|
||||
sender: userB,
|
||||
@@ -232,7 +271,7 @@ describe("MatrixClient syncing", () => {
|
||||
},
|
||||
{
|
||||
content: {
|
||||
join_rule: "invite",
|
||||
join_rule: KnownMembership.Invite,
|
||||
},
|
||||
origin_server_ts: 4,
|
||||
sender: userB,
|
||||
@@ -278,7 +317,7 @@ describe("MatrixClient syncing", () => {
|
||||
avatar_url: "",
|
||||
displayname: userA,
|
||||
is_direct: true,
|
||||
membership: "invite",
|
||||
membership: KnownMembership.Invite,
|
||||
},
|
||||
origin_server_ts: 8,
|
||||
sender: userB,
|
||||
@@ -300,7 +339,7 @@ describe("MatrixClient syncing", () => {
|
||||
content: {
|
||||
avatar_url: "",
|
||||
displayname: userA,
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
},
|
||||
origin_server_ts: 10,
|
||||
sender: userA,
|
||||
|
||||
@@ -43,6 +43,7 @@ import { IStoredClientOpts } from "../../src";
|
||||
import { logger } from "../../src/logger";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
import { defer } from "../../src/utils";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
describe("SlidingSyncSdk", () => {
|
||||
let client: MatrixClient | undefined;
|
||||
@@ -189,7 +190,7 @@ describe("SlidingSyncSdk", () => {
|
||||
name: "A",
|
||||
required_state: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnStateEvent(EventType.RoomName, { name: "A" }, ""),
|
||||
],
|
||||
@@ -204,7 +205,7 @@ describe("SlidingSyncSdk", () => {
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello B" }),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "world B" }),
|
||||
@@ -216,7 +217,7 @@ describe("SlidingSyncSdk", () => {
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello C" }),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "world C" }),
|
||||
@@ -229,7 +230,7 @@ describe("SlidingSyncSdk", () => {
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello D" }),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "world D" }),
|
||||
@@ -244,7 +245,7 @@ describe("SlidingSyncSdk", () => {
|
||||
invite_state: [
|
||||
{
|
||||
type: EventType.RoomMember,
|
||||
content: { membership: "invite" },
|
||||
content: { membership: KnownMembership.Invite },
|
||||
state_key: selfUserId,
|
||||
sender: "@bob:localhost",
|
||||
event_id: "$room_e_invite",
|
||||
@@ -265,7 +266,7 @@ describe("SlidingSyncSdk", () => {
|
||||
name: "#foo:localhost",
|
||||
required_state: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnStateEvent(EventType.RoomCanonicalAlias, { alias: "#foo:localhost" }, ""),
|
||||
mkOwnStateEvent(EventType.RoomName, { name: "This should be ignored" }, ""),
|
||||
@@ -281,7 +282,7 @@ describe("SlidingSyncSdk", () => {
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
],
|
||||
joined_count: 5,
|
||||
@@ -293,7 +294,7 @@ describe("SlidingSyncSdk", () => {
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "live event" }),
|
||||
],
|
||||
@@ -308,7 +309,7 @@ describe("SlidingSyncSdk", () => {
|
||||
const gotRoom = client!.getRoom(roomA);
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.name).toEqual(data[roomA].name);
|
||||
expect(gotRoom!.getMyMembership()).toEqual("join");
|
||||
expect(gotRoom!.getMyMembership()).toEqual(KnownMembership.Join);
|
||||
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline);
|
||||
});
|
||||
|
||||
@@ -318,7 +319,7 @@ describe("SlidingSyncSdk", () => {
|
||||
const gotRoom = client!.getRoom(roomB);
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.name).toEqual(data[roomB].name);
|
||||
expect(gotRoom!.getMyMembership()).toEqual("join");
|
||||
expect(gotRoom!.getMyMembership()).toEqual(KnownMembership.Join);
|
||||
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline);
|
||||
});
|
||||
|
||||
@@ -372,7 +373,7 @@ describe("SlidingSyncSdk", () => {
|
||||
const gotRoom = client!.getRoom(roomH);
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.name).toEqual(data[roomH].name);
|
||||
expect(gotRoom!.getMyMembership()).toEqual("join");
|
||||
expect(gotRoom!.getMyMembership()).toEqual(KnownMembership.Join);
|
||||
// check the entire timeline is correct
|
||||
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents(), data[roomH].timeline);
|
||||
await expect(seenLiveEventDeferred.promise).resolves.toBeTruthy();
|
||||
@@ -383,7 +384,7 @@ describe("SlidingSyncSdk", () => {
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const gotRoom = client!.getRoom(roomE);
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.getMyMembership()).toEqual("invite");
|
||||
expect(gotRoom!.getMyMembership()).toEqual(KnownMembership.Invite);
|
||||
expect(gotRoom!.currentState.getJoinRule()).toEqual(JoinRule.Invite);
|
||||
});
|
||||
|
||||
@@ -603,9 +604,9 @@ describe("SlidingSyncSdk", () => {
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "invite" }, invitee),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Invite }, invitee),
|
||||
],
|
||||
});
|
||||
await httpBackend!.flush("/profile", 1, 1000);
|
||||
@@ -719,7 +720,7 @@ describe("SlidingSyncSdk", () => {
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
|
||||
],
|
||||
@@ -923,7 +924,7 @@ describe("SlidingSyncSdk", () => {
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
|
||||
],
|
||||
@@ -964,7 +965,7 @@ describe("SlidingSyncSdk", () => {
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
|
||||
],
|
||||
@@ -1050,12 +1051,12 @@ describe("SlidingSyncSdk", () => {
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
{
|
||||
type: EventType.RoomMember,
|
||||
state_key: alice,
|
||||
content: { membership: "join" },
|
||||
content: { membership: KnownMembership.Join },
|
||||
sender: alice,
|
||||
origin_server_ts: Date.now(),
|
||||
event_id: "$alice",
|
||||
|
||||
@@ -107,8 +107,8 @@ describe("SlidingSync", () => {
|
||||
onRequest: (initial) => {
|
||||
return { initial: initial };
|
||||
},
|
||||
onResponse: (res) => {
|
||||
return {};
|
||||
onResponse: async (res) => {
|
||||
return;
|
||||
},
|
||||
when: () => ExtensionState.PreProcess,
|
||||
};
|
||||
@@ -1572,7 +1572,7 @@ describe("SlidingSync", () => {
|
||||
onPreExtensionRequest = () => {
|
||||
return extReq;
|
||||
};
|
||||
onPreExtensionResponse = (resp) => {
|
||||
onPreExtensionResponse = async (resp) => {
|
||||
extensionOnResponseCalled = true;
|
||||
callbackOrder.push("onPreExtensionResponse");
|
||||
expect(resp).toEqual(extResp);
|
||||
@@ -1613,7 +1613,7 @@ describe("SlidingSync", () => {
|
||||
return undefined;
|
||||
};
|
||||
let responseCalled = false;
|
||||
onPreExtensionResponse = (resp) => {
|
||||
onPreExtensionResponse = async (resp) => {
|
||||
responseCalled = true;
|
||||
};
|
||||
httpBackend!
|
||||
@@ -1649,7 +1649,7 @@ describe("SlidingSync", () => {
|
||||
};
|
||||
let responseCalled = false;
|
||||
const callbackOrder: string[] = [];
|
||||
onPostExtensionResponse = (resp) => {
|
||||
onPostExtensionResponse = async (resp) => {
|
||||
expect(resp).toEqual(extResp);
|
||||
responseCalled = true;
|
||||
callbackOrder.push("onPostExtensionResponse");
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
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 { MockOptionsMethodPut } from "fetch-mock";
|
||||
|
||||
import { ISyncResponder } from "./SyncResponder";
|
||||
|
||||
/**
|
||||
* An object which intercepts `account_data` get and set requests via fetch-mock.
|
||||
*/
|
||||
export class AccountDataAccumulator {
|
||||
/**
|
||||
* The account data events to be returned by the sync.
|
||||
* Will be updated when fetchMock intercepts calls to PUT `/_matrix/client/v3/user/:userId/account_data/`.
|
||||
* Will be used by `sendSyncResponseWithUpdatedAccountData`
|
||||
*/
|
||||
public accountDataEvents: Map<String, any> = new Map();
|
||||
|
||||
/**
|
||||
* Intercept requests to set a particular type of account data.
|
||||
*
|
||||
* Once it is set, its data is stored (for future return by `interceptGetAccountData` etc) and the resolved promise is
|
||||
* resolved.
|
||||
*
|
||||
* @param accountDataType - type of account data to be intercepted
|
||||
* @param opts - options to pass to fetchMock
|
||||
* @returns a Promise which will resolve (with the content of the account data) once it is set.
|
||||
*/
|
||||
public interceptSetAccountData(accountDataType: string, opts?: MockOptionsMethodPut): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
// Called when the cross signing key is uploaded
|
||||
fetchMock.put(
|
||||
`express:/_matrix/client/v3/user/:userId/account_data/${accountDataType}`,
|
||||
(url: string, options: RequestInit) => {
|
||||
const content = JSON.parse(options.body as string);
|
||||
const type = url.split("/").pop();
|
||||
// update account data for sync response
|
||||
this.accountDataEvents.set(type!, content);
|
||||
resolve(content);
|
||||
return {};
|
||||
},
|
||||
opts,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercept all requests to get account data
|
||||
*/
|
||||
public interceptGetAccountData(): void {
|
||||
fetchMock.get(
|
||||
`express:/_matrix/client/v3/user/:userId/account_data/:type`,
|
||||
(url) => {
|
||||
const type = url.split("/").pop();
|
||||
const existing = this.accountDataEvents.get(type!);
|
||||
if (existing) {
|
||||
// return it
|
||||
return {
|
||||
status: 200,
|
||||
body: existing,
|
||||
};
|
||||
} else {
|
||||
// 404
|
||||
return {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
|
||||
};
|
||||
}
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a sync response the current account data events.
|
||||
*/
|
||||
public sendSyncResponseWithUpdatedAccountData(syncResponder: ISyncResponder): void {
|
||||
try {
|
||||
syncResponder.sendOrQueueSyncResponse({
|
||||
next_batch: 1,
|
||||
account_data: {
|
||||
events: Array.from(this.accountDataEvents, ([type, content]) => ({
|
||||
type: type,
|
||||
content: content,
|
||||
})),
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
// Might fail with "Cannot queue more than one /sync response" if called too often.
|
||||
// It's ok if it fails here, the sync response is cumulative and will contain
|
||||
// the latest account data.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,11 +24,21 @@ import { KeyBackupInfo } from "../../src/crypto-api";
|
||||
* @param homeserverUrl - the homeserver url for the client under test
|
||||
*/
|
||||
export function mockInitialApiRequests(homeserverUrl: string) {
|
||||
fetchMock.getOnce(new URL("/_matrix/client/versions", homeserverUrl).toString(), { versions: ["v1.1"] });
|
||||
fetchMock.getOnce(new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(), {});
|
||||
fetchMock.postOnce(new URL("/_matrix/client/v3/user/%40alice%3Alocalhost/filter", homeserverUrl).toString(), {
|
||||
filter_id: "fid",
|
||||
});
|
||||
fetchMock.getOnce(
|
||||
new URL("/_matrix/client/versions", homeserverUrl).toString(),
|
||||
{ versions: ["v1.1"] },
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
fetchMock.getOnce(
|
||||
new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(),
|
||||
{},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
fetchMock.postOnce(
|
||||
new URL("/_matrix/client/v3/user/%40alice%3Alocalhost/filter", homeserverUrl).toString(),
|
||||
{ filter_id: "fid" },
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { OidcClientConfig } from "../../src";
|
||||
import { ValidatedIssuerMetadata } from "../../src/oidc/validate";
|
||||
import { OidcClientConfig, ValidatedIssuerMetadata } from "../../src";
|
||||
|
||||
/**
|
||||
* Makes a valid OidcClientConfig with minimum valid values
|
||||
@@ -26,8 +25,7 @@ export const makeDelegatedAuthConfig = (issuer = "https://auth.org/"): OidcClien
|
||||
const metadata = mockOpenIdConfiguration(issuer);
|
||||
|
||||
return {
|
||||
issuer,
|
||||
account: issuer + "account",
|
||||
accountManagementEndpoint: issuer + "account",
|
||||
registrationEndpoint: metadata.registration_endpoint,
|
||||
authorizationEndpoint: metadata.authorization_endpoint,
|
||||
tokenEndpoint: metadata.token_endpoint,
|
||||
@@ -46,6 +44,7 @@ export const mockOpenIdConfiguration = (issuer = "https://auth.org/"): Validated
|
||||
token_endpoint: issuer + "token",
|
||||
authorization_endpoint: issuer + "auth",
|
||||
registration_endpoint: issuer + "registration",
|
||||
device_authorization_endpoint: issuer + "device",
|
||||
jwks_uri: issuer + "jwks",
|
||||
response_types_supported: ["code"],
|
||||
grant_types_supported: ["authorization_code", "refresh_token"],
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { eventMapperFor } from "../../src/event-mapper";
|
||||
import { TEST_ROOM_ID } from "./test-data";
|
||||
import { KnownMembership, Membership } from "../../src/@types/membership";
|
||||
|
||||
/**
|
||||
* Return a promise that is resolved when the client next emits a
|
||||
@@ -87,7 +88,7 @@ export function getSyncResponse(roomMembers: string[], roomId = TEST_ROOM_ID): I
|
||||
for (let i = 0; i < roomMembers.length; i++) {
|
||||
roomResponse.state.events.push(
|
||||
mkMembershipCustom({
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
sender: roomMembers[i],
|
||||
}),
|
||||
);
|
||||
@@ -251,7 +252,7 @@ export function mkPresence(opts: IPresenceOpts & { event?: boolean }): Partial<I
|
||||
|
||||
interface IMembershipOpts {
|
||||
room?: string;
|
||||
mship: string;
|
||||
mship: Membership;
|
||||
sender?: string;
|
||||
user?: string;
|
||||
skey?: string;
|
||||
@@ -297,7 +298,7 @@ export function mkMembership(opts: IMembershipOpts & { event?: boolean }): Parti
|
||||
}
|
||||
|
||||
export function mkMembershipCustom<T>(
|
||||
base: T & { membership: string; sender: string; content?: IContent },
|
||||
base: T & { membership: Membership; sender: string; content?: IContent },
|
||||
): T & { type: EventType; sender: string; state_key: string; content: IContent } & GeneratedMetadata {
|
||||
const content = base.content || {};
|
||||
return mkEventCustom({
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
## Dumps of libolm indexeddb cryptostore
|
||||
|
||||
This directory contains several dumps of real indexeddb stores from a session using
|
||||
libolm crypto.
|
||||
|
||||
Each directory contains, in dump.json, a dump of data created by pasting the following
|
||||
code into the browser console; and in index.ts, details of the user, pickle key,
|
||||
and corresponding key query and backup responses (`DumpDataSetInfo`).
|
||||
|
||||
The dump is created by pasting the following into the browser console:
|
||||
|
||||
```javascript
|
||||
async function exportIndexedDb(name) {
|
||||
const db = await new Promise((resolve, reject) => {
|
||||
const dbReq = indexedDB.open(name);
|
||||
dbReq.onerror = reject;
|
||||
dbReq.onsuccess = () => resolve(dbReq.result);
|
||||
});
|
||||
|
||||
const storeNames = db.objectStoreNames;
|
||||
const exports = {};
|
||||
for (const store of storeNames) {
|
||||
exports[store] = [];
|
||||
const txn = db.transaction(store, "readonly");
|
||||
const objectStore = txn.objectStore(store);
|
||||
await new Promise((resolve, reject) => {
|
||||
const cursorReq = objectStore.openCursor();
|
||||
cursorReq.onerror = reject;
|
||||
cursorReq.onsuccess = (event) => {
|
||||
const cursor = event.target.result;
|
||||
if (cursor) {
|
||||
const entry = { value: cursor.value };
|
||||
if (!objectStore.keyPath) {
|
||||
entry.key = cursor.key;
|
||||
}
|
||||
exports[store].push(entry);
|
||||
cursor.continue();
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
return exports;
|
||||
}
|
||||
|
||||
window.saveAs(
|
||||
new Blob([JSON.stringify(await exportIndexedDb("matrix-js-sdk:crypto"), null, 2)], {
|
||||
type: "application/json;charset=utf-8",
|
||||
}),
|
||||
"dump.json",
|
||||
);
|
||||
```
|
||||
|
||||
The pickle key is extracted via `mxMatrixClientPeg.get().crypto.olmDevice.pickleKey`.
|
||||
@@ -0,0 +1,4 @@
|
||||
## Dump of a libolm indexeddb cryptostore to test migration of a full account
|
||||
|
||||
A dump of an account containing a complete set of data to migrate.
|
||||
The data set is substantial enough to allow for testing of chunking mechanisms and progress reporting during the migration process.
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,109 @@
|
||||
import { DumpDataSetInfo } from "../index";
|
||||
|
||||
/**
|
||||
* A key query response containing the current keys of the tested user.
|
||||
* To be used during tests with fetchmock.
|
||||
*/
|
||||
const KEYS_QUERY_RESPONSE: any = {
|
||||
device_keys: {
|
||||
"@vdhtest200713:matrix.org": {
|
||||
KMFSTJSMLB: {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "KMFSTJSMLB",
|
||||
keys: {
|
||||
"curve25519:KMFSTJSMLB": "LKv0bKbc0EC4h0jknbemv3QalEkeYvuNeUXVRgVVTTU",
|
||||
"ed25519:KMFSTJSMLB": "qK70DEqIXq7T+UU3v/al47Ab4JkMEBLpNrTBMbS5rrw",
|
||||
},
|
||||
user_id: "@vdhtest200713:matrix.org",
|
||||
signatures: {
|
||||
"@vdhtest200713:matrix.org": {
|
||||
"ed25519:KMFSTJSMLB":
|
||||
"aE+PdxLAdwQ/xfJwLmqebvt/lrT97fZas2SQFFrM+dPmHxQtjyS8csm88BLfGRjJKK1B/vWev3AaKqQZwLTUAw",
|
||||
"ed25519:lDvg6vi3P80L9XFNpUSU+5Y87m3p6yHcC83jhSU4Q5k":
|
||||
"lCd4SA/JT1nnxsgN9yQaLJQhH5hkLMVVx6ba5JAjL1wpWVqyPxzMJHImX6vTztk6S8rybcdfYkea5W/Ii+4HCQ",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
master_keys: {
|
||||
"@vdhtest200713:matrix.org": {
|
||||
user_id: "@vdhtest200713:matrix.org",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
"ed25519:gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI": "gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI",
|
||||
},
|
||||
signatures: {
|
||||
"@vdhtest200713:matrix.org": {
|
||||
"ed25519:MWOGVUTXZN":
|
||||
"stOu1aHbhsWB/Aj5M/HqBR83QzME+682C995Uc8JxSmmyrlWmgG8QrnoUDG2OFR1t6zNQ+QLEilU4WNEOV73DQ",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
self_signing_keys: {
|
||||
"@vdhtest200713:matrix.org": {
|
||||
user_id: "@vdhtest200713:matrix.org",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
"ed25519:lDvg6vi3P80L9XFNpUSU+5Y87m3p6yHcC83jhSU4Q5k": "lDvg6vi3P80L9XFNpUSU+5Y87m3p6yHcC83jhSU4Q5k",
|
||||
},
|
||||
signatures: {
|
||||
"@vdhtest200713:matrix.org": {
|
||||
"ed25519:gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI":
|
||||
"HKTC7NoBhAkfJtmemmkn/HvCCgBQViWZ0uH7aGPRaWMDFgD8T7Q+y1j3FKZv4mhSopR85Fq3FRyXsG8OVvGeBA",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
user_signing_keys: {
|
||||
"@vdhtest200713:matrix.org": {
|
||||
user_id: "@vdhtest200713:matrix.org",
|
||||
usage: ["user_signing"],
|
||||
keys: {
|
||||
"ed25519:YShqO/3u5vQ0uucojraWrtoLrek0CYrurN/vH/YPMg8": "YShqO/3u5vQ0uucojraWrtoLrek0CYrurN/vH/YPMg8",
|
||||
},
|
||||
signatures: {
|
||||
"@vdhtest200713:matrix.org": {
|
||||
"ed25519:gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI":
|
||||
"u8VOi4IaeRJwDgy2ftK02NJQPdBijy8f/0+WnHGG72yfOvMthwWzEw8SrRSNG8glBNrfHinKwCyJJzAJwyepCQ",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* A `/room_keys/version` response containing the current server-side backup info.
|
||||
* To be used during tests with fetchmock.
|
||||
*/
|
||||
const BACKUP_RESPONSE: any = {
|
||||
auth_data: {
|
||||
public_key: "q+HZiJdHl2Yopv9GGvv7EYSzDMrAiRknK4glSdoaomI",
|
||||
signatures: {
|
||||
"@vdhtest200713:matrix.org": {
|
||||
"ed25519:gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI":
|
||||
"reDp6Mu+j+tfUL3/T6f5OBT3N825Lzpc43vvG+RvjX6V+KxXzodBQArgCoeEHLtL9OgSBmNrhTkSOX87MWCKAw",
|
||||
"ed25519:KMFSTJSMLB":
|
||||
"F8tyV5W6wNi0GXTdSg+gxSCULQi0EYxdAAqfkyNq58KzssZMw5i+PRA0aI2b+D7NH/aZaJrtiYNHJ0gWLSQvAw",
|
||||
},
|
||||
},
|
||||
},
|
||||
version: "7",
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
etag: "1",
|
||||
count: 79,
|
||||
};
|
||||
|
||||
/**
|
||||
* A dataset containing the information for the tested user.
|
||||
* To be used during tests.
|
||||
*/
|
||||
export const FULL_ACCOUNT_DATASET: DumpDataSetInfo = {
|
||||
userId: "@vdhtest200713:matrix.org",
|
||||
deviceId: "KMFSTJSMLB",
|
||||
pickleKey: "+1k2Ppd7HIisUY824v7JtV3/oEE4yX0TqtmNPyhaD7o",
|
||||
backupResponse: BACKUP_RESPONSE,
|
||||
keyQueryResponse: KEYS_QUERY_RESPONSE,
|
||||
dumpPath: "spec/test-utils/test_indexeddb_cryptostore_dump/full_account/dump.json",
|
||||
};
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
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 { readFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
/**
|
||||
* Populate an IndexedDB store with a set of test data.
|
||||
*
|
||||
* @param name - Name of the IndexedDB database to create.
|
||||
* @param dumpPath - The path to the dump file to import.
|
||||
*/
|
||||
export async function populateStore(name: string, dumpPath: string): Promise<IDBDatabase> {
|
||||
const req = indexedDB.open(name, 11);
|
||||
|
||||
const db = await new Promise<IDBDatabase>((resolve, reject) => {
|
||||
req.onupgradeneeded = (ev): void => {
|
||||
const db = req.result;
|
||||
const oldVersion = ev.oldVersion;
|
||||
upgradeDatabase(oldVersion, db);
|
||||
};
|
||||
|
||||
req.onerror = (ev): void => {
|
||||
reject(req.error);
|
||||
};
|
||||
|
||||
req.onsuccess = (): void => {
|
||||
const db = req.result;
|
||||
resolve(db);
|
||||
};
|
||||
});
|
||||
|
||||
await importData(db, dumpPath);
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
/** Create the schema for the indexed db store */
|
||||
function upgradeDatabase(oldVersion: number, db: IDBDatabase) {
|
||||
if (oldVersion < 1) {
|
||||
const outgoingRoomKeyRequestsStore = db.createObjectStore("outgoingRoomKeyRequests", { keyPath: "requestId" });
|
||||
outgoingRoomKeyRequestsStore.createIndex("session", ["requestBody.room_id", "requestBody.session_id"]);
|
||||
outgoingRoomKeyRequestsStore.createIndex("state", "state");
|
||||
}
|
||||
|
||||
if (oldVersion < 2) {
|
||||
db.createObjectStore("account");
|
||||
}
|
||||
|
||||
if (oldVersion < 3) {
|
||||
const sessionsStore = db.createObjectStore("sessions", { keyPath: ["deviceKey", "sessionId"] });
|
||||
sessionsStore.createIndex("deviceKey", "deviceKey");
|
||||
}
|
||||
|
||||
if (oldVersion < 4) {
|
||||
db.createObjectStore("inbound_group_sessions", { keyPath: ["senderCurve25519Key", "sessionId"] });
|
||||
}
|
||||
|
||||
if (oldVersion < 5) {
|
||||
db.createObjectStore("device_data");
|
||||
}
|
||||
|
||||
if (oldVersion < 6) {
|
||||
db.createObjectStore("rooms");
|
||||
}
|
||||
|
||||
if (oldVersion < 7) {
|
||||
db.createObjectStore("sessions_needing_backup", { keyPath: ["senderCurve25519Key", "sessionId"] });
|
||||
}
|
||||
|
||||
if (oldVersion < 8) {
|
||||
db.createObjectStore("inbound_group_sessions_withheld", { keyPath: ["senderCurve25519Key", "sessionId"] });
|
||||
}
|
||||
|
||||
if (oldVersion < 9) {
|
||||
const problemsStore = db.createObjectStore("session_problems", { keyPath: ["deviceKey", "time"] });
|
||||
problemsStore.createIndex("deviceKey", "deviceKey");
|
||||
|
||||
db.createObjectStore("notified_error_devices", { keyPath: ["userId", "deviceId"] });
|
||||
}
|
||||
|
||||
if (oldVersion < 10) {
|
||||
db.createObjectStore("shared_history_inbound_group_sessions", { keyPath: ["roomId"] });
|
||||
}
|
||||
|
||||
if (oldVersion < 11) {
|
||||
db.createObjectStore("parked_shared_history", { keyPath: ["roomId"] });
|
||||
}
|
||||
}
|
||||
|
||||
async function importData(db: IDBDatabase, dumpPath: string) {
|
||||
const path = resolve(dumpPath);
|
||||
const json: Record<string, Array<{ key?: any; value: any }>> = JSON.parse(
|
||||
await readFile(path, { encoding: "utf8" }),
|
||||
);
|
||||
|
||||
for (const [storeName, data] of Object.entries(json)) {
|
||||
await new Promise((resolve, reject) => {
|
||||
const store = db.transaction(storeName, "readwrite").objectStore(storeName);
|
||||
|
||||
function putEntry(idx: number) {
|
||||
if (idx >= data.length) {
|
||||
resolve(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const { key, value } = data[idx];
|
||||
try {
|
||||
const putReq = store.put(value, key);
|
||||
putReq.onsuccess = (_) => putEntry(idx + 1);
|
||||
putReq.onerror = (_) => reject(putReq.error);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Error populating '${storeName}' with key ${JSON.stringify(key)}, value ${JSON.stringify(
|
||||
value,
|
||||
)}: ${e}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
putEntry(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export interface DumpDataSetInfo {
|
||||
/** The user ID to use for the test.*/
|
||||
userId: string;
|
||||
/** The device ID to use for the test.*/
|
||||
deviceId: string;
|
||||
/** The path to the dump file to import via {@link populateStore}.*/
|
||||
dumpPath: string;
|
||||
/** The pickle key to use for the dumped account.*/
|
||||
pickleKey: string;
|
||||
/** The response to use for the keys query. */
|
||||
keyQueryResponse: any;
|
||||
/** The response to use for the backup query.*/
|
||||
backupResponse?: any;
|
||||
/** Additional dump info specific for some tests.*/
|
||||
[key: string]: any;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
## Dump of a libolm indexeddb cryptostore where the msk is not cached
|
||||
|
||||
A dump simulating an account where the identity was verified, but the msk was not in cache.
|
||||
Used to test that the owner identity local trust is migrated correctly.
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,283 @@
|
||||
import { KeyBackupInfo } from "../../../../src/crypto-api/keybackup";
|
||||
import { DumpDataSetInfo } from "../index";
|
||||
|
||||
/**
|
||||
* A key query response containing the current keys of the tested user.
|
||||
* To be used during tests with fetchmock.
|
||||
*/
|
||||
const KEY_QUERY_RESPONSE: any = {
|
||||
device_keys: {
|
||||
"@migration:localhost": {
|
||||
CBGTADUILV: {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "CBGTADUILV",
|
||||
keys: {
|
||||
"curve25519:CBGTADUILV": "gqhFlc7Wzc1wmmmAu3ySIEe4LtDcBK/bdzrtZg+mMSg",
|
||||
"ed25519:CBGTADUILV": "q1q3L1Il4l61c/6TmI4fYWMsseNMJJYE2Y0r+5ajKQI",
|
||||
},
|
||||
signatures: {
|
||||
"@migration:localhost": {
|
||||
"ed25519:CBGTADUILV":
|
||||
"ppSmA0slyQ7RJOFn+qZSLCGeHN6/jAmqKvUZo5Q1hWk0ugkKycRoSUi9TOfbfAVSf8xvFirXy2VGXQbEVPJqAA",
|
||||
"ed25519:d+4HhsodR2Zqv4Z5V0VxPfy8zbjLjUCdCyv5qme5Ygc":
|
||||
"cFLWl1fjehLrzrEn3UnmZMIgy3C23WMgGRsn4e6Z/55vmen4KMs8bLpgZaDoWhIdn/8siHRWafA5sFdzK2NsBQ",
|
||||
"ed25519:bmFmNcVPvaqrlNzmyKn9uU+QRHyx2QRbn/bUAlTH760":
|
||||
"C6EeqNPcaQyuZgo8+HOUywc/TMkW5IMjg7aoxyu93X//KcNNXKRfj1banYP6XqyPuQITLamBYc1089Jpt9g4Cw",
|
||||
"ed25519:RkQzi0+aKIL9Y+GzsN23xMz3i3QRkH03G5aqqEbbuy4":
|
||||
"YwBN/SbCxO8hPgv1B9JY2WVFK4LNK9vq1UNVrkF2j0ZDw9LrvaOws72mbmzZ0nbD3ohcEZ8rXsEosxEVr5r7AQ",
|
||||
},
|
||||
},
|
||||
user_id: "@migration:localhost",
|
||||
unsigned: {
|
||||
device_display_name: "localhost:8080: Chrome on macOS",
|
||||
},
|
||||
},
|
||||
TMWBMDZPFT: {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "TMWBMDZPFT",
|
||||
keys: {
|
||||
"curve25519:TMWBMDZPFT": "oYP9EXvHMbliFdfk8jPvUw0KhAd0+PBqdMslJAt/ZGQ",
|
||||
"ed25519:TMWBMDZPFT": "IyfPT67JutFWJsUxrxSqEWxgRjKn9B/w78uKU4OBj1E",
|
||||
},
|
||||
signatures: {
|
||||
"@migration:localhost": {
|
||||
"ed25519:TMWBMDZPFT":
|
||||
"IWIuuDag4ZMDhMObYV63X7dBYEUYNHYR0Yu/bwLvQh5ieDjQSrZSLOzDrgCyPCM4hkc4JlhneQpJsYo1lUH7DA",
|
||||
"ed25519:d+4HhsodR2Zqv4Z5V0VxPfy8zbjLjUCdCyv5qme5Ygc":
|
||||
"iEcTKElQu4CAsQIXmBaZmXwfB6Diut+4ZXakP1ob7OIDMrCYBcgXsBFYg6GuxwL0LCTVcUgbUw7VuPKSvM8UAA",
|
||||
"ed25519:MYgcP5P7P6KucWjLvTRofY5PWxsf+WDj2BiXtqOO5Gw":
|
||||
"KcBLDWkCwZyIzlBkC29PNzHxx7Br14TYlhBfREEEQo/Rd34ZZUYwbQ8iPhB8S1GVq3YwgAV6piYIcxpQin+dBA",
|
||||
"ed25519:HGN9m99VprMuQBDA3o+KZKcEYTaGmiaujrkygjScMnY":
|
||||
"VqrvA148Uxib9TNFI1rc9r8qpwTojCkqLofEz9dMLc/XV3U14WD5/LDEhMuCwNu6wsu/uO+dS4AmJlJnN/iAAg",
|
||||
"ed25519:Nt0L/p+UVHMx603sYHXwXja+VyQIUVFvu0vDBYn56Zk":
|
||||
"D1COHzROOTNlCn8b1zI9+6phUtF0OVqWxLfOLnX5t14H2oENYV2ASgaxsdmXcSZPrGzaJkmSOginHHzsabe5CA",
|
||||
"ed25519:bmFmNcVPvaqrlNzmyKn9uU+QRHyx2QRbn/bUAlTH760":
|
||||
"SFSDrsi3GQ9jjBYUc2aUSzf777/0NfQWrOBi2CK+v5VQY3FkyHBln3K4YzvxIKSVIhOaQtBlEDtfQb33kwTgDg",
|
||||
"ed25519:RkQzi0+aKIL9Y+GzsN23xMz3i3QRkH03G5aqqEbbuy4":
|
||||
"BtJkzQe0YFAa8gJiYXYtzGtktl9vZMNYl5jd4DA8Toi4VxgosJNZQE7lT5qpYU0BrlFn46QIs/38X8JhSt+wAQ",
|
||||
},
|
||||
},
|
||||
user_id: "@migration:localhost",
|
||||
unsigned: {
|
||||
device_display_name: "localhost:8080: Chrome on macOS",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
failures: {},
|
||||
master_keys: {
|
||||
"@migration:localhost": {
|
||||
keys: {
|
||||
"ed25519:cFjUBAhAZ2tjYF1TpQtYNA3x9XRzTiIdP2N2EvRaOH4": "cFjUBAhAZ2tjYF1TpQtYNA3x9XRzTiIdP2N2EvRaOH4",
|
||||
},
|
||||
signatures: {
|
||||
"@migration:localhost": {
|
||||
"ed25519:TMWBMDZPFT":
|
||||
"RrPUnYoekK7wZGrLNXshgoupF8v53S/vJyvkBJi+q9THh4Qrf3CieuVJFx8mwtmEZgGoA2tSroAVnRqvEQ+IBQ",
|
||||
"ed25519:cFjUBAhAZ2tjYF1TpQtYNA3x9XRzTiIdP2N2EvRaOH4":
|
||||
"o4CbtdU3IqJK90UXAEBtxps2m4XBYvWJI2nbVlzBaGRr+Xt/3vtwDMlc5G970kPQWBbs/koYJh8MSaE7Fm1mAg",
|
||||
"ed25519:CBGTADUILV":
|
||||
"AgZoG+ix8aW3FAW6v+/Xu+QJpxzvsx5itbB8RyqMet9YlNqX90vYIbBV7IoV2WFY2WdANYEffX2CE0FpR6NnCg",
|
||||
},
|
||||
},
|
||||
usage: ["master"],
|
||||
user_id: "@migration:localhost",
|
||||
},
|
||||
},
|
||||
self_signing_keys: {
|
||||
"@migration:localhost": {
|
||||
keys: {
|
||||
"ed25519:RkQzi0+aKIL9Y+GzsN23xMz3i3QRkH03G5aqqEbbuy4": "RkQzi0+aKIL9Y+GzsN23xMz3i3QRkH03G5aqqEbbuy4",
|
||||
},
|
||||
signatures: {
|
||||
"@migration:localhost": {
|
||||
"ed25519:cFjUBAhAZ2tjYF1TpQtYNA3x9XRzTiIdP2N2EvRaOH4":
|
||||
"hs8VqoTfipDjC2pzFdmzb1aENhDjVV+gc86fuYftczaCcsXUWop/NPwoF51Ie6Nb3YL0N7ZZAUrycuJP5hFbDg",
|
||||
},
|
||||
},
|
||||
usage: ["self_signing"],
|
||||
user_id: "@migration:localhost",
|
||||
},
|
||||
},
|
||||
user_signing_keys: {
|
||||
"@migration:localhost": {
|
||||
keys: {
|
||||
"ed25519:WNJ2G3Ig5EdC4wYiRKcK7bhLP2+I4wI6V7SKgJTXdw8": "WNJ2G3Ig5EdC4wYiRKcK7bhLP2+I4wI6V7SKgJTXdw8",
|
||||
},
|
||||
signatures: {
|
||||
"@migration:localhost": {
|
||||
"ed25519:cFjUBAhAZ2tjYF1TpQtYNA3x9XRzTiIdP2N2EvRaOH4":
|
||||
"Vlba5rJQxG+ussVLoycvHcin7Ghv0uUeClDqDbM+RPF+jx9w4ozbcuEOTJdyzyPA+GxN9Kzh2lmVFMMQGyvNAw",
|
||||
},
|
||||
},
|
||||
usage: ["user_signing"],
|
||||
user_id: "@migration:localhost",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* A new key query response for the same user simulating a cross-signing key reset.
|
||||
* To be used during tests with fetchmock.
|
||||
*/
|
||||
const ROTATED_KEY_QUERY_RESPONSE: any = {
|
||||
device_keys: {
|
||||
"@migration:localhost": {
|
||||
TMWBMDZPFT: {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "TMWBMDZPFT",
|
||||
keys: {
|
||||
"curve25519:TMWBMDZPFT": "oYP9EXvHMbliFdfk8jPvUw0KhAd0+PBqdMslJAt/ZGQ",
|
||||
"ed25519:TMWBMDZPFT": "IyfPT67JutFWJsUxrxSqEWxgRjKn9B/w78uKU4OBj1E",
|
||||
},
|
||||
signatures: {
|
||||
"@migration:localhost": {
|
||||
"ed25519:TMWBMDZPFT":
|
||||
"IWIuuDag4ZMDhMObYV63X7dBYEUYNHYR0Yu/bwLvQh5ieDjQSrZSLOzDrgCyPCM4hkc4JlhneQpJsYo1lUH7DA",
|
||||
"ed25519:d+4HhsodR2Zqv4Z5V0VxPfy8zbjLjUCdCyv5qme5Ygc":
|
||||
"iEcTKElQu4CAsQIXmBaZmXwfB6Diut+4ZXakP1ob7OIDMrCYBcgXsBFYg6GuxwL0LCTVcUgbUw7VuPKSvM8UAA",
|
||||
"ed25519:MYgcP5P7P6KucWjLvTRofY5PWxsf+WDj2BiXtqOO5Gw":
|
||||
"KcBLDWkCwZyIzlBkC29PNzHxx7Br14TYlhBfREEEQo/Rd34ZZUYwbQ8iPhB8S1GVq3YwgAV6piYIcxpQin+dBA",
|
||||
"ed25519:HGN9m99VprMuQBDA3o+KZKcEYTaGmiaujrkygjScMnY":
|
||||
"VqrvA148Uxib9TNFI1rc9r8qpwTojCkqLofEz9dMLc/XV3U14WD5/LDEhMuCwNu6wsu/uO+dS4AmJlJnN/iAAg",
|
||||
"ed25519:Nt0L/p+UVHMx603sYHXwXja+VyQIUVFvu0vDBYn56Zk":
|
||||
"D1COHzROOTNlCn8b1zI9+6phUtF0OVqWxLfOLnX5t14H2oENYV2ASgaxsdmXcSZPrGzaJkmSOginHHzsabe5CA",
|
||||
"ed25519:bmFmNcVPvaqrlNzmyKn9uU+QRHyx2QRbn/bUAlTH760":
|
||||
"SFSDrsi3GQ9jjBYUc2aUSzf777/0NfQWrOBi2CK+v5VQY3FkyHBln3K4YzvxIKSVIhOaQtBlEDtfQb33kwTgDg",
|
||||
"ed25519:RkQzi0+aKIL9Y+GzsN23xMz3i3QRkH03G5aqqEbbuy4":
|
||||
"BtJkzQe0YFAa8gJiYXYtzGtktl9vZMNYl5jd4DA8Toi4VxgosJNZQE7lT5qpYU0BrlFn46QIs/38X8JhSt+wAQ",
|
||||
},
|
||||
},
|
||||
user_id: "@migration:localhost",
|
||||
unsigned: {
|
||||
device_display_name: "localhost:8080: Chrome on macOS",
|
||||
},
|
||||
},
|
||||
XFZFSCUOFL: {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "XFZFSCUOFL",
|
||||
keys: {
|
||||
"curve25519:XFZFSCUOFL": "aN2Ty+0rutNkrRtxhV+ciI8GhF4epSxzL7bAOr8zfkc",
|
||||
"ed25519:XFZFSCUOFL": "V7CPhXdfLFk+qAOFivrpFskmunVTeuM+EOM3DMlDxkI",
|
||||
},
|
||||
signatures: {
|
||||
"@migration:localhost": {
|
||||
"ed25519:XFZFSCUOFL":
|
||||
"4Pqc2FWJ5p/L/tSlfUBIlcQzLmN5CksJriAibY8LSDAXdGYiQJ7hvKqneEuVhrMYwqyIxb4bAad+r6wnY0/7Cg",
|
||||
"ed25519:RkQzi0+aKIL9Y+GzsN23xMz3i3QRkH03G5aqqEbbuy4":
|
||||
"yH8pKnD+E8YaawS+1NCjwy0cf2WzBRff9BBNX4YnAuTyc6s5b1QqNfu9DP5qblw8TZ7hZmaziePZKsjRiqJLBg",
|
||||
"ed25519:OEv0wHLusJx7zTCc0h3HbNIHLIxlGZKh63tc2ptKb+Y":
|
||||
"M8SfAiEUzd7AsWp8InS7BxV3cRqV3MjMxks4DwSxsVxvkCco2JWybKgev+vTZyM6XDg930o0FObQOxWm4+CkBw",
|
||||
},
|
||||
},
|
||||
user_id: "@migration:localhost",
|
||||
unsigned: {
|
||||
device_display_name: "localhost:8080: Chrome on macOS",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
failures: {},
|
||||
master_keys: {
|
||||
"@migration:localhost": {
|
||||
user_id: "@migration:localhost",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
"ed25519:rXCrBin/+xyh+yW//vWte+2UV0et1ZHTWfalp/Ekack": "rXCrBin/+xyh+yW//vWte+2UV0et1ZHTWfalp/Ekack",
|
||||
},
|
||||
signatures: {
|
||||
"@migration:localhost": {
|
||||
"ed25519:XFZFSCUOFL":
|
||||
"C8aswtyUABWvj2DInehVoh2P/EDbwRhlIk51LtV3L71POUCh7pZuyXRMMWKZeyRvHRmEllXBtRkH1iol/p56Bg",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
self_signing_keys: {
|
||||
"@migration:localhost": {
|
||||
user_id: "@migration:localhost",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
"ed25519:OEv0wHLusJx7zTCc0h3HbNIHLIxlGZKh63tc2ptKb+Y": "OEv0wHLusJx7zTCc0h3HbNIHLIxlGZKh63tc2ptKb+Y",
|
||||
},
|
||||
signatures: {
|
||||
"@migration:localhost": {
|
||||
"ed25519:rXCrBin/+xyh+yW//vWte+2UV0et1ZHTWfalp/Ekack":
|
||||
"dH596pGp8+f8dlwd81UrKDWoRDd24yAqqMSLqR4fJHyfszbn7qCvQA6LYZ023TLmk33FKcJqRtd2v/ykTmS3Bg",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
user_signing_keys: {
|
||||
"@migration:localhost": {
|
||||
user_id: "@migration:localhost",
|
||||
usage: ["user_signing"],
|
||||
keys: {
|
||||
"ed25519:8XHpC3MeMReIfYneWIRX8c4ANgJuQ1+oFrktBcLka4o": "8XHpC3MeMReIfYneWIRX8c4ANgJuQ1+oFrktBcLka4o",
|
||||
},
|
||||
signatures: {
|
||||
"@migration:localhost": {
|
||||
"ed25519:rXCrBin/+xyh+yW//vWte+2UV0et1ZHTWfalp/Ekack":
|
||||
"FX6ylagvx3IG1zMf/ayYgDb/1+x0/F28pHQqzQMGGssAmc15nat/R6AF0QO7Qg7uqTAf04ohuZtWax3dTwjNDQ",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* A `/room_keys/version` response containing the current server-side backup info.
|
||||
* To be used during tests with fetchmock.
|
||||
*/
|
||||
const BACKUP_RESPONSE: KeyBackupInfo = {
|
||||
auth_data: {
|
||||
public_key: "2ffIfIB4oryqZpsJQjQNUaxgCzxliC6A4PJvnrN+XAA",
|
||||
signatures: {
|
||||
"@migration:localhost": {
|
||||
"ed25519:TMWBMDZPFT":
|
||||
"qBvalid/G4hnSF3hAeX4TtRN6/BqprgiYnLEtDuatyQ5WxWr0s4uSOyvHSglsRdpoo32FDBHfTIZkCOVxSLwAA",
|
||||
},
|
||||
},
|
||||
},
|
||||
version: "2",
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
etag: "0",
|
||||
count: 0,
|
||||
};
|
||||
|
||||
/**
|
||||
* This was generated by doing a backup reset on the account.
|
||||
* This is a new valid backup for this account.
|
||||
*/
|
||||
const NEW_BACKUP_RESPONSE: KeyBackupInfo = {
|
||||
auth_data: {
|
||||
public_key: "CkDxWALi3lcChgjEZFEM6clYq5x768XBwsL++eaOzTI",
|
||||
signatures: {
|
||||
"@migration:localhost": {
|
||||
"ed25519:YVEGEYPYWX":
|
||||
"ZSYuQDdwgB9WKXQ+z5aWWfqSolBCGRw53kur1Vy956gFefgzCBkMbw5M0I2UgfU2Cukri7jZ4ig201zmLNmaAA",
|
||||
"ed25519:rXCrBin/+xyh+yW//vWte+2UV0et1ZHTWfalp/Ekack":
|
||||
"+UQ8EA507LoIqgK9rPsqPoGrj+iRBJeY2Oz0mMtXmVf8c1y8G0KWJNUWqvOysnOhsoJf1bt8ey48CxjjtSQ2AA",
|
||||
},
|
||||
},
|
||||
},
|
||||
version: "3",
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
etag: "0",
|
||||
count: 0,
|
||||
};
|
||||
|
||||
/**
|
||||
* A dataset containing the information for the tested user.
|
||||
* To be used during tests.
|
||||
*/
|
||||
export const MSK_NOT_CACHED_DATASET: DumpDataSetInfo = {
|
||||
userId: "@migration:localhost",
|
||||
deviceId: "CBGTADUILV",
|
||||
pickleKey: "qEURMepfkMvoBQGaWlI9MZKYnDMsSAiW8aFTKXaeDV0",
|
||||
keyQueryResponse: KEY_QUERY_RESPONSE,
|
||||
rotatedKeyQueryResponse: ROTATED_KEY_QUERY_RESPONSE,
|
||||
backupResponse: BACKUP_RESPONSE,
|
||||
newBackupResponse: NEW_BACKUP_RESPONSE,
|
||||
dumpPath: "spec/test-utils/test_indexeddb_cryptostore_dump/no_cached_msk_dump/dump.json",
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
## Dump of a libolm indexeddb cryptostore where the identity is not trusted.
|
||||
|
||||
A dump of an account where the identity was not verified.
|
||||
Used as a test case for migration of the identity local trust.
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,110 @@
|
||||
import { DumpDataSetInfo } from "../index";
|
||||
|
||||
/**
|
||||
* A key query response containing the current keys of the tested user.
|
||||
* To be used during tests with fetchmock.
|
||||
*/
|
||||
const KEY_QUERY_RESPONSE = {
|
||||
device_keys: {
|
||||
"@untrusted:localhost": {
|
||||
IXNYALOZWU: {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "IXNYALOZWU",
|
||||
keys: {
|
||||
"curve25519:IXNYALOZWU": "EHMQEtJd9INJg28HwKK8Te1EX8obR3VTtyNwf/rcczM",
|
||||
"ed25519:IXNYALOZWU": "OxMfZHsYJvroTp1RtjUOejpWbRBryN6VsojC5dKR74U",
|
||||
},
|
||||
signatures: {
|
||||
"@untrusted:localhost": {
|
||||
"ed25519:IXNYALOZWU":
|
||||
"tWaTiRKc95ZCqM2qrKTdq1sQ3DPFgw3vdrOVmWIHQwj92DCgJtnQ9uymLMOq+MSb80bdBBjXwrNeOufgaL/6CQ",
|
||||
"ed25519:+ik0n/QnBPq8H/48wAT+54slKk1SL7NIk/HtiN/cNEg":
|
||||
"+QXZFLiAv+k7UXgAP6AXLk/PdZ3TlJ77M23m73v8qvavAlnkLBAjKNA3BG39JTQET5UhW5DnCohwsbGP+aY1Cw",
|
||||
},
|
||||
},
|
||||
user_id: "@untrusted:localhost",
|
||||
unsigned: {
|
||||
device_display_name: "localhost:8080: Chrome on macOS",
|
||||
},
|
||||
},
|
||||
VJPSPVPWZT: {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "VJPSPVPWZT",
|
||||
keys: {
|
||||
"curve25519:VJPSPVPWZT": "+RxCNRFPqBZJm6PLjEJsSdFixGWQJygD5Os11/+6PC0",
|
||||
"ed25519:VJPSPVPWZT": "wqH7xK/DQya8m05Vy4rnacjugGNBiY+7Ml6wyRVkM9U",
|
||||
},
|
||||
signatures: {
|
||||
"@untrusted:localhost": {
|
||||
"ed25519:VJPSPVPWZT":
|
||||
"XC+RoKL/zVZOIwk/bGEQJlJu49QicY1v6vSDMHA2y0/fpX/MD4KiWGD7+W5DFD54E8FrFVTsIgkzat561qdTBQ",
|
||||
},
|
||||
},
|
||||
user_id: "@untrusted:localhost",
|
||||
unsigned: {
|
||||
device_display_name: "localhost:8080: Chrome on macOS",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
failures: {},
|
||||
master_keys: {
|
||||
"@untrusted:localhost": {
|
||||
keys: {
|
||||
"ed25519:Uahbc3+Rk65y0ku6T2RL/29fEA9Bum6+OaqptG6df3g": "Uahbc3+Rk65y0ku6T2RL/29fEA9Bum6+OaqptG6df3g",
|
||||
},
|
||||
signatures: {
|
||||
"@untrusted:localhost": {
|
||||
"ed25519:IXNYALOZWU":
|
||||
"KdAdyKO2sb3Di3bdK+oxf+gjMSmW/sisRNvpKZORPKwmy2SGaKGYkecBtslunoFjnb+hjIESgweQu6cHoNX4AA",
|
||||
"ed25519:Uahbc3+Rk65y0ku6T2RL/29fEA9Bum6+OaqptG6df3g":
|
||||
"b0R9Id5HxHYo+MA22Vlq0OckTrWnSWhgHLvF8Wr4e154JdtOyK7N0aXPQPkrLB0fmyVmGdbDa9xs9jsfINGmDw",
|
||||
},
|
||||
},
|
||||
usage: ["master"],
|
||||
user_id: "@untrusted:localhost",
|
||||
},
|
||||
},
|
||||
self_signing_keys: {
|
||||
"@untrusted:localhost": {
|
||||
keys: {
|
||||
"ed25519:+ik0n/QnBPq8H/48wAT+54slKk1SL7NIk/HtiN/cNEg": "+ik0n/QnBPq8H/48wAT+54slKk1SL7NIk/HtiN/cNEg",
|
||||
},
|
||||
signatures: {
|
||||
"@untrusted:localhost": {
|
||||
"ed25519:Uahbc3+Rk65y0ku6T2RL/29fEA9Bum6+OaqptG6df3g":
|
||||
"z/5z51jbRpyDQhYnfUHhhb5fUbzRDlfjD8mZA2ZGStpE/F41lDyxjlvF2W/E2CJ27bmJFdk7nC+ZCwriYfYxBw",
|
||||
},
|
||||
},
|
||||
usage: ["self_signing"],
|
||||
user_id: "@untrusted:localhost",
|
||||
},
|
||||
},
|
||||
user_signing_keys: {
|
||||
"@untrusted:localhost": {
|
||||
keys: {
|
||||
"ed25519:L/8HbQWnK9OidAcDVB+Az9b0Mx3OdBtIMFsUjV6qgSQ": "L/8HbQWnK9OidAcDVB+Az9b0Mx3OdBtIMFsUjV6qgSQ",
|
||||
},
|
||||
signatures: {
|
||||
"@untrusted:localhost": {
|
||||
"ed25519:Uahbc3+Rk65y0ku6T2RL/29fEA9Bum6+OaqptG6df3g":
|
||||
"UuNvzebLQn31LYGbx+ADe60BB25kWy4SVVyd9BXlY/tAZMoA8Tmq1e2R2tJJtPdJxC/Oogktj2+iikZV/YMjAQ",
|
||||
},
|
||||
},
|
||||
usage: ["user_signing"],
|
||||
user_id: "@untrusted:localhost",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* A dataset containing the information for the tested user.
|
||||
* To be used during tests.
|
||||
*/
|
||||
export const IDENTITY_NOT_TRUSTED_DATASET: DumpDataSetInfo = {
|
||||
userId: "@untrusted:localhost",
|
||||
deviceId: "VJPSPVPWZT",
|
||||
pickleKey: "WVllQb4Lk/WwP4Q7iBfeTUHpgydZm9YqXI1B5bTvnIM",
|
||||
keyQueryResponse: KEY_QUERY_RESPONSE,
|
||||
dumpPath: "spec/test-utils/test_indexeddb_cryptostore_dump/unverified/dump.json",
|
||||
};
|
||||
@@ -161,3 +161,23 @@ export const mkThread = ({
|
||||
|
||||
return { thread, rootEvent, events };
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a thread, and make sure the events are added to the thread and the
|
||||
* room's timeline as if they came in via sync.
|
||||
*
|
||||
* Note that mkThread doesn't actually add the events properly to the room.
|
||||
*/
|
||||
export const populateThread = ({
|
||||
room,
|
||||
client,
|
||||
authorId,
|
||||
participantUserIds,
|
||||
length = 2,
|
||||
ts = 1,
|
||||
}: MakeThreadProps): MakeThreadResult => {
|
||||
const ret = mkThread({ room, client, authorId, participantUserIds, length, ts });
|
||||
ret.thread.initialEventsFetched = true;
|
||||
room.addLiveEvents(ret.events);
|
||||
return ret;
|
||||
};
|
||||
|
||||
@@ -269,7 +269,11 @@ export class MockRTCRtpTransceiver {
|
||||
}
|
||||
|
||||
export class MockMediaStreamTrack {
|
||||
constructor(public readonly id: string, public readonly kind: "audio" | "video", public enabled = true) {}
|
||||
constructor(
|
||||
public readonly id: string,
|
||||
public readonly kind: "audio" | "video",
|
||||
public enabled = true,
|
||||
) {}
|
||||
|
||||
public stop = jest.fn<void, []>();
|
||||
|
||||
@@ -306,7 +310,10 @@ export class MockMediaStreamTrack {
|
||||
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
|
||||
// implementation
|
||||
export class MockMediaStream {
|
||||
constructor(public id: string, private tracks: MockMediaStreamTrack[] = []) {}
|
||||
constructor(
|
||||
public id: string,
|
||||
private tracks: MockMediaStreamTrack[] = [],
|
||||
) {}
|
||||
|
||||
public listeners: [string, (...args: any[]) => any][] = [];
|
||||
public isStopped = false;
|
||||
@@ -435,7 +442,11 @@ type EmittedEventMap = CallEventHandlerEventHandlerMap &
|
||||
export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, EmittedEventMap> {
|
||||
public mediaHandler = new MockMediaHandler();
|
||||
|
||||
constructor(public userId: string, public deviceId: string, public sessionId: string) {
|
||||
constructor(
|
||||
public userId: string,
|
||||
public deviceId: string,
|
||||
public sessionId: string,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -502,7 +513,10 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
|
||||
}
|
||||
|
||||
export class MockMatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap> {
|
||||
constructor(public roomId: string, public groupCallId?: string) {
|
||||
constructor(
|
||||
public roomId: string,
|
||||
public groupCallId?: string,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -550,7 +564,11 @@ export class MockMatrixCall extends TypedEventEmitter<CallEvent, CallEventHandle
|
||||
}
|
||||
|
||||
export class MockCallFeed {
|
||||
constructor(public userId: string, public deviceId: string | undefined, public stream: MockMediaStream) {}
|
||||
constructor(
|
||||
public userId: string,
|
||||
public deviceId: string | undefined,
|
||||
public stream: MockMediaStream,
|
||||
) {}
|
||||
|
||||
public measureVolumeActivity(val: boolean) {}
|
||||
public dispose() {}
|
||||
|
||||
+11
-108
@@ -15,13 +15,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import { AutoDiscoveryAction, M_AUTHENTICATION } from "../../src";
|
||||
import { AutoDiscoveryAction } from "../../src";
|
||||
import { AutoDiscovery } from "../../src/autodiscovery";
|
||||
import { OidcError } from "../../src/oidc/error";
|
||||
import { makeDelegatedAuthConfig } from "../test-utils/oidc";
|
||||
|
||||
// keep to reset the fetch function after using MockHttpBackend
|
||||
// @ts-ignore private property
|
||||
@@ -351,7 +348,7 @@ describe("AutoDiscovery", function () {
|
||||
function () {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
||||
not_matrix_versions: ["v1.1"],
|
||||
not_matrix_versions: ["v1.5"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
@@ -388,7 +385,7 @@ describe("AutoDiscovery", function () {
|
||||
expect(req.path).toEqual("https://example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["v1.1"],
|
||||
versions: ["v1.5"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
@@ -409,10 +406,6 @@ describe("AutoDiscovery", function () {
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
"m.authentication": {
|
||||
state: "IGNORE",
|
||||
error: OidcError.NotSupported,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
@@ -428,7 +421,7 @@ describe("AutoDiscovery", function () {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["v1.1"],
|
||||
versions: ["v1.5"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
@@ -450,10 +443,6 @@ describe("AutoDiscovery", function () {
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
"m.authentication": {
|
||||
state: "IGNORE",
|
||||
error: OidcError.NotSupported,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
@@ -469,16 +458,13 @@ describe("AutoDiscovery", function () {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["v1.1"],
|
||||
versions: ["v1.5"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
"m.authentication": {
|
||||
invalid: true,
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
@@ -494,10 +480,6 @@ describe("AutoDiscovery", function () {
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
"m.authentication": {
|
||||
state: "FAIL_ERROR",
|
||||
error: OidcError.Misconfigured,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
@@ -515,7 +497,7 @@ describe("AutoDiscovery", function () {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["v1.1"],
|
||||
versions: ["v1.5"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
@@ -560,7 +542,7 @@ describe("AutoDiscovery", function () {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["v1.1"],
|
||||
versions: ["v1.5"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
@@ -606,7 +588,7 @@ describe("AutoDiscovery", function () {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["v1.1"],
|
||||
versions: ["v1.5"],
|
||||
});
|
||||
httpBackend.when("GET", "/_matrix/identity/v2").respond(404, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
@@ -653,7 +635,7 @@ describe("AutoDiscovery", function () {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["v1.1"],
|
||||
versions: ["v1.5"],
|
||||
});
|
||||
httpBackend.when("GET", "/_matrix/identity/v2").respond(500, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
@@ -697,7 +679,7 @@ describe("AutoDiscovery", function () {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["v1.1"],
|
||||
versions: ["v1.5"],
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/_matrix/identity/v2")
|
||||
@@ -728,10 +710,6 @@ describe("AutoDiscovery", function () {
|
||||
error: null,
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
"m.authentication": {
|
||||
state: "IGNORE",
|
||||
error: OidcError.NotSupported,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
@@ -747,7 +725,7 @@ describe("AutoDiscovery", function () {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["v1.1"],
|
||||
versions: ["v1.5"],
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/_matrix/identity/v2")
|
||||
@@ -784,10 +762,6 @@ describe("AutoDiscovery", function () {
|
||||
"org.example.custom.property": {
|
||||
cupcakes: "yes",
|
||||
},
|
||||
"m.authentication": {
|
||||
state: "IGNORE",
|
||||
error: OidcError.NotSupported,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
@@ -897,75 +871,4 @@ describe("AutoDiscovery", function () {
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
describe("m.authentication", () => {
|
||||
const homeserverName = "example.org";
|
||||
const homeserverUrl = "https://chat.example.org/";
|
||||
const issuer = "https://auth.org/";
|
||||
|
||||
beforeAll(() => {
|
||||
// make these tests independent from fetch mocking above
|
||||
AutoDiscovery.setFetchFn(realAutoDiscoveryFetch);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.resetBehavior();
|
||||
fetchMock.get(`${homeserverUrl}_matrix/client/versions`, { versions: ["v1.1"] });
|
||||
|
||||
fetchMock.get("https://example.org/.well-known/matrix/client", {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
"m.authentication": {
|
||||
issuer,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should return valid authentication configuration", async () => {
|
||||
const config = makeDelegatedAuthConfig(issuer);
|
||||
|
||||
fetchMock.get(`${config.metadata.issuer}.well-known/openid-configuration`, config.metadata);
|
||||
fetchMock.get(`${config.metadata.issuer}jwks`, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
keys: [],
|
||||
});
|
||||
|
||||
const result = await AutoDiscovery.findClientConfig(homeserverName);
|
||||
|
||||
expect(result[M_AUTHENTICATION.stable!]).toEqual({
|
||||
state: AutoDiscovery.SUCCESS,
|
||||
...config,
|
||||
signingKeys: [],
|
||||
account: undefined,
|
||||
error: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("should set state to error for invalid authentication configuration", async () => {
|
||||
const config = makeDelegatedAuthConfig(issuer);
|
||||
// authorization_code is required
|
||||
config.metadata.grant_types_supported = ["openid"];
|
||||
|
||||
fetchMock.get(`${config.metadata.issuer}.well-known/openid-configuration`, config.metadata);
|
||||
fetchMock.get(`${config.metadata.issuer}jwks`, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
keys: [],
|
||||
});
|
||||
|
||||
const result = await AutoDiscovery.findClientConfig(homeserverName);
|
||||
|
||||
expect(result[M_AUTHENTICATION.stable!]).toEqual({
|
||||
state: AutoDiscovery.FAIL_ERROR,
|
||||
error: OidcError.OpSupport,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,6 +37,21 @@ describe("ContentRepo", function () {
|
||||
);
|
||||
});
|
||||
|
||||
it("should allow redirects when requested on download URLs", function () {
|
||||
const mxcUri = "mxc://server.name/resourceid";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, undefined, undefined, undefined, false, true)).toEqual(
|
||||
baseUrl + "/_matrix/media/v3/download/server.name/resourceid?allow_redirect=true",
|
||||
);
|
||||
});
|
||||
|
||||
it("should allow redirects when requested on thumbnail URLs", function () {
|
||||
const mxcUri = "mxc://server.name/resourceid";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, 32, 32, "scale", false, true)).toEqual(
|
||||
baseUrl +
|
||||
"/_matrix/media/v3/thumbnail/server.name/resourceid?width=32&height=32&method=scale&allow_redirect=true",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return the empty string for null input", function () {
|
||||
expect(getHttpUriForMxc(null as any, "")).toEqual("");
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@ import { UserTrustLevel } from "../../src/crypto/CrossSigning";
|
||||
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
|
||||
import { EventDecryptionResult } from "../../src/common-crypto/CryptoBackend";
|
||||
import * as testData from "../test-utils/test-data";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -356,7 +357,6 @@ describe("Crypto", function () {
|
||||
|
||||
let crypto: Crypto;
|
||||
let mockBaseApis: MatrixClient;
|
||||
let mockRoomList: RoomList;
|
||||
|
||||
let fakeEmitter: EventEmitter;
|
||||
|
||||
@@ -390,19 +390,10 @@ describe("Crypto", function () {
|
||||
isGuest: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
} as unknown as MatrixClient;
|
||||
mockRoomList = {} as unknown as RoomList;
|
||||
|
||||
fakeEmitter = new EventEmitter();
|
||||
|
||||
crypto = new Crypto(
|
||||
mockBaseApis,
|
||||
"@alice:home.server",
|
||||
"FLIBBLE",
|
||||
clientStore,
|
||||
cryptoStore,
|
||||
mockRoomList,
|
||||
[],
|
||||
);
|
||||
crypto = new Crypto(mockBaseApis, "@alice:home.server", "FLIBBLE", clientStore, cryptoStore, []);
|
||||
crypto.registerEventHandlers(fakeEmitter as any);
|
||||
await crypto.init();
|
||||
});
|
||||
@@ -473,7 +464,7 @@ describe("Crypto", function () {
|
||||
type: "m.room.member",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
content: { membership: "invite" },
|
||||
content: { membership: KnownMembership.Invite },
|
||||
state_key: "@bob:example.com",
|
||||
}),
|
||||
]);
|
||||
@@ -805,7 +796,7 @@ describe("Crypto", function () {
|
||||
type: "m.room.member",
|
||||
sender: "@clara:example.com",
|
||||
room_id: roomId,
|
||||
content: { membership: "invite" },
|
||||
content: { membership: KnownMembership.Invite },
|
||||
state_key: "@bob:example.com",
|
||||
}),
|
||||
]);
|
||||
@@ -1273,7 +1264,7 @@ describe("Crypto", function () {
|
||||
({
|
||||
init_with_private_key: jest.fn(),
|
||||
free,
|
||||
} as unknown as PkDecryption),
|
||||
}) as unknown as PkDecryption,
|
||||
);
|
||||
client.client.checkSecretStoragePrivateKey(new Uint8Array(), "");
|
||||
expect(free).toHaveBeenCalled();
|
||||
@@ -1299,7 +1290,7 @@ describe("Crypto", function () {
|
||||
({
|
||||
init_with_seed: jest.fn(),
|
||||
free,
|
||||
} as unknown as PkSigning),
|
||||
}) as unknown as PkSigning,
|
||||
);
|
||||
client.client.checkCrossSigningPrivateKey(new Uint8Array(), "");
|
||||
expect(free).toHaveBeenCalled();
|
||||
@@ -1341,15 +1332,9 @@ describe("Crypto", function () {
|
||||
setRoomEncryption: jest.fn().mockResolvedValue(undefined),
|
||||
} as unknown as RoomList;
|
||||
|
||||
crypto = new Crypto(
|
||||
mockClient,
|
||||
"@alice:home.server",
|
||||
"FLIBBLE",
|
||||
clientStore,
|
||||
cryptoStore,
|
||||
mockRoomList,
|
||||
[],
|
||||
);
|
||||
crypto = new Crypto(mockClient, "@alice:home.server", "FLIBBLE", clientStore, cryptoStore, []);
|
||||
// @ts-ignore we are injecting a mock into a private property
|
||||
crypto.roomList = mockRoomList;
|
||||
});
|
||||
|
||||
it("should set the algorithm if called for a known room", async () => {
|
||||
|
||||
@@ -36,6 +36,7 @@ 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";
|
||||
import { KnownMembership } from "../../../../src/@types/membership";
|
||||
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!;
|
||||
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!;
|
||||
@@ -806,11 +807,11 @@ describe("MegolmDecryption", function () {
|
||||
aliceRoom.getEncryptionTargetMembers = jest.fn().mockResolvedValue([
|
||||
{
|
||||
userId: "@alice:example.com",
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
},
|
||||
{
|
||||
userId: "@bob:example.com",
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
},
|
||||
]);
|
||||
const BOB_DEVICES = {
|
||||
|
||||
@@ -106,7 +106,7 @@ describe("Cross Signing", function () {
|
||||
});
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
alice.setAccountData = async () => ({});
|
||||
alice.getAccountDataFromServer = async <T>() => ({} as T);
|
||||
alice.getAccountDataFromServer = async <T>() => ({}) as T;
|
||||
// set Alice's cross-signing key
|
||||
await alice.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async (func) => {
|
||||
@@ -146,7 +146,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);
|
||||
alice.getAccountDataFromServer = async <T extends { [k: string]: any }>(): Promise<T | null> => ({}) as T;
|
||||
const authUploadDeviceSigningKeys: BootstrapCrossSigningOpts["authUploadDeviceSigningKeys"] = async (func) => {
|
||||
await func({});
|
||||
};
|
||||
|
||||
@@ -33,12 +33,10 @@ export async function resetCrossSigningKeys(
|
||||
|
||||
export async function createSecretStorageKey(): Promise<IRecoveryKey> {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
const storagePublicKey = decryption.generate_key();
|
||||
decryption.generate_key();
|
||||
const storagePrivateKey = decryption.get_private_key();
|
||||
decryption.free();
|
||||
return {
|
||||
// `pubkey` not used anymore with symmetric 4S
|
||||
keyInfo: { pubkey: storagePublicKey, key: undefined! },
|
||||
privateKey: storagePrivateKey,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -190,10 +190,7 @@ describe("Secrets", function () {
|
||||
};
|
||||
resetCrossSigningKeys(alice);
|
||||
|
||||
const { keyId: newKeyId } = await alice.addSecretStorageKey(SECRET_STORAGE_ALGORITHM_V1_AES, {
|
||||
pubkey: undefined,
|
||||
key: undefined,
|
||||
});
|
||||
const { keyId: newKeyId } = await alice.addSecretStorageKey(SECRET_STORAGE_ALGORITHM_V1_AES, { key });
|
||||
// we don't await on this because it waits for the event to come down the sync
|
||||
// which won't happen in the test setup
|
||||
alice.setDefaultSecretStorageKeyId(newKeyId);
|
||||
@@ -335,7 +332,6 @@ describe("Secrets", function () {
|
||||
|
||||
it("bootstraps when cross-signing keys in secret storage", async function () {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
const storagePublicKey = decryption.generate_key();
|
||||
const storagePrivateKey = decryption.get_private_key();
|
||||
|
||||
const bob: MatrixClient = await makeTestClient(
|
||||
@@ -378,8 +374,6 @@ describe("Secrets", function () {
|
||||
});
|
||||
await bob.bootstrapSecretStorage({
|
||||
createSecretStorageKey: async () => ({
|
||||
// `pubkey` not used anymore with symmetric 4S
|
||||
keyInfo: { pubkey: storagePublicKey },
|
||||
privateKey: storagePrivateKey,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
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 "jest-localstorage-mock";
|
||||
import { IndexedDBCryptoStore, LocalStorageCryptoStore, MemoryCryptoStore } from "../../../../src";
|
||||
import { CryptoStore, MigrationState, SESSION_BATCH_SIZE } from "../../../../src/crypto/store/base";
|
||||
|
||||
describe.each([
|
||||
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(global.indexedDB, "tests")],
|
||||
["LocalStorageCryptoStore", () => new LocalStorageCryptoStore(localStorage)],
|
||||
["MemoryCryptoStore", () => new MemoryCryptoStore()],
|
||||
])("CryptoStore tests for %s", function (name, dbFactory) {
|
||||
let store: CryptoStore;
|
||||
|
||||
beforeEach(async () => {
|
||||
store = dbFactory();
|
||||
});
|
||||
|
||||
describe("containsData", () => {
|
||||
it("returns false at first", async () => {
|
||||
expect(await store.containsData()).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true after startup and account setup", async () => {
|
||||
await store.startup();
|
||||
await store.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
|
||||
store.storeAccount(txn, "not a real account");
|
||||
});
|
||||
expect(await store.containsData()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("migrationState", () => {
|
||||
beforeEach(async () => {
|
||||
await store.startup();
|
||||
});
|
||||
|
||||
it("returns 0 at first", async () => {
|
||||
expect(await store.getMigrationState()).toEqual(MigrationState.NOT_STARTED);
|
||||
});
|
||||
|
||||
it("stores updates", async () => {
|
||||
await store.setMigrationState(MigrationState.INITIAL_DATA_MIGRATED);
|
||||
expect(await store.getMigrationState()).toEqual(MigrationState.INITIAL_DATA_MIGRATED);
|
||||
});
|
||||
});
|
||||
|
||||
describe("get/delete EndToEndSessionsBatch", () => {
|
||||
beforeEach(async () => {
|
||||
await store.startup();
|
||||
});
|
||||
|
||||
it("returns null at first", async () => {
|
||||
expect(await store.getEndToEndSessionsBatch()).toBe(null);
|
||||
});
|
||||
|
||||
it("returns a batch of sessions", async () => {
|
||||
// First store some sessions in the db
|
||||
const N_DEVICES = 6;
|
||||
const N_SESSIONS_PER_DEVICE = 6;
|
||||
await createSessions(N_DEVICES, N_SESSIONS_PER_DEVICE);
|
||||
|
||||
let nSessions = 0;
|
||||
await store.doTxn("readonly", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) =>
|
||||
store.countEndToEndSessions(txn, (n) => (nSessions = n)),
|
||||
);
|
||||
expect(nSessions).toEqual(N_DEVICES * N_SESSIONS_PER_DEVICE);
|
||||
|
||||
// Then, get a batch and check it looks right.
|
||||
const batch = await store.getEndToEndSessionsBatch();
|
||||
expect(batch!.length).toEqual(N_DEVICES * N_SESSIONS_PER_DEVICE);
|
||||
for (let i = 0; i < N_DEVICES; i++) {
|
||||
for (let j = 0; j < N_SESSIONS_PER_DEVICE; j++) {
|
||||
const r = batch![i * N_DEVICES + j];
|
||||
|
||||
expect(r.deviceKey).toEqual(`device${i}`);
|
||||
expect(r.sessionId).toEqual(`session${j}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("returns another batch of sessions after the first batch is deleted", async () => {
|
||||
// First store some sessions in the db
|
||||
const N_DEVICES = 8;
|
||||
const N_SESSIONS_PER_DEVICE = 8;
|
||||
await createSessions(N_DEVICES, N_SESSIONS_PER_DEVICE);
|
||||
|
||||
// Get the first batch
|
||||
const batch = (await store.getEndToEndSessionsBatch())!;
|
||||
expect(batch.length).toEqual(SESSION_BATCH_SIZE);
|
||||
|
||||
// ... and delete.
|
||||
await store.deleteEndToEndSessionsBatch(batch);
|
||||
|
||||
// Fetch a second batch
|
||||
const batch2 = (await store.getEndToEndSessionsBatch())!;
|
||||
expect(batch2.length).toEqual(N_DEVICES * N_SESSIONS_PER_DEVICE - SESSION_BATCH_SIZE);
|
||||
|
||||
// ... and delete.
|
||||
await store.deleteEndToEndSessionsBatch(batch2);
|
||||
|
||||
// the batch should now be null.
|
||||
expect(await store.getEndToEndSessionsBatch()).toBe(null);
|
||||
});
|
||||
|
||||
/** Create a bunch of fake Olm sessions and stash them in the DB. */
|
||||
async function createSessions(nDevices: number, nSessionsPerDevice: number) {
|
||||
await store.doTxn("readwrite", IndexedDBCryptoStore.STORE_SESSIONS, (txn) => {
|
||||
for (let i = 0; i < nDevices; i++) {
|
||||
for (let j = 0; j < nSessionsPerDevice; j++) {
|
||||
store.storeEndToEndSession(
|
||||
`device${i}`,
|
||||
`session${j}`,
|
||||
{
|
||||
deviceKey: `device${i}`,
|
||||
sessionId: `session${j}`,
|
||||
},
|
||||
txn,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("get/delete EndToEndInboundGroupSessionsBatch", () => {
|
||||
beforeEach(async () => {
|
||||
await store.startup();
|
||||
});
|
||||
|
||||
it("returns null at first", async () => {
|
||||
expect(await store.getEndToEndInboundGroupSessionsBatch()).toBe(null);
|
||||
});
|
||||
|
||||
it("returns a batch of sessions", async () => {
|
||||
const N_DEVICES = 6;
|
||||
const N_SESSIONS_PER_DEVICE = 6;
|
||||
await createSessions(N_DEVICES, N_SESSIONS_PER_DEVICE);
|
||||
|
||||
// Mark one of the sessions as needing backup
|
||||
await store.doTxn("readwrite", IndexedDBCryptoStore.STORE_BACKUP, async (txn) => {
|
||||
await store.markSessionsNeedingBackup([{ senderKey: pad43("device5"), sessionId: "session5" }], txn);
|
||||
});
|
||||
|
||||
expect(await store.countEndToEndInboundGroupSessions()).toEqual(N_DEVICES * N_SESSIONS_PER_DEVICE);
|
||||
|
||||
const batch = await store.getEndToEndInboundGroupSessionsBatch();
|
||||
expect(batch!.length).toEqual(N_DEVICES * N_SESSIONS_PER_DEVICE);
|
||||
for (let i = 0; i < N_DEVICES; i++) {
|
||||
for (let j = 0; j < N_SESSIONS_PER_DEVICE; j++) {
|
||||
const r = batch![i * N_DEVICES + j];
|
||||
|
||||
expect(r.senderKey).toEqual(pad43(`device${i}`));
|
||||
expect(r.sessionId).toEqual(`session${j}`);
|
||||
|
||||
// only the last session needs backup
|
||||
expect(r.needsBackup).toBe(i === 5 && j === 5);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("returns another batch of sessions after the first batch is deleted", async () => {
|
||||
// First store some sessions in the db
|
||||
const N_DEVICES = 8;
|
||||
const N_SESSIONS_PER_DEVICE = 8;
|
||||
await createSessions(N_DEVICES, N_SESSIONS_PER_DEVICE);
|
||||
|
||||
// Get the first batch
|
||||
const batch = (await store.getEndToEndInboundGroupSessionsBatch())!;
|
||||
expect(batch.length).toEqual(SESSION_BATCH_SIZE);
|
||||
|
||||
// ... and delete.
|
||||
await store.deleteEndToEndInboundGroupSessionsBatch(batch);
|
||||
|
||||
// Fetch a second batch
|
||||
const batch2 = (await store.getEndToEndInboundGroupSessionsBatch())!;
|
||||
expect(batch2.length).toEqual(N_DEVICES * N_SESSIONS_PER_DEVICE - SESSION_BATCH_SIZE);
|
||||
|
||||
// ... and delete.
|
||||
await store.deleteEndToEndInboundGroupSessionsBatch(batch2);
|
||||
|
||||
// the batch should now be null.
|
||||
expect(await store.getEndToEndInboundGroupSessionsBatch()).toBe(null);
|
||||
});
|
||||
|
||||
/** Create a bunch of fake megolm sessions and stash them in the DB. */
|
||||
async function createSessions(nDevices: number, nSessionsPerDevice: number) {
|
||||
await store.doTxn("readwrite", IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, (txn) => {
|
||||
for (let i = 0; i < nDevices; i++) {
|
||||
for (let j = 0; j < nSessionsPerDevice; j++) {
|
||||
store.storeEndToEndInboundGroupSession(
|
||||
pad43(`device${i}`),
|
||||
`session${j}`,
|
||||
{
|
||||
forwardingCurve25519KeyChain: [],
|
||||
keysClaimed: {},
|
||||
room_id: "",
|
||||
session: "",
|
||||
},
|
||||
txn,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/** Pad a string to 43 characters long */
|
||||
function pad43(x: string): string {
|
||||
return x + ".".repeat(43 - x.length);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
Copyright 2024 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 { IndexedDBCryptoStore } from "../../../../src";
|
||||
import { MigrationState } from "../../../../src/crypto/store/base";
|
||||
|
||||
describe("IndexedDBCryptoStore", () => {
|
||||
describe("Test `existsAndIsNotMigrated`", () => {
|
||||
beforeEach(async () => {
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = new IDBFactory();
|
||||
});
|
||||
|
||||
it("Should be true if there is a legacy database", async () => {
|
||||
// should detect a store that is not migrated
|
||||
const store = new IndexedDBCryptoStore(global.indexedDB, "tests");
|
||||
await store.startup();
|
||||
|
||||
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests");
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("Should be true if there is a legacy database in non migrated state", async () => {
|
||||
// should detect a store that is not migrated
|
||||
const store = new IndexedDBCryptoStore(global.indexedDB, "tests");
|
||||
await store.startup();
|
||||
await store.setMigrationState(MigrationState.NOT_STARTED);
|
||||
|
||||
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests");
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
describe.each([
|
||||
MigrationState.INITIAL_DATA_MIGRATED,
|
||||
MigrationState.OLM_SESSIONS_MIGRATED,
|
||||
MigrationState.MEGOLM_SESSIONS_MIGRATED,
|
||||
MigrationState.ROOM_SETTINGS_MIGRATED,
|
||||
])("Exists and Migration state is %s", (migrationState) => {
|
||||
it("Should be false if migration has started", async () => {
|
||||
// should detect a store that is not migrated
|
||||
const store = new IndexedDBCryptoStore(global.indexedDB, "tests");
|
||||
await store.startup();
|
||||
await store.setMigrationState(migrationState);
|
||||
|
||||
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests");
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("Should be false if there is no legacy database", async () => {
|
||||
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests");
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -75,6 +75,20 @@ class MockWidgetApi extends EventEmitter {
|
||||
public transport = { reply: jest.fn() };
|
||||
}
|
||||
|
||||
declare module "../../src/types" {
|
||||
interface StateEvents {
|
||||
"org.example.foo": {
|
||||
hello: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface TimelineEvents {
|
||||
"org.matrix.rageshake_request": {
|
||||
request_id: number;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
describe("RoomWidgetClient", () => {
|
||||
let widgetApi: MockedObject<WidgetApi>;
|
||||
let client: MatrixClient;
|
||||
@@ -87,9 +101,12 @@ describe("RoomWidgetClient", () => {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
const makeClient = async (capabilities: ICapabilities): Promise<void> => {
|
||||
const makeClient = async (
|
||||
capabilities: ICapabilities,
|
||||
sendContentLoaded: boolean | undefined = undefined,
|
||||
): Promise<void> => {
|
||||
const baseUrl = "https://example.org";
|
||||
client = createRoomWidgetClient(widgetApi, capabilities, "!1:example.org", { baseUrl });
|
||||
client = createRoomWidgetClient(widgetApi, capabilities, "!1:example.org", { baseUrl }, sendContentLoaded);
|
||||
expect(widgetApi.start).toHaveBeenCalled(); // needs to have been called early in order to not miss messages
|
||||
widgetApi.emit("ready");
|
||||
await client.startClient();
|
||||
@@ -143,7 +160,7 @@ describe("RoomWidgetClient", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("messages", () => {
|
||||
describe("initialization", () => {
|
||||
it("requests permissions for specific message types", async () => {
|
||||
await makeClient({ sendMessage: [MsgType.Text], receiveMessage: [MsgType.Text] });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
@@ -158,6 +175,15 @@ describe("RoomWidgetClient", () => {
|
||||
expect(widgetApi.requestCapabilityToReceiveMessage).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it("sends content loaded when configured", async () => {
|
||||
await makeClient({});
|
||||
expect(widgetApi.sendContentLoaded).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not sent content loaded when configured", async () => {
|
||||
await makeClient({}, false);
|
||||
expect(widgetApi.sendContentLoaded).not.toHaveBeenCalled();
|
||||
});
|
||||
// No point in testing sending and receiving since it's done exactly the
|
||||
// same way as non-message events
|
||||
});
|
||||
@@ -305,12 +331,14 @@ describe("RoomWidgetClient", () => {
|
||||
expect(await emittedSync).toEqual(SyncState.Syncing);
|
||||
});
|
||||
});
|
||||
|
||||
describe("oidc token", () => {
|
||||
it("requests an oidc token", async () => {
|
||||
await makeClient({});
|
||||
expect(await client.getOpenIdToken()).toStrictEqual(testOIDCToken);
|
||||
});
|
||||
});
|
||||
|
||||
it("gets TURN servers", async () => {
|
||||
const server1: ITurnServer = {
|
||||
uris: [
|
||||
|
||||
@@ -7,6 +7,7 @@ import { MatrixClient } from "../../src/matrix";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { RoomMember } from "../../src/models/room-member";
|
||||
import { EventTimelineSet } from "../../src/models/event-timeline-set";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
describe("EventTimeline", function () {
|
||||
const roomId = "!foo:bar";
|
||||
@@ -50,7 +51,7 @@ describe("EventTimeline", function () {
|
||||
const events = [
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "invite",
|
||||
mship: KnownMembership.Invite,
|
||||
user: userB,
|
||||
skey: userA,
|
||||
event: true,
|
||||
@@ -87,7 +88,7 @@ describe("EventTimeline", function () {
|
||||
const state = [
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "invite",
|
||||
mship: KnownMembership.Invite,
|
||||
user: userB,
|
||||
skey: userA,
|
||||
event: true,
|
||||
@@ -203,11 +204,11 @@ describe("EventTimeline", function () {
|
||||
it("should set event.sender for new and old events", function () {
|
||||
const sentinel = new RoomMember(roomId, userA);
|
||||
sentinel.name = "Alice";
|
||||
sentinel.membership = "join";
|
||||
sentinel.membership = KnownMembership.Join;
|
||||
|
||||
const oldSentinel = new RoomMember(roomId, userA);
|
||||
sentinel.name = "Old Alice";
|
||||
sentinel.membership = "join";
|
||||
sentinel.membership = KnownMembership.Join;
|
||||
|
||||
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
if (uid === userA) {
|
||||
@@ -246,11 +247,11 @@ describe("EventTimeline", function () {
|
||||
it("should set event.target for new and old m.room.member events", function () {
|
||||
const sentinel = new RoomMember(roomId, userA);
|
||||
sentinel.name = "Alice";
|
||||
sentinel.membership = "join";
|
||||
sentinel.membership = KnownMembership.Join;
|
||||
|
||||
const oldSentinel = new RoomMember(roomId, userA);
|
||||
sentinel.name = "Old Alice";
|
||||
sentinel.membership = "join";
|
||||
sentinel.membership = KnownMembership.Join;
|
||||
|
||||
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
if (uid === userA) {
|
||||
@@ -267,14 +268,14 @@ describe("EventTimeline", function () {
|
||||
|
||||
const newEv = utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "invite",
|
||||
mship: KnownMembership.Invite,
|
||||
user: userB,
|
||||
skey: userA,
|
||||
event: true,
|
||||
});
|
||||
const oldEv = utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "ban",
|
||||
mship: KnownMembership.Ban,
|
||||
user: userB,
|
||||
skey: userA,
|
||||
event: true,
|
||||
@@ -291,7 +292,7 @@ describe("EventTimeline", function () {
|
||||
const events = [
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "invite",
|
||||
mship: KnownMembership.Invite,
|
||||
user: userB,
|
||||
skey: userA,
|
||||
event: true,
|
||||
@@ -330,7 +331,7 @@ describe("EventTimeline", function () {
|
||||
const events = [
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "invite",
|
||||
mship: KnownMembership.Invite,
|
||||
user: userB,
|
||||
skey: userA,
|
||||
event: true,
|
||||
|
||||
@@ -22,6 +22,7 @@ import { Filter } from "../../src/filter";
|
||||
import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace";
|
||||
import {
|
||||
EventType,
|
||||
MsgType,
|
||||
RelationType,
|
||||
RoomCreateTypeField,
|
||||
RoomType,
|
||||
@@ -38,38 +39,42 @@ import * as testUtils from "../test-utils/test-utils";
|
||||
import { makeBeaconInfoContent } from "../../src/content-helpers";
|
||||
import { M_BEACON_INFO } from "../../src/@types/beacon";
|
||||
import {
|
||||
ContentHelpers,
|
||||
ClientPrefix,
|
||||
ConditionKind,
|
||||
ContentHelpers,
|
||||
Direction,
|
||||
EventTimeline,
|
||||
EventTimelineSet,
|
||||
getHttpUriForMxc,
|
||||
ICreateRoomOpts,
|
||||
IPushRule,
|
||||
IRequestOpts,
|
||||
MatrixError,
|
||||
MatrixHttpApi,
|
||||
MatrixScheduler,
|
||||
Method,
|
||||
Room,
|
||||
EventTimelineSet,
|
||||
PushRuleActionName,
|
||||
TweakName,
|
||||
Room,
|
||||
RuleId,
|
||||
IPushRule,
|
||||
ConditionKind,
|
||||
TweakName,
|
||||
} from "../../src";
|
||||
import { supportsMatrixCall } from "../../src/webrtc/call";
|
||||
import { makeBeaconEvent } from "../test-utils/beacon";
|
||||
import {
|
||||
IGNORE_INVITES_ACCOUNT_EVENT_KEY,
|
||||
POLICIES_ACCOUNT_EVENT_TYPE,
|
||||
PolicyRecommendation,
|
||||
PolicyScope,
|
||||
} from "../../src/models/invites-ignorer";
|
||||
import { IOlmDevice } from "../../src/crypto/algorithms/megolm";
|
||||
import { QueryDict } from "../../src/utils";
|
||||
import { defer, 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";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
import { RoomMessageEventContent } from "../../src/@types/events";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
@@ -369,6 +374,21 @@ describe("MatrixClient", function () {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
describe("mxcUrlToHttp", () => {
|
||||
it("should call getHttpUriForMxc", () => {
|
||||
const mxc = "mxc://server/example";
|
||||
expect(client.mxcUrlToHttp(mxc)).toBe(getHttpUriForMxc(client.baseUrl, mxc));
|
||||
expect(client.mxcUrlToHttp(mxc, 32)).toBe(getHttpUriForMxc(client.baseUrl, mxc, 32));
|
||||
expect(client.mxcUrlToHttp(mxc, 32, 46)).toBe(getHttpUriForMxc(client.baseUrl, mxc, 32, 46));
|
||||
expect(client.mxcUrlToHttp(mxc, 32, 46, "scale")).toBe(
|
||||
getHttpUriForMxc(client.baseUrl, mxc, 32, 46, "scale"),
|
||||
);
|
||||
expect(client.mxcUrlToHttp(mxc, 32, 46, "scale", false, true)).toBe(
|
||||
getHttpUriForMxc(client.baseUrl, mxc, 32, 46, "scale", false, true),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("timestampToEvent", () => {
|
||||
const roomId = "!room:server.org";
|
||||
const eventId = "$eventId:example.org";
|
||||
@@ -549,7 +569,7 @@ describe("MatrixClient", function () {
|
||||
describe("sendEvent", () => {
|
||||
const roomId = "!room:example.org";
|
||||
const body = "This is the body";
|
||||
const content = { body };
|
||||
const content = { body, msgtype: MsgType.Text } satisfies RoomMessageEventContent;
|
||||
|
||||
it("overload without threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
@@ -644,12 +664,13 @@ describe("MatrixClient", function () {
|
||||
|
||||
const content = {
|
||||
body,
|
||||
"msgtype": MsgType.Text,
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: "$other:event",
|
||||
},
|
||||
},
|
||||
};
|
||||
} satisfies RoomMessageEventContent;
|
||||
|
||||
const room = new Room(roomId, client, userId);
|
||||
mocked(store.getRoom).mockReturnValue(room);
|
||||
@@ -734,7 +755,7 @@ describe("MatrixClient", function () {
|
||||
it("should get (unstable) file trees with valid state", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
getMyMembership: () => "join",
|
||||
getMyMembership: () => KnownMembership.Join,
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
@@ -773,7 +794,7 @@ describe("MatrixClient", function () {
|
||||
it("should not get (unstable) file trees if not joined", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
getMyMembership: () => "leave", // "not join"
|
||||
getMyMembership: () => KnownMembership.Leave, // "not join"
|
||||
} as unknown as Room;
|
||||
client.getRoom = (getRoomId) => {
|
||||
expect(getRoomId).toEqual(roomId);
|
||||
@@ -796,7 +817,7 @@ describe("MatrixClient", function () {
|
||||
it("should not get (unstable) file trees with invalid create contents", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
getMyMembership: () => "join",
|
||||
getMyMembership: () => KnownMembership.Join,
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
@@ -833,7 +854,7 @@ describe("MatrixClient", function () {
|
||||
it("should not get (unstable) file trees with invalid purpose/subtype contents", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
getMyMembership: () => "join",
|
||||
getMyMembership: () => KnownMembership.Join,
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
@@ -1293,7 +1314,7 @@ describe("MatrixClient", function () {
|
||||
describe("redactEvent", () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
getMyMembership: () => "join",
|
||||
getMyMembership: () => KnownMembership.Join,
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
if (eventType === EventType.RoomEncryption) {
|
||||
@@ -1432,27 +1453,13 @@ describe("MatrixClient", function () {
|
||||
const txnId = "m12345";
|
||||
|
||||
const mockRoom = {
|
||||
getMyMembership: () => "join",
|
||||
getMyMembership: () => KnownMembership.Join,
|
||||
updatePendingEvent: (event: MatrixEvent, status: EventStatus) => event.setStatus(status),
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[RoomCreateTypeField]: RoomType.Space,
|
||||
},
|
||||
});
|
||||
} else if (eventType === EventType.RoomEncryption) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({ content: {} });
|
||||
} else {
|
||||
throw new Error("Unexpected event type or state key");
|
||||
}
|
||||
},
|
||||
} as Room["currentState"],
|
||||
hasEncryptionStateEvent: jest.fn().mockReturnValue(true),
|
||||
} as unknown as Room;
|
||||
|
||||
let mockCrypto: Mocked<Crypto>;
|
||||
|
||||
let event: MatrixEvent;
|
||||
beforeEach(async () => {
|
||||
event = new MatrixEvent({
|
||||
@@ -1467,11 +1474,12 @@ describe("MatrixClient", function () {
|
||||
expect(getRoomId).toEqual(roomId);
|
||||
return mockRoom;
|
||||
};
|
||||
client.crypto = client["cryptoBackend"] = {
|
||||
// mock crypto
|
||||
encryptEvent: () => new Promise(() => {}),
|
||||
mockCrypto = {
|
||||
isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(true),
|
||||
encryptEvent: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
} as unknown as Crypto;
|
||||
} as unknown as Mocked<Crypto>;
|
||||
client.crypto = client["cryptoBackend"] = mockCrypto;
|
||||
});
|
||||
|
||||
function assertCancelled() {
|
||||
@@ -1488,12 +1496,21 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
|
||||
it("should cancel an event which is encrypting", async () => {
|
||||
const encryptEventDefer = defer();
|
||||
mockCrypto.encryptEvent.mockReturnValue(encryptEventDefer.promise);
|
||||
|
||||
const statusPromise = testUtils.emitPromise(event, "Event.status");
|
||||
// @ts-ignore protected method access
|
||||
client.encryptAndSendEvent(mockRoom, event);
|
||||
await testUtils.emitPromise(event, "Event.status");
|
||||
const encryptAndSendPromise = client.encryptAndSendEvent(mockRoom, event);
|
||||
await statusPromise;
|
||||
expect(event.status).toBe(EventStatus.ENCRYPTING);
|
||||
client.cancelPendingEvent(event);
|
||||
assertCancelled();
|
||||
|
||||
// now let the encryption complete, and check that the message is not sent.
|
||||
encryptEventDefer.resolve();
|
||||
await encryptAndSendPromise;
|
||||
assertCancelled();
|
||||
});
|
||||
|
||||
it("should cancel an event which is not sent", () => {
|
||||
@@ -1514,8 +1531,6 @@ describe("MatrixClient", function () {
|
||||
{ startOpts: {}, hasThreadSupport: false },
|
||||
{ startOpts: { threadSupport: true }, hasThreadSupport: true },
|
||||
{ startOpts: { threadSupport: false }, hasThreadSupport: false },
|
||||
{ startOpts: { experimentalThreadSupport: true }, hasThreadSupport: true },
|
||||
{ startOpts: { experimentalThreadSupport: true, threadSupport: false }, hasThreadSupport: false },
|
||||
])("enabled thread support for the SDK instance", async ({ startOpts, hasThreadSupport }) => {
|
||||
await client.startClient(startOpts);
|
||||
expect(client.supportsThreads()).toBe(hasThreadSupport);
|
||||
@@ -2069,10 +2084,10 @@ describe("MatrixClient", function () {
|
||||
await client.ignoredInvites.addSource(NEW_SOURCE_ROOM_ID);
|
||||
|
||||
// Add a rule in the new source room.
|
||||
await client.sendStateEvent(NEW_SOURCE_ROOM_ID, PolicyScope.User, {
|
||||
await client.sendStateEvent(NEW_SOURCE_ROOM_ID, EventType.PolicyRuleUser, {
|
||||
entity: "*:example.org",
|
||||
reason: "just a test",
|
||||
recommendation: "m.ban",
|
||||
recommendation: PolicyRecommendation.Ban,
|
||||
});
|
||||
|
||||
// We should reject this invite.
|
||||
@@ -2159,8 +2174,8 @@ describe("MatrixClient", function () {
|
||||
// Check where it shows up.
|
||||
const targetRoomId = ignoreInvites2.target;
|
||||
const targetRoom = client.getRoom(targetRoomId) as WrappedRoom;
|
||||
expect(targetRoom._state.get(PolicyScope.User)[eventId]).toBeTruthy();
|
||||
expect(newSourceRoom._state.get(PolicyScope.User)?.[eventId]).toBeFalsy();
|
||||
expect(targetRoom._state.get(EventType.PolicyRuleUser)[eventId]).toBeTruthy();
|
||||
expect(newSourceRoom._state.get(EventType.PolicyRuleUser)?.[eventId]).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2211,8 +2226,7 @@ describe("MatrixClient", function () {
|
||||
"org.matrix.msc3391": true,
|
||||
},
|
||||
};
|
||||
jest.spyOn(client.http, "request").mockResolvedValue(versionsResponse);
|
||||
const requestSpy = jest.spyOn(client.http, "authedRequest").mockImplementation(() => Promise.resolve());
|
||||
const requestSpy = jest.spyOn(client.http, "authedRequest").mockResolvedValue(versionsResponse);
|
||||
const unstablePrefix = "/_matrix/client/unstable/org.matrix.msc3391";
|
||||
const path = `/user/${encodeURIComponent(userId)}/account_data/${eventType}`;
|
||||
|
||||
@@ -2250,8 +2264,7 @@ describe("MatrixClient", function () {
|
||||
"org.matrix.msc3391": false,
|
||||
},
|
||||
};
|
||||
jest.spyOn(client.http, "request").mockResolvedValue(versionsResponse);
|
||||
const requestSpy = jest.spyOn(client.http, "authedRequest").mockImplementation(() => Promise.resolve());
|
||||
const requestSpy = jest.spyOn(client.http, "authedRequest").mockResolvedValue(versionsResponse);
|
||||
const path = `/user/${encodeURIComponent(userId)}/account_data/${eventType}`;
|
||||
|
||||
// populate version support
|
||||
@@ -3002,4 +3015,22 @@ describe("MatrixClient", function () {
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAuthIssuer", () => {
|
||||
it("should use unstable prefix", async () => {
|
||||
httpLookups = [
|
||||
{
|
||||
method: "GET",
|
||||
path: `/auth_issuer`,
|
||||
data: {
|
||||
issuer: "https://issuer/",
|
||||
},
|
||||
prefix: "/_matrix/client/unstable/org.matrix.msc2965",
|
||||
},
|
||||
];
|
||||
|
||||
await expect(client.getAuthIssuer()).resolves.toEqual({ issuer: "https://issuer/" });
|
||||
expect(httpLookups.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,9 +34,12 @@ function makeMockEvent(originTs = 0): MatrixEvent {
|
||||
}
|
||||
|
||||
describe("CallMembership", () => {
|
||||
it("rejects membership with no expiry", () => {
|
||||
it("rejects membership with no expiry and no expires_ts", () => {
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { expires: undefined }));
|
||||
new CallMembership(
|
||||
makeMockEvent(),
|
||||
Object.assign({}, membershipTemplate, { expires: undefined, expires_ts: undefined }),
|
||||
);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
@@ -57,6 +60,16 @@ describe("CallMembership", () => {
|
||||
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined }));
|
||||
}).toThrow();
|
||||
});
|
||||
it("rejects with malformatted expires_ts", () => {
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { expires_ts: "string" }));
|
||||
}).toThrow();
|
||||
});
|
||||
it("rejects with malformatted expires", () => {
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { expires: "string" }));
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("uses event timestamp if no created_ts", () => {
|
||||
const membership = new CallMembership(makeMockEvent(12345), membershipTemplate);
|
||||
@@ -71,11 +84,19 @@ describe("CallMembership", () => {
|
||||
expect(membership.createdTs()).toEqual(67890);
|
||||
});
|
||||
|
||||
it("computes absolute expiry time", () => {
|
||||
it("computes absolute expiry time based on expires", () => {
|
||||
const membership = new CallMembership(makeMockEvent(1000), membershipTemplate);
|
||||
expect(membership.getAbsoluteExpiry()).toEqual(5000 + 1000);
|
||||
});
|
||||
|
||||
it("computes absolute expiry time based on expires_ts", () => {
|
||||
const membership = new CallMembership(
|
||||
makeMockEvent(1000),
|
||||
Object.assign({}, membershipTemplate, { expires: undefined, expires_ts: 6000 }),
|
||||
);
|
||||
expect(membership.getAbsoluteExpiry()).toEqual(5000 + 1000);
|
||||
});
|
||||
|
||||
it("considers memberships unexpired if local age low enough", () => {
|
||||
const fakeEvent = makeMockEvent(1000);
|
||||
fakeEvent.getLocalAge = jest.fn().mockReturnValue(3000);
|
||||
|
||||
@@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventTimeline, EventType, MatrixClient, MatrixError, MatrixEvent, Room } from "../../../src";
|
||||
import { KnownMembership } from "../../../src/@types/membership";
|
||||
import { CallMembershipData } from "../../../src/matrixrtc/CallMembership";
|
||||
import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession";
|
||||
import { EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
|
||||
@@ -74,6 +75,13 @@ describe("MatrixRTCSession", () => {
|
||||
expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
|
||||
});
|
||||
|
||||
it("ignores memberships events of members not in the room", () => {
|
||||
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||
mockRoom.hasMembershipState = (state) => state === KnownMembership.Join;
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
expect(sess?.memberships.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("honours created_ts", () => {
|
||||
const expiredMembership = Object.assign({}, membershipTemplate);
|
||||
expiredMembership.created_ts = 500;
|
||||
@@ -91,9 +99,12 @@ describe("MatrixRTCSession", () => {
|
||||
|
||||
it("safely ignores events with no memberships section", () => {
|
||||
const mockRoom = {
|
||||
...makeMockRoom([]),
|
||||
roomId: randomString(8),
|
||||
getLiveTimeline: jest.fn().mockReturnValue({
|
||||
getState: jest.fn().mockReturnValue({
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
getStateEvents: (_type: string, _stateKey: string) => [
|
||||
{
|
||||
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
||||
@@ -112,9 +123,12 @@ describe("MatrixRTCSession", () => {
|
||||
|
||||
it("safely ignores events with junk memberships section", () => {
|
||||
const mockRoom = {
|
||||
...makeMockRoom([]),
|
||||
roomId: randomString(8),
|
||||
getLiveTimeline: jest.fn().mockReturnValue({
|
||||
getState: jest.fn().mockReturnValue({
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
getStateEvents: (_type: string, _stateKey: string) => [
|
||||
{
|
||||
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
||||
@@ -214,8 +228,8 @@ describe("MatrixRTCSession", () => {
|
||||
});
|
||||
|
||||
it("sends a membership event when joining a call", () => {
|
||||
jest.useFakeTimers();
|
||||
sess!.joinRoomSession([mockFocus]);
|
||||
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||
mockRoom!.roomId,
|
||||
EventType.GroupCallMemberPrefix,
|
||||
@@ -227,6 +241,7 @@ describe("MatrixRTCSession", () => {
|
||||
call_id: "",
|
||||
device_id: "AAAAAAA",
|
||||
expires: 3600000,
|
||||
expires_ts: Date.now() + 3600000,
|
||||
foci_active: [{ type: "mock" }],
|
||||
membershipID: expect.stringMatching(".*"),
|
||||
},
|
||||
@@ -234,6 +249,7 @@ describe("MatrixRTCSession", () => {
|
||||
},
|
||||
"@alice:example.org",
|
||||
);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("does nothing if join called when already joined", () => {
|
||||
@@ -291,6 +307,7 @@ describe("MatrixRTCSession", () => {
|
||||
call_id: "",
|
||||
device_id: "AAAAAAA",
|
||||
expires: 3600000 * 2,
|
||||
expires_ts: 1000 + 3600000 * 2,
|
||||
foci_active: [{ type: "mock" }],
|
||||
created_ts: 1000,
|
||||
membershipID: expect.stringMatching(".*"),
|
||||
@@ -510,7 +527,7 @@ describe("MatrixRTCSession", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("Does not emits if no membership changes", () => {
|
||||
it("Does not emit if no membership changes", () => {
|
||||
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
|
||||
@@ -591,6 +608,7 @@ describe("MatrixRTCSession", () => {
|
||||
call_id: "",
|
||||
device_id: "AAAAAAA",
|
||||
expires: 3600000,
|
||||
expires_ts: Date.now() + 3600000,
|
||||
foci_active: [mockFocus],
|
||||
membershipID: expect.stringMatching(".*"),
|
||||
},
|
||||
@@ -605,7 +623,7 @@ describe("MatrixRTCSession", () => {
|
||||
|
||||
it("fills in created_ts for other memberships on update", () => {
|
||||
client.sendStateEvent = jest.fn();
|
||||
|
||||
jest.useFakeTimers();
|
||||
const mockRoom = makeMockRoom([
|
||||
Object.assign({}, membershipTemplate, {
|
||||
device_id: "OTHERDEVICE",
|
||||
@@ -635,6 +653,7 @@ describe("MatrixRTCSession", () => {
|
||||
call_id: "",
|
||||
device_id: "AAAAAAA",
|
||||
expires: 3600000,
|
||||
expires_ts: Date.now() + 3600000,
|
||||
foci_active: [mockFocus],
|
||||
membershipID: expect.stringMatching(".*"),
|
||||
},
|
||||
@@ -642,6 +661,7 @@ describe("MatrixRTCSession", () => {
|
||||
},
|
||||
"@alice:example.org",
|
||||
);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("collects keys from encryption events", () => {
|
||||
|
||||
@@ -87,7 +87,7 @@ describe("MatrixRTCSessionManager", () => {
|
||||
expect(onEnded).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
|
||||
});
|
||||
|
||||
it("Calls onCallEncryption on encryption keys event", () => {
|
||||
it("Calls onCallEncryption on encryption keys event", async () => {
|
||||
const room1 = makeMockRoom([membershipTemplate]);
|
||||
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room1);
|
||||
@@ -95,7 +95,7 @@ describe("MatrixRTCSessionManager", () => {
|
||||
client.emit(ClientEvent.Room, room1);
|
||||
const onCallEncryptionMock = jest.fn();
|
||||
client.matrixRTC.getRoomSession(room1).onCallEncryption = onCallEncryptionMock;
|
||||
|
||||
client.decryptEventIfNeeded = () => Promise.resolve();
|
||||
const timelineEvent = {
|
||||
getType: jest.fn().mockReturnValue(EventType.CallEncryptionKeysPrefix),
|
||||
getContent: jest.fn().mockReturnValue({}),
|
||||
@@ -106,6 +106,7 @@ describe("MatrixRTCSessionManager", () => {
|
||||
},
|
||||
} as unknown as MatrixEvent;
|
||||
client.emit(RoomEvent.Timeline, timelineEvent, undefined, undefined, false, {} as IRoomTimelineData);
|
||||
await new Promise(process.nextTick);
|
||||
expect(onCallEncryptionMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ export function makeMockRoom(memberships: CallMembershipData[], localAge: number
|
||||
const roomState = makeMockRoomState(memberships, roomId, localAge);
|
||||
return {
|
||||
roomId: roomId,
|
||||
hasMembershipState: jest.fn().mockReturnValue(true),
|
||||
getLiveTimeline: jest.fn().mockReturnValue({
|
||||
getState: jest.fn().mockReturnValue(roomState),
|
||||
}),
|
||||
@@ -33,6 +34,8 @@ export function makeMockRoom(memberships: CallMembershipData[], localAge: number
|
||||
export function makeMockRoomState(memberships: CallMembershipData[], roomId: string, localAge: number | null = null) {
|
||||
const event = mockRTCEvent(memberships, roomId, localAge);
|
||||
return {
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
getStateEvents: (_: string, stateKey: string) => {
|
||||
if (stateKey !== undefined) return event;
|
||||
return [event];
|
||||
|
||||
@@ -213,7 +213,7 @@ describe("MSC3089Branch", () => {
|
||||
expect(eventId).toEqual(fileEventId);
|
||||
return fileEvent;
|
||||
},
|
||||
} as EventTimelineSet);
|
||||
}) as EventTimelineSet;
|
||||
client.mxcUrlToHttp = (mxc: string) => {
|
||||
expect(mxc).toEqual("mxc://" + mxcLatter);
|
||||
return `https://example.org/_matrix/media/v1/download/${mxcLatter}`;
|
||||
@@ -239,7 +239,7 @@ describe("MSC3089Branch", () => {
|
||||
expect(eventId).toEqual(fileEventId);
|
||||
return fileEvent;
|
||||
},
|
||||
} as EventTimelineSet);
|
||||
}) as EventTimelineSet;
|
||||
client.mxcUrlToHttp = (mxc: string) => {
|
||||
expect(mxc).toEqual("mxc://" + mxcLatter);
|
||||
return `https://example.org/_matrix/media/v1/download/${mxcLatter}`;
|
||||
@@ -332,7 +332,7 @@ describe("MSC3089Branch", () => {
|
||||
getId: () => "$unknown",
|
||||
},
|
||||
];
|
||||
staticRoom.getLiveTimeline = () => ({ getEvents: () => events } as EventTimeline);
|
||||
staticRoom.getLiveTimeline = () => ({ getEvents: () => events }) as EventTimeline;
|
||||
|
||||
directory.getFile = (evId: string) => {
|
||||
expect(evId).toEqual(fileEventId);
|
||||
|
||||
@@ -25,6 +25,8 @@ import {
|
||||
} from "../../../src/models/MSC3089TreeSpace";
|
||||
import { DEFAULT_ALPHABET } from "../../../src/utils";
|
||||
import { MatrixError } from "../../../src/http-api";
|
||||
import { KnownMembership } from "../../../src/@types/membership";
|
||||
import { EncryptedFile } from "../../../src/@types/media";
|
||||
|
||||
describe("MSC3089TreeSpace", () => {
|
||||
let client: MatrixClient;
|
||||
@@ -399,7 +401,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
];
|
||||
},
|
||||
};
|
||||
client.getRoom = () => ({} as Room); // to appease the TreeSpace constructor
|
||||
client.getRoom = () => ({}) as Room; // to appease the TreeSpace constructor
|
||||
|
||||
const getFn = jest.fn().mockImplementation((roomId: string) => {
|
||||
if (roomId === thirdChildRoom) {
|
||||
@@ -422,7 +424,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
});
|
||||
|
||||
it("should find specific directories", () => {
|
||||
client.getRoom = () => ({} as Room); // to appease the TreeSpace constructor
|
||||
client.getRoom = () => ({}) as Room; // to appease the TreeSpace constructor
|
||||
|
||||
// Only mocking used API
|
||||
const firstSubdirectory = { roomId: "!first:example.org" } as any as MSC3089TreeSpace;
|
||||
@@ -458,14 +460,14 @@ describe("MSC3089TreeSpace", () => {
|
||||
expect(stateKey).toBeUndefined();
|
||||
return [
|
||||
// Partial implementations
|
||||
{ getContent: () => ({ membership: "join" }), getStateKey: () => joinMemberId },
|
||||
{ getContent: () => ({ membership: "knock" }), getStateKey: () => knockMemberId },
|
||||
{ getContent: () => ({ membership: "invite" }), getStateKey: () => inviteMemberId },
|
||||
{ getContent: () => ({ membership: "leave" }), getStateKey: () => leaveMemberId },
|
||||
{ getContent: () => ({ membership: "ban" }), getStateKey: () => banMemberId },
|
||||
{ getContent: () => ({ membership: KnownMembership.Join }), getStateKey: () => joinMemberId },
|
||||
{ getContent: () => ({ membership: KnownMembership.Knock }), getStateKey: () => knockMemberId },
|
||||
{ getContent: () => ({ membership: KnownMembership.Invite }), getStateKey: () => inviteMemberId },
|
||||
{ getContent: () => ({ membership: KnownMembership.Leave }), getStateKey: () => leaveMemberId },
|
||||
{ getContent: () => ({ membership: KnownMembership.Ban }), getStateKey: () => banMemberId },
|
||||
|
||||
// ensure we don't kick ourselves
|
||||
{ getContent: () => ({ membership: "join" }), getStateKey: () => selfUserId },
|
||||
{ getContent: () => ({ membership: KnownMembership.Join }), getStateKey: () => selfUserId },
|
||||
];
|
||||
},
|
||||
};
|
||||
@@ -946,7 +948,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
const fileInfo = {
|
||||
mimetype: "text/plain",
|
||||
// other fields as required by encryption, but ignored here
|
||||
};
|
||||
} as unknown as EncryptedFile;
|
||||
const fileEventId = "$file";
|
||||
const fileName = "My File.txt";
|
||||
const fileContents = "This is a test file";
|
||||
@@ -1006,7 +1008,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
const fileInfo = {
|
||||
mimetype: "text/plain",
|
||||
// other fields as required by encryption, but ignored here
|
||||
};
|
||||
} as unknown as EncryptedFile;
|
||||
const fileEventId = "$file";
|
||||
const fileName = "My File.txt";
|
||||
const fileContents = "This is a test file";
|
||||
|
||||
@@ -27,6 +27,8 @@ import {
|
||||
THREAD_RELATION_TYPE,
|
||||
TweakName,
|
||||
} from "../../../src";
|
||||
import { DecryptionFailureCode } from "../../../src/crypto-api";
|
||||
import { DecryptionError } from "../../../src/common-crypto/CryptoBackend";
|
||||
|
||||
describe("MatrixEvent", () => {
|
||||
it("should create copies of itself", () => {
|
||||
@@ -360,20 +362,50 @@ describe("MatrixEvent", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should report decryption errors", async () => {
|
||||
it("should report unknown decryption errors", async () => {
|
||||
const decryptionListener = jest.fn();
|
||||
encryptedEvent.addListener(MatrixEventEvent.Decrypted, decryptionListener);
|
||||
|
||||
const testError = new Error("test error");
|
||||
const crypto = {
|
||||
decryptEvent: jest.fn().mockRejectedValue(new Error("test error")),
|
||||
decryptEvent: jest.fn().mockRejectedValue(testError),
|
||||
} as unknown as Crypto;
|
||||
|
||||
await encryptedEvent.attemptDecryption(crypto);
|
||||
expect(encryptedEvent.isEncrypted()).toBeTruthy();
|
||||
expect(encryptedEvent.isBeingDecrypted()).toBeFalsy();
|
||||
expect(encryptedEvent.isDecryptionFailure()).toBeTruthy();
|
||||
expect(encryptedEvent.decryptionFailureReason).toEqual(DecryptionFailureCode.UNKNOWN_ERROR);
|
||||
expect(encryptedEvent.isEncryptedDisabledForUnverifiedDevices).toBeFalsy();
|
||||
expect(encryptedEvent.getContent()).toEqual({
|
||||
msgtype: "m.bad.encrypted",
|
||||
body: "** Unable to decrypt: Error: test error **",
|
||||
});
|
||||
expect(decryptionListener).toHaveBeenCalledWith(encryptedEvent, testError);
|
||||
});
|
||||
|
||||
it("should report known decryption errors", async () => {
|
||||
const decryptionListener = jest.fn();
|
||||
encryptedEvent.addListener(MatrixEventEvent.Decrypted, decryptionListener);
|
||||
|
||||
const testError = new DecryptionError(DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID, "uisi");
|
||||
const crypto = {
|
||||
decryptEvent: jest.fn().mockRejectedValue(testError),
|
||||
} as unknown as Crypto;
|
||||
|
||||
await encryptedEvent.attemptDecryption(crypto);
|
||||
expect(encryptedEvent.isEncrypted()).toBeTruthy();
|
||||
expect(encryptedEvent.isBeingDecrypted()).toBeFalsy();
|
||||
expect(encryptedEvent.isDecryptionFailure()).toBeTruthy();
|
||||
expect(encryptedEvent.decryptionFailureReason).toEqual(
|
||||
DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
|
||||
);
|
||||
expect(encryptedEvent.isEncryptedDisabledForUnverifiedDevices).toBeFalsy();
|
||||
expect(encryptedEvent.getContent()).toEqual({
|
||||
msgtype: "m.bad.encrypted",
|
||||
body: "** Unable to decrypt: DecryptionError: uisi **",
|
||||
});
|
||||
expect(decryptionListener).toHaveBeenCalledWith(encryptedEvent, testError);
|
||||
});
|
||||
|
||||
it(`should report "DecryptionError: The sender has disabled encrypting to unverified devices."`, async () => {
|
||||
@@ -423,6 +455,8 @@ describe("MatrixEvent", () => {
|
||||
expect(eventAttemptDecryptionSpy).toHaveBeenCalledTimes(2);
|
||||
expect(crypto.decryptEvent).toHaveBeenCalledTimes(2);
|
||||
expect(encryptedEvent.getType()).toEqual("m.room.message");
|
||||
expect(encryptedEvent.isDecryptionFailure()).toBe(false);
|
||||
expect(encryptedEvent.decryptionFailureReason).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -469,52 +503,6 @@ describe("MatrixEvent", () => {
|
||||
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", () => {
|
||||
@@ -543,7 +531,7 @@ describe("MatrixEvent", () => {
|
||||
});
|
||||
event.setPushDetails(prevActions, pushRule);
|
||||
|
||||
event.setPushActions(actions);
|
||||
event.setPushDetails(actions);
|
||||
|
||||
// rule is not in event push cache
|
||||
expect(event.getPushDetails()).toEqual({ actions });
|
||||
|
||||
@@ -0,0 +1,541 @@
|
||||
/*
|
||||
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 { FeatureSupport, MatrixClient, MatrixEvent, ReceiptContent, THREAD_RELATION_TYPE, Thread } from "../../../src";
|
||||
import { Room } from "../../../src/models/room";
|
||||
|
||||
/**
|
||||
* Note, these tests check the functionality of the RoomReceipts class, but most
|
||||
* of them access that functionality via the surrounding Room class, because a
|
||||
* room is required for RoomReceipts to function, and this matches the pattern
|
||||
* of how this code is used in the wild.
|
||||
*/
|
||||
describe("RoomReceipts", () => {
|
||||
beforeAll(() => {
|
||||
jest.replaceProperty(Thread, "hasServerSideSupport", FeatureSupport.Stable);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("reports events unread if there are no receipts", () => {
|
||||
// Given there are no receipts in the room
|
||||
const room = createRoom();
|
||||
const [event] = createEvent();
|
||||
room.addLiveEvents([event]);
|
||||
|
||||
// When I ask about any event, then it is unread
|
||||
expect(room.hasUserReadEvent(readerId, event.getId()!)).toBe(false);
|
||||
});
|
||||
|
||||
it("reports events we sent as read even if there are no (real) receipts", () => {
|
||||
// Given there are no receipts in the room
|
||||
const room = createRoom();
|
||||
const [event] = createEventSentBy(readerId);
|
||||
room.addLiveEvents([event]);
|
||||
|
||||
// When I ask about an event I sent, it is read (because a synthetic
|
||||
// receipt was created and stored in RoomReceipts)
|
||||
expect(room.hasUserReadEvent(readerId, event.getId()!)).toBe(true);
|
||||
});
|
||||
|
||||
it("reports read if we receive an unthreaded receipt for this event", () => {
|
||||
// Given my event exists and is unread
|
||||
const room = createRoom();
|
||||
const [event, eventId] = createEvent();
|
||||
room.addLiveEvents([event]);
|
||||
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
|
||||
|
||||
// When we receive a receipt for this event+user
|
||||
room.addReceipt(createReceipt(readerId, event));
|
||||
|
||||
// Then that event is read
|
||||
expect(room.hasUserReadEvent(readerId, eventId)).toBe(true);
|
||||
});
|
||||
|
||||
it("reports read if we receive an unthreaded receipt for a later event", () => {
|
||||
// Given we have 2 events
|
||||
const room = createRoom();
|
||||
const [event1, event1Id] = createEvent();
|
||||
const [event2] = createEvent();
|
||||
room.addLiveEvents([event1, event2]);
|
||||
|
||||
// When we receive a receipt for the later event
|
||||
room.addReceipt(createReceipt(readerId, event2));
|
||||
|
||||
// Then the earlier one is read
|
||||
expect(room.hasUserReadEvent(readerId, event1Id)).toBe(true);
|
||||
});
|
||||
|
||||
it("reports read for a non-live event if we receive an unthreaded receipt for a live one", () => {
|
||||
// Given we have 2 events: one live and one old
|
||||
const room = createRoom();
|
||||
const [oldEvent, oldEventId] = createEvent();
|
||||
const [liveEvent] = createEvent();
|
||||
room.addLiveEvents([liveEvent]);
|
||||
createOldTimeline(room, [oldEvent]);
|
||||
|
||||
// When we receive a receipt for the live event
|
||||
room.addReceipt(createReceipt(readerId, liveEvent));
|
||||
|
||||
// Then the earlier one is read
|
||||
expect(room.hasUserReadEvent(readerId, oldEventId)).toBe(true);
|
||||
});
|
||||
|
||||
it("compares by timestamp if two events are in separate old timelines", () => {
|
||||
// Given we have 2 events, both in old timelines, with event2 after
|
||||
// event1 in terms of timestamps
|
||||
const room = createRoom();
|
||||
const [event1, event1Id] = createEvent();
|
||||
const [event2, event2Id] = createEvent();
|
||||
event1.event.origin_server_ts = 1;
|
||||
event2.event.origin_server_ts = 2;
|
||||
createOldTimeline(room, [event1]);
|
||||
createOldTimeline(room, [event2]);
|
||||
|
||||
// When we receive a receipt for the older event
|
||||
room.addReceipt(createReceipt(readerId, event1));
|
||||
|
||||
// Then the earlier one is read and the later one is not
|
||||
expect(room.hasUserReadEvent(readerId, event1Id)).toBe(true);
|
||||
expect(room.hasUserReadEvent(readerId, event2Id)).toBe(false);
|
||||
});
|
||||
|
||||
it("reports unread if we receive an unthreaded receipt for an earlier event", () => {
|
||||
// Given we have 2 events
|
||||
const room = createRoom();
|
||||
const [event1] = createEvent();
|
||||
const [event2, event2Id] = createEvent();
|
||||
room.addLiveEvents([event1, event2]);
|
||||
|
||||
// When we receive a receipt for the earlier event
|
||||
room.addReceipt(createReceipt(readerId, event1));
|
||||
|
||||
// Then the later one is unread
|
||||
expect(room.hasUserReadEvent(readerId, event2Id)).toBe(false);
|
||||
});
|
||||
|
||||
it("reports unread if we receive an unthreaded receipt for a different user", () => {
|
||||
// Given my event exists and is unread
|
||||
const room = createRoom();
|
||||
const [event, eventId] = createEvent();
|
||||
room.addLiveEvents([event]);
|
||||
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
|
||||
|
||||
// When we receive a receipt for another user
|
||||
room.addReceipt(createReceipt(otherUserId, event));
|
||||
|
||||
// Then the event is still unread since the receipt was not for us
|
||||
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
|
||||
|
||||
// But it's read for the other person
|
||||
expect(room.hasUserReadEvent(otherUserId, eventId)).toBe(true);
|
||||
});
|
||||
|
||||
it("reports events we sent as read even if an earlier receipt arrives", () => {
|
||||
// Given we sent an event after some other event
|
||||
const room = createRoom();
|
||||
const [previousEvent] = createEvent();
|
||||
const [myEvent] = createEventSentBy(readerId);
|
||||
room.addLiveEvents([previousEvent, myEvent]);
|
||||
|
||||
// And I just received a receipt for the previous event
|
||||
room.addReceipt(createReceipt(readerId, previousEvent));
|
||||
|
||||
// When I ask about the event I sent, it is read (because of synthetic receipts)
|
||||
expect(room.hasUserReadEvent(readerId, myEvent.getId()!)).toBe(true);
|
||||
});
|
||||
|
||||
it("considers events after ones we sent to be unread", () => {
|
||||
// Given we sent an event, then another event came in
|
||||
const room = createRoom();
|
||||
const [myEvent] = createEventSentBy(readerId);
|
||||
const [laterEvent] = createEvent();
|
||||
room.addLiveEvents([myEvent, laterEvent]);
|
||||
|
||||
// When I ask about the later event, it is unread (because it's after the synthetic receipt)
|
||||
expect(room.hasUserReadEvent(readerId, laterEvent.getId()!)).toBe(false);
|
||||
});
|
||||
|
||||
it("correctly reports readness even when receipts arrive out of order", () => {
|
||||
// Given we have 3 events
|
||||
const room = createRoom();
|
||||
const [event1] = createEvent();
|
||||
const [event2, event2Id] = createEvent();
|
||||
const [event3, event3Id] = createEvent();
|
||||
room.addLiveEvents([event1, event2, event3]);
|
||||
|
||||
// When we receive receipts for the older events out of order
|
||||
room.addReceipt(createReceipt(readerId, event2));
|
||||
room.addReceipt(createReceipt(readerId, event1));
|
||||
|
||||
// Then we correctly ignore the older receipt
|
||||
expect(room.hasUserReadEvent(readerId, event2Id)).toBe(true);
|
||||
expect(room.hasUserReadEvent(readerId, event3Id)).toBe(false);
|
||||
});
|
||||
|
||||
it("reports read if we receive a threaded receipt for this event (main)", () => {
|
||||
// Given my event exists and is unread
|
||||
const room = createRoom();
|
||||
const [event, eventId] = createEvent();
|
||||
room.addLiveEvents([event]);
|
||||
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
|
||||
|
||||
// When we receive a receipt for this event+user
|
||||
room.addReceipt(createThreadedReceipt(readerId, event, "main"));
|
||||
|
||||
// Then that event is read
|
||||
expect(room.hasUserReadEvent(readerId, eventId)).toBe(true);
|
||||
});
|
||||
|
||||
it("reports read if we receive a threaded receipt for this event (non-main)", () => {
|
||||
// Given my event exists and is unread
|
||||
const room = createRoom();
|
||||
const [root, rootId] = createEvent();
|
||||
const [event, eventId] = createThreadedEvent(root);
|
||||
setupThread(room, root);
|
||||
room.addLiveEvents([root, event]);
|
||||
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
|
||||
|
||||
// When we receive a receipt for this event on this thread
|
||||
room.addReceipt(createThreadedReceipt(readerId, event, rootId));
|
||||
|
||||
// Then that event is read
|
||||
expect(room.hasUserReadEvent(readerId, eventId)).toBe(true);
|
||||
});
|
||||
|
||||
it("reports read if we receive an threaded receipt for a later event", () => {
|
||||
// Given we have 2 events in a thread
|
||||
const room = createRoom();
|
||||
const [root, rootId] = createEvent();
|
||||
const [event1, event1Id] = createThreadedEvent(root);
|
||||
const [event2] = createThreadedEvent(root);
|
||||
setupThread(room, root);
|
||||
room.addLiveEvents([root, event1, event2]);
|
||||
|
||||
// When we receive a receipt for the later event
|
||||
room.addReceipt(createThreadedReceipt(readerId, event2, rootId));
|
||||
|
||||
// Then the earlier one is read
|
||||
expect(room.hasUserReadEvent(readerId, event1Id)).toBe(true);
|
||||
});
|
||||
|
||||
it("reports unread if we receive an threaded receipt for an earlier event", () => {
|
||||
// Given we have 2 events in a thread
|
||||
const room = createRoom();
|
||||
const [root, rootId] = createEvent();
|
||||
const [event1] = createThreadedEvent(root);
|
||||
const [event2, event2Id] = createThreadedEvent(root);
|
||||
setupThread(room, root);
|
||||
room.addLiveEvents([root, event1, event2]);
|
||||
|
||||
// When we receive a receipt for the earlier event
|
||||
room.addReceipt(createThreadedReceipt(readerId, event1, rootId));
|
||||
|
||||
// Then the later one is unread
|
||||
expect(room.hasUserReadEvent(readerId, event2Id)).toBe(false);
|
||||
});
|
||||
|
||||
it("reports unread if we receive an threaded receipt for a different user", () => {
|
||||
// Given my event exists and is unread
|
||||
const room = createRoom();
|
||||
const [root, rootId] = createEvent();
|
||||
const [event, eventId] = createThreadedEvent(root);
|
||||
setupThread(room, root);
|
||||
room.addLiveEvents([root, event]);
|
||||
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
|
||||
|
||||
// When we receive a receipt for another user
|
||||
room.addReceipt(createThreadedReceipt(otherUserId, event, rootId));
|
||||
|
||||
// Then the event is still unread since the receipt was not for us
|
||||
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
|
||||
|
||||
// But it's read for the other person
|
||||
expect(room.hasUserReadEvent(otherUserId, eventId)).toBe(true);
|
||||
});
|
||||
|
||||
it("reports unread if we receive a receipt for a later event in a different thread", () => {
|
||||
// Given 2 events exist in different threads
|
||||
const room = createRoom();
|
||||
const [root1] = createEvent();
|
||||
const [root2] = createEvent();
|
||||
const [thread1, thread1Id] = createThreadedEvent(root1);
|
||||
const [thread2] = createThreadedEvent(root2);
|
||||
setupThread(room, root1);
|
||||
setupThread(room, root2);
|
||||
room.addLiveEvents([root1, root2, thread1, thread2]);
|
||||
|
||||
// When we receive a receipt for the later event
|
||||
room.addReceipt(createThreadedReceipt(readerId, thread2, root2.getId()!));
|
||||
|
||||
// Then the old one is still unread since the receipt was not for this thread
|
||||
expect(room.hasUserReadEvent(readerId, thread1Id)).toBe(false);
|
||||
});
|
||||
|
||||
it("correctly reports readness even when threaded receipts arrive out of order", () => {
|
||||
// Given we have 3 events
|
||||
const room = createRoom();
|
||||
const [root, rootId] = createEvent();
|
||||
const [event1] = createThreadedEvent(root);
|
||||
const [event2, event2Id] = createThreadedEvent(root);
|
||||
const [event3, event3Id] = createThreadedEvent(root);
|
||||
setupThread(room, root);
|
||||
room.addLiveEvents([root, event1, event2, event3]);
|
||||
|
||||
// When we receive receipts for the older events out of order
|
||||
room.addReceipt(createThreadedReceipt(readerId, event2, rootId));
|
||||
room.addReceipt(createThreadedReceipt(readerId, event1, rootId));
|
||||
|
||||
// Then we correctly ignore the older receipt
|
||||
expect(room.hasUserReadEvent(readerId, event2Id)).toBe(true);
|
||||
expect(room.hasUserReadEvent(readerId, event3Id)).toBe(false);
|
||||
});
|
||||
|
||||
it("correctly reports readness when mixing threaded and unthreaded receipts", () => {
|
||||
// Given we have a setup from this presentation:
|
||||
// https://docs.google.com/presentation/d/1H1gxRmRFAm8d71hCILWmpOYezsvdlb7cB6ANl-20Gns/edit?usp=sharing
|
||||
//
|
||||
// Main1----\
|
||||
// | ---Thread1a <- threaded receipt
|
||||
// | |
|
||||
// | Thread1b
|
||||
// threaded receipt -> Main2--\
|
||||
// | ----------------Thread2a <- unthreaded receipt
|
||||
// Main3 |
|
||||
// Thread2b <- threaded receipt
|
||||
//
|
||||
const room = createRoom();
|
||||
const [main1, main1Id] = createEvent();
|
||||
const [main2, main2Id] = createEvent();
|
||||
const [main3, main3Id] = createEvent();
|
||||
const [thread1a, thread1aId] = createThreadedEvent(main1);
|
||||
const [thread1b, thread1bId] = createThreadedEvent(main1);
|
||||
const [thread2a, thread2aId] = createThreadedEvent(main2);
|
||||
const [thread2b, thread2bId] = createThreadedEvent(main2);
|
||||
setupThread(room, main1);
|
||||
setupThread(room, main2);
|
||||
room.addLiveEvents([main1, thread1a, thread1b, main2, thread2a, main3, thread2b]);
|
||||
|
||||
// And the timestamps on the events are consistent with the order above
|
||||
main1.event.origin_server_ts = 1;
|
||||
thread1a.event.origin_server_ts = 2;
|
||||
thread1b.event.origin_server_ts = 3;
|
||||
main2.event.origin_server_ts = 4;
|
||||
thread2a.event.origin_server_ts = 5;
|
||||
main3.event.origin_server_ts = 6;
|
||||
thread2b.event.origin_server_ts = 7;
|
||||
// (Note: in principle, we have the information needed to order these
|
||||
// events without using their timestamps, since they all came in via
|
||||
// addLiveEvents. In reality, some of them would have come in via the
|
||||
// /relations API, making it impossible to get the correct ordering
|
||||
// without MSC4033, which is why we fall back to timestamps. I.e. we
|
||||
// definitely could fix the code to make the above
|
||||
// timestamp-manipulation unnecessary, but it would only make this test
|
||||
// neater, not actually help in the real world.)
|
||||
|
||||
// When the receipts arrive
|
||||
room.addReceipt(createThreadedReceipt(readerId, main2, "main"));
|
||||
room.addReceipt(createThreadedReceipt(readerId, thread1a, main1Id));
|
||||
room.addReceipt(createReceipt(readerId, thread2a));
|
||||
room.addReceipt(createThreadedReceipt(readerId, thread2b, main2Id));
|
||||
|
||||
// Then we correctly identify that only main3 is unread
|
||||
expect(room.hasUserReadEvent(readerId, main1Id)).toBe(true);
|
||||
expect(room.hasUserReadEvent(readerId, main2Id)).toBe(true);
|
||||
expect(room.hasUserReadEvent(readerId, main3Id)).toBe(false);
|
||||
expect(room.hasUserReadEvent(readerId, thread1aId)).toBe(true);
|
||||
expect(room.hasUserReadEvent(readerId, thread1bId)).toBe(true);
|
||||
expect(room.hasUserReadEvent(readerId, thread2aId)).toBe(true);
|
||||
expect(room.hasUserReadEvent(readerId, thread2bId)).toBe(true);
|
||||
});
|
||||
|
||||
describe("dangling receipts", () => {
|
||||
it("reports unread if the unthreaded receipt is in a dangling state", () => {
|
||||
const room = createRoom();
|
||||
const [event, eventId] = createEvent();
|
||||
// When we receive a receipt for this event+user
|
||||
room.addReceipt(createReceipt(readerId, event));
|
||||
|
||||
// The event is not added in the room
|
||||
// So the receipt is in a dangling state
|
||||
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
|
||||
|
||||
// Add the event to the room
|
||||
// The receipt is removed from the dangling state
|
||||
room.addLiveEvents([event]);
|
||||
|
||||
// Then the event is read
|
||||
expect(room.hasUserReadEvent(readerId, eventId)).toBe(true);
|
||||
});
|
||||
|
||||
it("reports unread if the threaded receipt is in a dangling state", () => {
|
||||
const room = createRoom();
|
||||
const [root, rootId] = createEvent();
|
||||
const [event, eventId] = createThreadedEvent(root);
|
||||
setupThread(room, root);
|
||||
|
||||
// When we receive a receipt for this event+user
|
||||
room.addReceipt(createThreadedReceipt(readerId, event, rootId));
|
||||
|
||||
// The event is not added in the room
|
||||
// So the receipt is in a dangling state
|
||||
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
|
||||
|
||||
// Add the events to the room
|
||||
// The receipt is removed from the dangling state
|
||||
room.addLiveEvents([root, event]);
|
||||
|
||||
// Then the event is read
|
||||
expect(room.hasUserReadEvent(readerId, eventId)).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle multiple dangling receipts for the same event", () => {
|
||||
const room = createRoom();
|
||||
const [event, eventId] = createEvent();
|
||||
// When we receive a receipt for this event+user
|
||||
room.addReceipt(createReceipt(readerId, event));
|
||||
// We receive another receipt in the same event for another user
|
||||
room.addReceipt(createReceipt(otherUserId, event));
|
||||
|
||||
// The event is not added in the room
|
||||
// So the receipt is in a dangling state
|
||||
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
|
||||
|
||||
// Add the event to the room
|
||||
// The two receipts should be processed
|
||||
room.addLiveEvents([event]);
|
||||
|
||||
// Then the event is read
|
||||
// We expect that the receipt of `otherUserId` didn't replace/erase the receipt of `readerId`
|
||||
expect(room.hasUserReadEvent(readerId, eventId)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createFakeClient(): MatrixClient {
|
||||
return {
|
||||
getUserId: jest.fn(),
|
||||
getEventMapper: jest.fn().mockReturnValue(jest.fn()),
|
||||
isInitialSyncComplete: jest.fn().mockReturnValue(true),
|
||||
supportsThreads: jest.fn().mockReturnValue(true),
|
||||
fetchRoomEvent: jest.fn().mockResolvedValue({}),
|
||||
paginateEventTimeline: jest.fn(),
|
||||
canSupport: { get: jest.fn() },
|
||||
} as unknown as MatrixClient;
|
||||
}
|
||||
|
||||
const senderId = "sender:s.ss";
|
||||
const readerId = "reader:r.rr";
|
||||
const otherUserId = "other:o.oo";
|
||||
|
||||
function createRoom(): Room {
|
||||
return new Room("!rid", createFakeClient(), "@u:s.nz", { timelineSupport: true });
|
||||
}
|
||||
|
||||
let idCounter = 0;
|
||||
function nextId(): string {
|
||||
return "$" + (idCounter++).toString(10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an event and return it and its ID.
|
||||
*/
|
||||
function createEvent(): [MatrixEvent, string] {
|
||||
return createEventSentBy(senderId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an event with the supplied sender and return it and its ID.
|
||||
*/
|
||||
function createEventSentBy(customSenderId: string): [MatrixEvent, string] {
|
||||
const event = new MatrixEvent({ sender: customSenderId, event_id: nextId() });
|
||||
return [event, event.getId()!];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an event in the thread of the supplied root and return it and its ID.
|
||||
*/
|
||||
function createThreadedEvent(root: MatrixEvent): [MatrixEvent, string] {
|
||||
const rootEventId = root.getId()!;
|
||||
const event = new MatrixEvent({
|
||||
sender: senderId,
|
||||
event_id: nextId(),
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
event_id: rootEventId,
|
||||
rel_type: THREAD_RELATION_TYPE.name,
|
||||
["m.in_reply_to"]: {
|
||||
event_id: rootEventId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return [event, event.getId()!];
|
||||
}
|
||||
|
||||
function createReceipt(userId: string, referencedEvent: MatrixEvent): MatrixEvent {
|
||||
const content: ReceiptContent = {
|
||||
[referencedEvent.getId()!]: {
|
||||
"m.read": {
|
||||
[userId]: {
|
||||
ts: 123,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return new MatrixEvent({
|
||||
type: "m.receipt",
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
function createThreadedReceipt(userId: string, referencedEvent: MatrixEvent, threadId: string): MatrixEvent {
|
||||
const content: ReceiptContent = {
|
||||
[referencedEvent.getId()!]: {
|
||||
"m.read": {
|
||||
[userId]: {
|
||||
ts: 123,
|
||||
thread_id: threadId,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return new MatrixEvent({
|
||||
type: "m.receipt",
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a timeline in the timeline set that is not the live timeline.
|
||||
*/
|
||||
function createOldTimeline(room: Room, events: MatrixEvent[]) {
|
||||
const oldTimeline = room.getUnfilteredTimelineSet().addTimeline();
|
||||
room.getUnfilteredTimelineSet().addEventsToTimeline(events, true, oldTimeline);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the hacks required for this room to create a thread based on the root
|
||||
* event supplied.
|
||||
*/
|
||||
function setupThread(room: Room, root: MatrixEvent) {
|
||||
const thread = room.createThread(root.getId()!, root, [root], false);
|
||||
thread.initialEventsFetched = true;
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import { mocked } from "jest-mock";
|
||||
import { MatrixClient, PendingEventOrdering } from "../../../src/client";
|
||||
import { Room, RoomEvent } from "../../../src/models/room";
|
||||
import { FeatureSupport, Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../../src/models/thread";
|
||||
import { makeThreadEvent, mkThread } from "../../test-utils/thread";
|
||||
import { makeThreadEvent, mkThread, populateThread } from "../../test-utils/thread";
|
||||
import { TestClient } from "../../TestClient";
|
||||
import { emitPromise, mkEdit, mkMessage, mkReaction, mock } from "../../test-utils/test-utils";
|
||||
import { Direction, EventStatus, EventType, MatrixEvent } from "../../../src";
|
||||
@@ -149,20 +149,38 @@ describe("Thread", () => {
|
||||
});
|
||||
|
||||
it("considers other events with no RR as unread", () => {
|
||||
const { thread, events } = mkThread({
|
||||
// Given a long thread exists
|
||||
const { thread, events } = populateThread({
|
||||
room,
|
||||
client,
|
||||
authorId: myUserId,
|
||||
participantUserIds: [myUserId],
|
||||
authorId: "@other:foo.com",
|
||||
participantUserIds: ["@other:foo.com"],
|
||||
length: 25,
|
||||
ts: 190,
|
||||
});
|
||||
|
||||
// Before alice's last unthreaded receipt
|
||||
expect(thread.hasUserReadEvent("@alice:example.org", events.at(1)!.getId() ?? "")).toBeTruthy();
|
||||
const event1 = events.at(1)!;
|
||||
const event2 = events.at(2)!;
|
||||
const event24 = events.at(24)!;
|
||||
|
||||
// After alice's last unthreaded receipt
|
||||
expect(thread.hasUserReadEvent("@alice:example.org", events.at(-1)!.getId() ?? "")).toBeFalsy();
|
||||
// And we have read the second message in it with an unthreaded receipt
|
||||
const receipt = new MatrixEvent({
|
||||
type: "m.receipt",
|
||||
room_id: room.roomId,
|
||||
content: {
|
||||
// unthreaded receipt for the second message in the thread
|
||||
[event2.getId()!]: {
|
||||
[ReceiptType.Read]: {
|
||||
[myUserId]: { ts: 200 },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
room.addReceipt(receipt);
|
||||
|
||||
// Then we have read the first message in the thread, and not the last
|
||||
expect(thread.hasUserReadEvent(myUserId, event1.getId()!)).toBe(true);
|
||||
expect(thread.hasUserReadEvent(myUserId, event24.getId()!)).toBe(false);
|
||||
});
|
||||
|
||||
it("considers event as read if there's a more recent unthreaded receipt", () => {
|
||||
@@ -481,13 +499,13 @@ describe("Thread", () => {
|
||||
|
||||
// And a thread with an added event (with later timestamp)
|
||||
const userId = "user1";
|
||||
const { thread, message } = await createThreadAndEvent(client, 1, 100, userId);
|
||||
const { thread, message2 } = await createThreadAnd2Events(client, 1, 100, 200, 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?.eventId).toEqual(message2.getId());
|
||||
expect(receipt?.data.ts).toEqual(200);
|
||||
expect(receipt?.data.thread_id).toEqual(thread.id);
|
||||
|
||||
// (And the receipt was synthetic)
|
||||
@@ -505,14 +523,14 @@ describe("Thread", () => {
|
||||
|
||||
// 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);
|
||||
const { thread, message1 } = await createThreadAnd2Events(client, 300, 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
|
||||
// Then the receipt is for the first message, because its
|
||||
// timestamp is later. 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());
|
||||
expect(thread.getReadReceiptForUserId(userId)?.eventId).toEqual(message1.getId());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -530,11 +548,11 @@ describe("Thread", () => {
|
||||
|
||||
// And a thread with an added event (with later timestamp)
|
||||
const userId = "user1";
|
||||
const { thread, message } = await createThreadAndEvent(client, 1, 100, userId);
|
||||
const { thread, message2 } = await createThreadAnd2Events(client, 1, 100, 200, userId);
|
||||
|
||||
// Then a receipt was added to the thread
|
||||
const receipt = thread.getReadReceiptForUserId(userId);
|
||||
expect(receipt?.eventId).toEqual(message.getId());
|
||||
expect(receipt?.eventId).toEqual(message2.getId());
|
||||
});
|
||||
|
||||
it("Creates a local echo receipt even for events BEFORE an existing receipt", async () => {
|
||||
@@ -550,22 +568,24 @@ describe("Thread", () => {
|
||||
|
||||
// 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);
|
||||
const { thread, message2 } = await createThreadAnd2Events(client, 300, 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.
|
||||
// Then a receipt was added for the last message, even though it
|
||||
// has lower ts, 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());
|
||||
expect(receipt?.eventId).toEqual(message2.getId());
|
||||
});
|
||||
});
|
||||
|
||||
async function createThreadAndEvent(
|
||||
async function createThreadAnd2Events(
|
||||
client: MatrixClient,
|
||||
rootTs: number,
|
||||
eventTs: number,
|
||||
message1Ts: number,
|
||||
message2Ts: number,
|
||||
userId: string,
|
||||
): Promise<{ thread: Thread; message: MatrixEvent }> {
|
||||
): Promise<{ thread: Thread; message1: MatrixEvent; message2: MatrixEvent }> {
|
||||
const room = new Room("room1", client, userId);
|
||||
|
||||
// Given a thread
|
||||
@@ -576,24 +596,41 @@ describe("Thread", () => {
|
||||
participantUserIds: [],
|
||||
ts: rootTs,
|
||||
});
|
||||
// Sanity: the current receipt is for the thread root
|
||||
expect(thread.getReadReceiptForUserId(userId)?.eventId).toEqual(thread.rootEvent?.getId());
|
||||
// Sanity: there is no read receipt on the thread yet because the
|
||||
// thread events don't get properly added to the room by mkThread.
|
||||
expect(thread.getReadReceiptForUserId(userId)).toBeNull();
|
||||
|
||||
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({
|
||||
// Add a message with ts message1Ts
|
||||
const message1 = makeThreadEvent({
|
||||
event: true,
|
||||
rootEventId: thread.id,
|
||||
replyToEventId: thread.id,
|
||||
user: userId,
|
||||
room: room.roomId,
|
||||
ts: eventTs,
|
||||
ts: message1Ts,
|
||||
});
|
||||
await thread.addEvent(message, false, true);
|
||||
await thread.addEvent(message1, false, true);
|
||||
await awaitTimelineEvent;
|
||||
|
||||
return { thread, message };
|
||||
// Sanity: the thread now has a properly-added event, so this event
|
||||
// has a synthetic receipt.
|
||||
expect(thread.getReadReceiptForUserId(userId)?.eventId).toEqual(message1.getId());
|
||||
|
||||
// Add a message with ts message2Ts
|
||||
const message2 = makeThreadEvent({
|
||||
event: true,
|
||||
rootEventId: thread.id,
|
||||
replyToEventId: thread.id,
|
||||
user: userId,
|
||||
room: room.roomId,
|
||||
ts: message2Ts,
|
||||
});
|
||||
await thread.addEvent(message2, false, true);
|
||||
await awaitTimelineEvent;
|
||||
|
||||
return { thread, message1, message2 };
|
||||
}
|
||||
|
||||
function createClientWithEventMapper(canSupport: Map<Feature, ServerSupport> = new Map()): MatrixClient {
|
||||
|
||||
@@ -106,6 +106,8 @@ describe("fixNotificationCountOnDecryption", () => {
|
||||
mockClient,
|
||||
);
|
||||
|
||||
room.addLiveEvents([event]);
|
||||
|
||||
THREAD_ID = event.getId()!;
|
||||
threadEvent = mkEvent({
|
||||
type: EventType.RoomMessage,
|
||||
|
||||
@@ -20,7 +20,10 @@ limitations under the License.
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import { mocked } from "jest-mock";
|
||||
import jwtDecode from "jwt-decode";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import { Crypto } from "@peculiar/webcrypto";
|
||||
import { getRandomValues } from "node:crypto";
|
||||
import { TextEncoder } from "node:util";
|
||||
|
||||
import { Method } from "../../../src";
|
||||
import * as crypto from "../../../src/crypto/crypto";
|
||||
@@ -36,13 +39,15 @@ import { makeDelegatedAuthConfig, mockOpenIdConfiguration } from "../../test-uti
|
||||
|
||||
jest.mock("jwt-decode");
|
||||
|
||||
const webCrypto = new Crypto();
|
||||
|
||||
// save for resetting mocks
|
||||
const realSubtleCrypto = crypto.subtleCrypto;
|
||||
|
||||
describe("oidc authorization", () => {
|
||||
const delegatedAuthConfig = makeDelegatedAuthConfig();
|
||||
const authorizationEndpoint = delegatedAuthConfig.metadata.authorization_endpoint;
|
||||
const tokenEndpoint = delegatedAuthConfig.metadata.token_endpoint;
|
||||
const authorizationEndpoint = delegatedAuthConfig.authorizationEndpoint;
|
||||
const tokenEndpoint = delegatedAuthConfig.tokenEndpoint;
|
||||
const clientId = "xyz789";
|
||||
const baseUrl = "https://test.com";
|
||||
|
||||
@@ -53,7 +58,19 @@ describe("oidc authorization", () => {
|
||||
jest.spyOn(logger, "warn");
|
||||
jest.setSystemTime(now);
|
||||
|
||||
fetchMock.get(delegatedAuthConfig.issuer + ".well-known/openid-configuration", mockOpenIdConfiguration());
|
||||
fetchMock.get(
|
||||
delegatedAuthConfig.metadata.issuer + ".well-known/openid-configuration",
|
||||
mockOpenIdConfiguration(),
|
||||
);
|
||||
|
||||
Object.defineProperty(window, "crypto", {
|
||||
value: {
|
||||
getRandomValues,
|
||||
randomUUID: jest.fn().mockReturnValue("not-random-uuid"),
|
||||
subtle: webCrypto.subtle,
|
||||
},
|
||||
});
|
||||
global.TextEncoder = TextEncoder;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -165,7 +182,8 @@ describe("oidc authorization", () => {
|
||||
token_type: "Bearer",
|
||||
access_token: "test_access_token",
|
||||
refresh_token: "test_refresh_token",
|
||||
id_token: "valid.id.token",
|
||||
id_token:
|
||||
"eyJhbGciOiJSUzI1NiIsImtpZCI6Imh4ZEhXb0Y5bW4ifQ.eyJleHAiOjE3MDgzNTY3NjcsInN1YiI6IjAxSFBQMkZTQllERTlQOUVNTThERDdXWkhSIiwiYXVkIjoiMDFIUTBXSDUyV0paV1JSU1k5V0VFUTVUMlEiLCJub25jZSI6ImhScEI2cGtFMDYiLCJhdXRoX3RpbWUiOjE3MDc5OTAzMTIsImlhdCI6MTcwODM1MzE2NywiYXRfaGFzaCI6Il9HSEU4cDhocHFnMW1ac041YUlycVEiLCJpc3MiOiJodHRwczovL2F1dGgtb2lkYy5sYWIuZWxlbWVudC5kZXYvIiwiY19oYXNoIjoib2hJRmNuaUZWd3pGSzVJdXlsX1RlQSJ9.SGUG78dCC3sTWgQBDTicKwamKiPpb6REiz79CM2ml_kVJCoS7gT0TlztC4h25FKi3c9aB3XCVn9J8UzvJgvG8Rt_oS--FIuhK6oRm7NdcN0bCkbG7iZEWGxx-kQnifcCFHyZ6T1CxR8X00Uvc6_lRfBZVlTyuuQaJ_PHiiKMlV93FbxvQUIq6FTkQP2Z56p4JIXIzjOONzA91skTqQGycl5f9Vhp6cqXFzl6ARK30M7A-8UI5qCxClUJ7kD9KgN5YZ7uivLp1x01WBnik2DXH0eSwXcTX2WLkYtMXgMxylJhIiO586apIC5nr7sfip-Y_4PgBlSjRRgrmOGC-VUFCA",
|
||||
expires_in: 300,
|
||||
};
|
||||
|
||||
|
||||
@@ -17,39 +17,48 @@ limitations under the License.
|
||||
import fetchMockJest from "fetch-mock-jest";
|
||||
|
||||
import { OidcError } from "../../../src/oidc/error";
|
||||
import { registerOidcClient } from "../../../src/oidc/register";
|
||||
import { OidcRegistrationClientMetadata, registerOidcClient } from "../../../src/oidc/register";
|
||||
import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
|
||||
|
||||
describe("registerOidcClient()", () => {
|
||||
const issuer = "https://auth.com/";
|
||||
const registrationEndpoint = "https://auth.com/register";
|
||||
const clientName = "Element";
|
||||
const baseUrl = "https://just.testing";
|
||||
const metadata: OidcRegistrationClientMetadata = {
|
||||
clientUri: baseUrl,
|
||||
redirectUris: [baseUrl],
|
||||
clientName,
|
||||
applicationType: "web",
|
||||
tosUri: "http://tos-uri",
|
||||
policyUri: "http://policy-uri",
|
||||
contacts: ["admin@example.com"],
|
||||
};
|
||||
const dynamicClientId = "xyz789";
|
||||
|
||||
const delegatedAuthConfig = {
|
||||
issuer,
|
||||
registrationEndpoint,
|
||||
authorizationEndpoint: issuer + "auth",
|
||||
tokenEndpoint: issuer + "token",
|
||||
};
|
||||
const delegatedAuthConfig = makeDelegatedAuthConfig(issuer);
|
||||
beforeEach(() => {
|
||||
fetchMockJest.mockClear();
|
||||
fetchMockJest.resetBehavior();
|
||||
});
|
||||
|
||||
it("should make correct request to register client", async () => {
|
||||
fetchMockJest.post(registrationEndpoint, {
|
||||
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
|
||||
status: 200,
|
||||
body: JSON.stringify({ client_id: dynamicClientId }),
|
||||
});
|
||||
expect(await registerOidcClient(delegatedAuthConfig, clientName, baseUrl)).toEqual(dynamicClientId);
|
||||
expect(fetchMockJest).toHaveBeenCalledWith(registrationEndpoint, {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
expect(await registerOidcClient(delegatedAuthConfig, metadata)).toEqual(dynamicClientId);
|
||||
expect(fetchMockJest).toHaveBeenCalledWith(
|
||||
delegatedAuthConfig.registrationEndpoint!,
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
}),
|
||||
);
|
||||
expect(JSON.parse(fetchMockJest.mock.calls[0][1]!.body as string)).toEqual(
|
||||
expect.objectContaining({
|
||||
client_name: clientName,
|
||||
client_uri: baseUrl,
|
||||
response_types: ["code"],
|
||||
@@ -59,26 +68,53 @@ describe("registerOidcClient()", () => {
|
||||
token_endpoint_auth_method: "none",
|
||||
application_type: "web",
|
||||
}),
|
||||
});
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw when registration request fails", async () => {
|
||||
fetchMockJest.post(registrationEndpoint, {
|
||||
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
|
||||
status: 500,
|
||||
});
|
||||
await expect(() => registerOidcClient(delegatedAuthConfig, clientName, baseUrl)).rejects.toThrow(
|
||||
await expect(() => registerOidcClient(delegatedAuthConfig, metadata)).rejects.toThrow(
|
||||
OidcError.DynamicRegistrationFailed,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw when registration response is invalid", async () => {
|
||||
fetchMockJest.post(registrationEndpoint, {
|
||||
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
|
||||
status: 200,
|
||||
// no clientId in response
|
||||
body: "{}",
|
||||
});
|
||||
await expect(() => registerOidcClient(delegatedAuthConfig, clientName, baseUrl)).rejects.toThrow(
|
||||
await expect(() => registerOidcClient(delegatedAuthConfig, metadata)).rejects.toThrow(
|
||||
OidcError.DynamicRegistrationInvalid,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw when required endpoints are unavailable", async () => {
|
||||
await expect(() =>
|
||||
registerOidcClient(
|
||||
{
|
||||
...delegatedAuthConfig,
|
||||
registrationEndpoint: undefined,
|
||||
},
|
||||
metadata,
|
||||
),
|
||||
).rejects.toThrow(OidcError.DynamicRegistrationNotSupported);
|
||||
});
|
||||
|
||||
it("should throw when required scopes are unavailable", async () => {
|
||||
await expect(() =>
|
||||
registerOidcClient(
|
||||
{
|
||||
...delegatedAuthConfig,
|
||||
metadata: {
|
||||
...delegatedAuthConfig.metadata,
|
||||
grant_types_supported: [delegatedAuthConfig.metadata.grant_types_supported[0]],
|
||||
},
|
||||
},
|
||||
metadata,
|
||||
),
|
||||
).rejects.toThrow(OidcError.DynamicRegistrationNotSupported);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,7 +64,7 @@ describe("OidcTokenRefresher", () => {
|
||||
keys: [],
|
||||
});
|
||||
|
||||
fetchMock.post(config.metadata.token_endpoint, {
|
||||
fetchMock.post(config.tokenEndpoint, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -88,7 +88,7 @@ describe("OidcTokenRefresher", () => {
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
await expect(refresher.oidcClientReady).rejects.toThrow();
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
"Failed to initialise OIDC client.",
|
||||
@@ -98,7 +98,7 @@ describe("OidcTokenRefresher", () => {
|
||||
});
|
||||
|
||||
it("initialises oidc client", async () => {
|
||||
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
await refresher.oidcClientReady;
|
||||
|
||||
// @ts-ignore peek at private property to see we initialised the client correctly
|
||||
@@ -114,19 +114,19 @@ describe("OidcTokenRefresher", () => {
|
||||
|
||||
describe("doRefreshAccessToken()", () => {
|
||||
it("should throw when oidcClient has not been initialised", async () => {
|
||||
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
await expect(refresher.doRefreshAccessToken("token")).rejects.toThrow(
|
||||
"Cannot get new token before OIDC client is initialised.",
|
||||
);
|
||||
});
|
||||
|
||||
it("should refresh the tokens", async () => {
|
||||
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
await refresher.oidcClientReady;
|
||||
|
||||
const result = await refresher.doRefreshAccessToken("refresh-token");
|
||||
|
||||
expect(fetchMock).toHaveFetched(config.metadata.token_endpoint, {
|
||||
expect(fetchMock).toHaveFetched(config.tokenEndpoint, {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
@@ -137,7 +137,7 @@ describe("OidcTokenRefresher", () => {
|
||||
});
|
||||
|
||||
it("should persist the new tokens", async () => {
|
||||
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
await refresher.oidcClientReady;
|
||||
// spy on our stub
|
||||
jest.spyOn(refresher, "persistTokens");
|
||||
@@ -153,7 +153,7 @@ describe("OidcTokenRefresher", () => {
|
||||
it("should only have one inflight refresh request at once", async () => {
|
||||
fetchMock
|
||||
.postOnce(
|
||||
config.metadata.token_endpoint,
|
||||
config.tokenEndpoint,
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
@@ -164,7 +164,7 @@ describe("OidcTokenRefresher", () => {
|
||||
{ overwriteRoutes: true },
|
||||
)
|
||||
.postOnce(
|
||||
config.metadata.token_endpoint,
|
||||
config.tokenEndpoint,
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
@@ -175,7 +175,7 @@ describe("OidcTokenRefresher", () => {
|
||||
{ overwriteRoutes: false },
|
||||
);
|
||||
|
||||
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
await refresher.oidcClientReady;
|
||||
// reset call counts
|
||||
fetchMock.resetHistory();
|
||||
@@ -188,7 +188,7 @@ describe("OidcTokenRefresher", () => {
|
||||
const result2 = await first;
|
||||
|
||||
// only one call to token endpoint
|
||||
expect(fetchMock).toHaveFetchedTimes(1, config.metadata.token_endpoint);
|
||||
expect(fetchMock).toHaveFetchedTimes(1, config.tokenEndpoint);
|
||||
expect(result1).toEqual({
|
||||
accessToken: "first-new-access-token",
|
||||
refreshToken: "first-new-refresh-token",
|
||||
@@ -208,7 +208,7 @@ describe("OidcTokenRefresher", () => {
|
||||
|
||||
it("should log and rethrow when token refresh fails", async () => {
|
||||
fetchMock.post(
|
||||
config.metadata.token_endpoint,
|
||||
config.tokenEndpoint,
|
||||
{
|
||||
status: 503,
|
||||
headers: {
|
||||
@@ -218,7 +218,7 @@ describe("OidcTokenRefresher", () => {
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
|
||||
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
await refresher.oidcClientReady;
|
||||
|
||||
await expect(refresher.doRefreshAccessToken("refresh-token")).rejects.toThrow();
|
||||
@@ -228,7 +228,7 @@ describe("OidcTokenRefresher", () => {
|
||||
// make sure inflight request is cleared after a failure
|
||||
fetchMock
|
||||
.postOnce(
|
||||
config.metadata.token_endpoint,
|
||||
config.tokenEndpoint,
|
||||
{
|
||||
status: 503,
|
||||
headers: {
|
||||
@@ -238,7 +238,7 @@ describe("OidcTokenRefresher", () => {
|
||||
{ overwriteRoutes: true },
|
||||
)
|
||||
.postOnce(
|
||||
config.metadata.token_endpoint,
|
||||
config.tokenEndpoint,
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
@@ -249,7 +249,7 @@ describe("OidcTokenRefresher", () => {
|
||||
{ overwriteRoutes: false },
|
||||
);
|
||||
|
||||
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
await refresher.oidcClientReady;
|
||||
// reset call counts
|
||||
fetchMock.resetHistory();
|
||||
|
||||
@@ -15,107 +15,14 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import jwtDecode from "jwt-decode";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
|
||||
import { M_AUTHENTICATION } from "../../../src";
|
||||
import { logger } from "../../../src/logger";
|
||||
import {
|
||||
validateIdToken,
|
||||
validateOIDCIssuerWellKnown,
|
||||
validateWellKnownAuthentication,
|
||||
} from "../../../src/oidc/validate";
|
||||
import { validateIdToken, validateOIDCIssuerWellKnown } from "../../../src/oidc/validate";
|
||||
import { OidcError } from "../../../src/oidc/error";
|
||||
|
||||
jest.mock("jwt-decode");
|
||||
|
||||
describe("validateWellKnownAuthentication()", () => {
|
||||
const baseWk = {
|
||||
"m.homeserver": {
|
||||
base_url: "https://hs.org",
|
||||
},
|
||||
};
|
||||
it("should throw not supported error when wellKnown has no m.authentication section", () => {
|
||||
expect(() => validateWellKnownAuthentication(undefined)).toThrow(OidcError.NotSupported);
|
||||
});
|
||||
|
||||
it("should throw misconfigured error when authentication issuer is not a string", () => {
|
||||
const wk = {
|
||||
...baseWk,
|
||||
[M_AUTHENTICATION.stable!]: {
|
||||
issuer: { url: "test.com" },
|
||||
},
|
||||
};
|
||||
expect(() => validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!] as any)).toThrow(
|
||||
OidcError.Misconfigured,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw misconfigured error when authentication account is not a string", () => {
|
||||
const wk = {
|
||||
...baseWk,
|
||||
[M_AUTHENTICATION.stable!]: {
|
||||
issuer: "test.com",
|
||||
account: { url: "test" },
|
||||
},
|
||||
};
|
||||
expect(() => validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!] as any)).toThrow(
|
||||
OidcError.Misconfigured,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw misconfigured error when authentication account is false", () => {
|
||||
const wk = {
|
||||
...baseWk,
|
||||
[M_AUTHENTICATION.stable!]: {
|
||||
issuer: "test.com",
|
||||
account: false,
|
||||
},
|
||||
};
|
||||
expect(() => validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!] as any)).toThrow(
|
||||
OidcError.Misconfigured,
|
||||
);
|
||||
});
|
||||
|
||||
it("should return valid config when wk uses stable m.authentication", () => {
|
||||
const wk = {
|
||||
...baseWk,
|
||||
[M_AUTHENTICATION.stable!]: {
|
||||
issuer: "test.com",
|
||||
account: "account.com",
|
||||
},
|
||||
};
|
||||
expect(validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!])).toEqual({
|
||||
issuer: "test.com",
|
||||
account: "account.com",
|
||||
});
|
||||
});
|
||||
|
||||
it("should return valid config when m.authentication account is missing", () => {
|
||||
const wk = {
|
||||
...baseWk,
|
||||
[M_AUTHENTICATION.stable!]: {
|
||||
issuer: "test.com",
|
||||
},
|
||||
};
|
||||
expect(validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!])).toEqual({
|
||||
issuer: "test.com",
|
||||
});
|
||||
});
|
||||
|
||||
it("should remove unexpected properties", () => {
|
||||
const wk = {
|
||||
...baseWk,
|
||||
[M_AUTHENTICATION.stable!]: {
|
||||
issuer: "test.com",
|
||||
somethingElse: "test",
|
||||
},
|
||||
};
|
||||
expect(validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!])).toEqual({
|
||||
issuer: "test.com",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateOIDCIssuerWellKnown", () => {
|
||||
const validWk: any = {
|
||||
authorization_endpoint: "https://test.org/authorize",
|
||||
@@ -125,6 +32,8 @@ describe("validateOIDCIssuerWellKnown", () => {
|
||||
response_types_supported: ["code"],
|
||||
grant_types_supported: ["authorization_code"],
|
||||
code_challenge_methods_supported: ["S256"],
|
||||
account_management_uri: "https://authorize.org/account",
|
||||
account_management_actions_supported: ["org.matrix.cross_signing_reset"],
|
||||
};
|
||||
beforeEach(() => {
|
||||
// stub to avoid console litter
|
||||
@@ -157,6 +66,8 @@ describe("validateOIDCIssuerWellKnown", () => {
|
||||
authorizationEndpoint: validWk.authorization_endpoint,
|
||||
tokenEndpoint: validWk.token_endpoint,
|
||||
registrationEndpoint: validWk.registration_endpoint,
|
||||
accountManagementActionsSupported: ["org.matrix.cross_signing_reset"],
|
||||
accountManagementEndpoint: "https://authorize.org/account",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -167,6 +78,8 @@ describe("validateOIDCIssuerWellKnown", () => {
|
||||
authorizationEndpoint: validWk.authorization_endpoint,
|
||||
tokenEndpoint: validWk.token_endpoint,
|
||||
registrationEndpoint: undefined,
|
||||
accountManagementActionsSupported: ["org.matrix.cross_signing_reset"],
|
||||
accountManagementEndpoint: "https://authorize.org/account",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -186,6 +99,8 @@ describe("validateOIDCIssuerWellKnown", () => {
|
||||
["code_challenge_methods_supported", undefined],
|
||||
["code_challenge_methods_supported", "not an array"],
|
||||
["code_challenge_methods_supported", ["doesnt include S256"]],
|
||||
["account_management_uri", { not: "a string" }],
|
||||
["account_management_actions_supported", { not: "an array" }],
|
||||
])("should throw OP support error when %s is %s", (key, value) => {
|
||||
const wk = {
|
||||
...validWk,
|
||||
|
||||
+462
-157
@@ -1,8 +1,35 @@
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { IActionsObject, PushProcessor } from "../../src/pushprocessor";
|
||||
import { ConditionKind, EventType, IContent, MatrixClient, MatrixEvent, PushRuleActionName, RuleId } from "../../src";
|
||||
import {
|
||||
ConditionKind,
|
||||
EventType,
|
||||
IContent,
|
||||
IPushRule,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
PushRuleActionName,
|
||||
RuleId,
|
||||
TweakName,
|
||||
} from "../../src";
|
||||
import { mockClientMethodsUser } from "../test-utils/client";
|
||||
|
||||
const msc3914RoomCallRule: IPushRule = {
|
||||
rule_id: ".org.matrix.msc3914.rule.room.call",
|
||||
default: true,
|
||||
enabled: true,
|
||||
conditions: [
|
||||
{
|
||||
kind: ConditionKind.EventMatch,
|
||||
key: "type",
|
||||
pattern: "org.matrix.msc3401.call",
|
||||
},
|
||||
{
|
||||
kind: ConditionKind.CallStarted,
|
||||
},
|
||||
],
|
||||
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Sound, value: "default" }],
|
||||
};
|
||||
|
||||
describe("NotificationService", function () {
|
||||
const testUserId = "@ali:matrix.org";
|
||||
const testDisplayName = "Alice M";
|
||||
@@ -12,164 +39,150 @@ describe("NotificationService", function () {
|
||||
|
||||
let pushProcessor: PushProcessor;
|
||||
|
||||
const msc3914RoomCallRule = {
|
||||
rule_id: ".org.matrix.msc3914.rule.room.call",
|
||||
default: true,
|
||||
enabled: true,
|
||||
conditions: [
|
||||
{
|
||||
kind: "event_match",
|
||||
key: "type",
|
||||
pattern: "org.matrix.msc3401.call",
|
||||
},
|
||||
{
|
||||
kind: "call_started",
|
||||
},
|
||||
],
|
||||
actions: ["notify", { set_tweak: "sound", value: "default" }],
|
||||
};
|
||||
|
||||
// These would be better if individual rules were configured in the tests themselves.
|
||||
const matrixClient = {
|
||||
getRoom: function () {
|
||||
return {
|
||||
currentState: {
|
||||
getMember: function () {
|
||||
return {
|
||||
name: testDisplayName,
|
||||
};
|
||||
},
|
||||
getJoinedMemberCount: function () {
|
||||
return 0;
|
||||
},
|
||||
members: {},
|
||||
},
|
||||
};
|
||||
},
|
||||
...mockClientMethodsUser(testUserId),
|
||||
supportsIntentionalMentions: () => true,
|
||||
pushRules: {
|
||||
device: {},
|
||||
global: {
|
||||
content: [
|
||||
{
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "sound",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
pattern: "ali",
|
||||
rule_id: ".m.rule.contains_user_name",
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "sound",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
pattern: "coffee",
|
||||
rule_id: "coffee",
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "sound",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
pattern: "foo*bar",
|
||||
rule_id: "foobar",
|
||||
},
|
||||
],
|
||||
override: [
|
||||
{
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "sound",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
},
|
||||
],
|
||||
conditions: [
|
||||
{
|
||||
kind: "contains_display_name",
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
rule_id: ".m.rule.contains_display_name",
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "sound",
|
||||
value: "default",
|
||||
},
|
||||
],
|
||||
conditions: [
|
||||
{
|
||||
is: "2",
|
||||
kind: "room_member_count",
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
rule_id: ".m.rule.room_one_to_one",
|
||||
},
|
||||
],
|
||||
room: [],
|
||||
sender: [],
|
||||
underride: [
|
||||
msc3914RoomCallRule,
|
||||
{
|
||||
actions: ["dont-notify"],
|
||||
conditions: [
|
||||
{
|
||||
key: "content.msgtype",
|
||||
kind: "event_match",
|
||||
pattern: "m.notice",
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
rule_id: ".m.rule.suppress_notices",
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
conditions: [],
|
||||
enabled: true,
|
||||
rule_id: ".m.rule.fallback",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
} as unknown as MatrixClient;
|
||||
let matrixClient: MatrixClient;
|
||||
|
||||
beforeEach(function () {
|
||||
// These would be better if individual rules were configured in the tests themselves.
|
||||
matrixClient = {
|
||||
getRoom: function () {
|
||||
return {
|
||||
currentState: {
|
||||
getMember: function () {
|
||||
return {
|
||||
name: testDisplayName,
|
||||
};
|
||||
},
|
||||
getJoinedMemberCount: function () {
|
||||
return 0;
|
||||
},
|
||||
members: {},
|
||||
},
|
||||
};
|
||||
},
|
||||
...mockClientMethodsUser(testUserId),
|
||||
supportsIntentionalMentions: () => true,
|
||||
pushRules: {
|
||||
device: {},
|
||||
global: {
|
||||
content: [
|
||||
{
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "sound",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
pattern: "ali",
|
||||
rule_id: ".m.rule.contains_user_name",
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "sound",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
pattern: "coffee",
|
||||
rule_id: "coffee",
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "sound",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
pattern: "foo*bar",
|
||||
rule_id: "foobar",
|
||||
},
|
||||
],
|
||||
override: [
|
||||
{
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "sound",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
},
|
||||
],
|
||||
conditions: [
|
||||
{
|
||||
kind: "contains_display_name",
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
default: true,
|
||||
rule_id: ".m.rule.contains_display_name",
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "sound",
|
||||
value: "default",
|
||||
},
|
||||
],
|
||||
conditions: [
|
||||
{
|
||||
is: "2",
|
||||
kind: "room_member_count",
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
rule_id: ".m.rule.room_one_to_one",
|
||||
},
|
||||
],
|
||||
room: [],
|
||||
sender: [],
|
||||
underride: [
|
||||
msc3914RoomCallRule,
|
||||
{
|
||||
actions: ["dont-notify"],
|
||||
conditions: [
|
||||
{
|
||||
key: "content.msgtype",
|
||||
kind: "event_match",
|
||||
pattern: "m.notice",
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
rule_id: ".m.rule.suppress_notices",
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
conditions: [],
|
||||
enabled: true,
|
||||
rule_id: ".m.rule.fallback",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
} as unknown as MatrixClient;
|
||||
|
||||
testEvent = utils.mkEvent({
|
||||
type: "m.room.message",
|
||||
room: testRoomId,
|
||||
@@ -699,3 +712,295 @@ describe("Test PushProcessor.partsForDottedKey", function () {
|
||||
expect(PushProcessor.partsForDottedKey(path)).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rewriteDefaultRules", () => {
|
||||
it("should add default rules in the correct order", () => {
|
||||
const pushRules = PushProcessor.rewriteDefaultRules({
|
||||
device: {},
|
||||
global: {
|
||||
content: [],
|
||||
override: [
|
||||
// Include user-defined push rules inbetween .m.rule.master and other default rules to assert they are maintained in-order.
|
||||
{
|
||||
rule_id: ".m.rule.master",
|
||||
default: true,
|
||||
enabled: false,
|
||||
conditions: [],
|
||||
actions: [],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
PushRuleActionName.Notify,
|
||||
{
|
||||
set_tweak: TweakName.Sound,
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
set_tweak: TweakName.Highlight,
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
pattern: "coffee",
|
||||
rule_id: "coffee",
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
PushRuleActionName.Notify,
|
||||
{
|
||||
set_tweak: TweakName.Sound,
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
set_tweak: TweakName.Highlight,
|
||||
},
|
||||
],
|
||||
conditions: [
|
||||
{
|
||||
kind: ConditionKind.ContainsDisplayName,
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
default: true,
|
||||
rule_id: ".m.rule.contains_display_name",
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
PushRuleActionName.Notify,
|
||||
{
|
||||
set_tweak: TweakName.Sound,
|
||||
value: "default",
|
||||
},
|
||||
],
|
||||
conditions: [
|
||||
{
|
||||
is: "2",
|
||||
kind: ConditionKind.RoomMemberCount,
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
rule_id: ".m.rule.room_one_to_one",
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
room: [],
|
||||
sender: [],
|
||||
underride: [
|
||||
{
|
||||
actions: [
|
||||
PushRuleActionName.Notify,
|
||||
{
|
||||
set_tweak: TweakName.Highlight,
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
conditions: [],
|
||||
enabled: true,
|
||||
rule_id: "user-defined",
|
||||
default: false,
|
||||
},
|
||||
msc3914RoomCallRule,
|
||||
{
|
||||
actions: [
|
||||
PushRuleActionName.Notify,
|
||||
{
|
||||
set_tweak: TweakName.Highlight,
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
conditions: [],
|
||||
enabled: true,
|
||||
rule_id: ".m.rule.fallback",
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
// By the time we get here, we expect the PushProcessor to have merged the new .m.rule.is_room_mention rule into the existing list of rules.
|
||||
// Check that has happened, and that it is in the right place.
|
||||
const containsDisplayNameRuleIdx = pushRules.global.override?.findIndex(
|
||||
(rule) => rule.rule_id === RuleId.ContainsDisplayName,
|
||||
);
|
||||
expect(containsDisplayNameRuleIdx).toBeGreaterThan(-1);
|
||||
const isRoomMentionRuleIdx = pushRules.global.override?.findIndex(
|
||||
(rule) => rule.rule_id === RuleId.IsRoomMention,
|
||||
);
|
||||
expect(isRoomMentionRuleIdx).toBeGreaterThan(-1);
|
||||
const mReactionRuleIdx = pushRules.global.override?.findIndex((rule) => rule.rule_id === ".m.rule.reaction");
|
||||
expect(mReactionRuleIdx).toBeGreaterThan(-1);
|
||||
|
||||
expect(containsDisplayNameRuleIdx).toBeLessThan(isRoomMentionRuleIdx!);
|
||||
expect(isRoomMentionRuleIdx).toBeLessThan(mReactionRuleIdx!);
|
||||
|
||||
expect(pushRules.global.override?.map((r) => r.rule_id)).toEqual([
|
||||
".m.rule.master",
|
||||
"coffee",
|
||||
".m.rule.contains_display_name",
|
||||
".m.rule.room_one_to_one",
|
||||
".m.rule.is_room_mention",
|
||||
".m.rule.reaction",
|
||||
".org.matrix.msc3786.rule.room.server_acl",
|
||||
]);
|
||||
expect(pushRules.global.underride?.map((r) => r.rule_id)).toEqual([
|
||||
"user-defined",
|
||||
".org.matrix.msc3914.rule.room.call",
|
||||
// Assert that unknown default rules are maintained
|
||||
".m.rule.fallback",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should add missing msc3914 rule in correct place", () => {
|
||||
const pushRules = PushProcessor.rewriteDefaultRules({
|
||||
device: {},
|
||||
global: {
|
||||
// Sample push rules from a Synapse user.
|
||||
// Note that rules 2 and 3 are backwards, this will trigger a warning in the console.
|
||||
underride: [
|
||||
{
|
||||
conditions: [
|
||||
{
|
||||
kind: "event_match",
|
||||
key: "type",
|
||||
pattern: "m.call.invite",
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "sound",
|
||||
value: "ring",
|
||||
},
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
rule_id: ".m.rule.call",
|
||||
default: true,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
conditions: [
|
||||
{
|
||||
kind: "event_match",
|
||||
key: "type",
|
||||
pattern: "m.room.message",
|
||||
},
|
||||
{
|
||||
kind: "room_member_count",
|
||||
is: "2",
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "sound",
|
||||
value: "TEST1",
|
||||
},
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
rule_id: ".m.rule.room_one_to_one",
|
||||
default: true,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
conditions: [
|
||||
{
|
||||
kind: "event_match",
|
||||
key: "type",
|
||||
pattern: "m.room.encrypted",
|
||||
},
|
||||
{
|
||||
kind: "room_member_count",
|
||||
is: "2",
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "sound",
|
||||
value: "TEST2",
|
||||
},
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
rule_id: ".m.rule.encrypted_room_one_to_one",
|
||||
default: true,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
conditions: [
|
||||
{
|
||||
kind: "event_match",
|
||||
key: "type",
|
||||
pattern: "m.room.message",
|
||||
},
|
||||
],
|
||||
actions: ["dont_notify"],
|
||||
rule_id: ".m.rule.message",
|
||||
default: true,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
conditions: [
|
||||
{
|
||||
kind: "event_match",
|
||||
key: "type",
|
||||
pattern: "m.room.encrypted",
|
||||
},
|
||||
],
|
||||
actions: ["dont_notify"],
|
||||
rule_id: ".m.rule.encrypted",
|
||||
default: true,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
conditions: [
|
||||
{
|
||||
kind: "event_match",
|
||||
key: "type",
|
||||
pattern: "im.vector.modular.widgets",
|
||||
},
|
||||
{
|
||||
kind: "event_match",
|
||||
key: "content.type",
|
||||
pattern: "jitsi",
|
||||
},
|
||||
{
|
||||
kind: "event_match",
|
||||
key: "state_key",
|
||||
pattern: "*",
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
rule_id: ".im.vector.jitsi",
|
||||
default: true,
|
||||
enabled: true,
|
||||
},
|
||||
] as IPushRule[],
|
||||
},
|
||||
});
|
||||
|
||||
expect(pushRules.global.underride?.map((r) => r.rule_id)).toEqual([
|
||||
".m.rule.call",
|
||||
".org.matrix.msc3914.rule.room.call",
|
||||
".m.rule.room_one_to_one",
|
||||
".m.rule.encrypted_room_one_to_one",
|
||||
".m.rule.message",
|
||||
".m.rule.encrypted",
|
||||
".im.vector.jitsi",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -265,6 +265,7 @@ describe.each([[StoreType.Memory], [StoreType.IndexedDB]])("queueToDevice (%s st
|
||||
});
|
||||
const mockRoom = {
|
||||
updatePendingEvent: jest.fn(),
|
||||
hasEncryptionStateEvent: jest.fn().mockReturnValue(false),
|
||||
} as unknown as Room;
|
||||
client.resendEvent(dummyEvent, mockRoom);
|
||||
|
||||
|
||||
@@ -43,8 +43,10 @@ const THREAD_ID = "$thread_event_id";
|
||||
const ROOM_ID = "!123:matrix.org";
|
||||
|
||||
describe("Read receipt", () => {
|
||||
let threadRoot: MatrixEvent;
|
||||
let threadEvent: MatrixEvent;
|
||||
let roomEvent: MatrixEvent;
|
||||
let editOfThreadRoot: MatrixEvent;
|
||||
|
||||
beforeEach(() => {
|
||||
httpBackend = new MockHttpBackend();
|
||||
@@ -57,6 +59,15 @@ describe("Read receipt", () => {
|
||||
client.isGuest = () => false;
|
||||
client.supportsThreads = () => true;
|
||||
|
||||
threadRoot = utils.mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
user: "@bob:matrix.org",
|
||||
room: ROOM_ID,
|
||||
content: { body: "This is the thread root" },
|
||||
});
|
||||
threadRoot.event.event_id = THREAD_ID;
|
||||
|
||||
threadEvent = utils.mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
@@ -82,6 +93,9 @@ describe("Read receipt", () => {
|
||||
body: "Hello from a room",
|
||||
},
|
||||
});
|
||||
|
||||
editOfThreadRoot = utils.mkEdit(threadRoot, client, "@bob:matrix.org", ROOM_ID);
|
||||
editOfThreadRoot.setThreadId(THREAD_ID);
|
||||
});
|
||||
|
||||
describe("sendReceipt", () => {
|
||||
@@ -208,6 +222,7 @@ describe("Read receipt", () => {
|
||||
it.each([
|
||||
{ getEvent: () => roomEvent, destinationId: MAIN_ROOM_TIMELINE },
|
||||
{ getEvent: () => threadEvent, destinationId: THREAD_ID },
|
||||
{ getEvent: () => editOfThreadRoot, destinationId: MAIN_ROOM_TIMELINE },
|
||||
])("adds the receipt to $destinationId", ({ getEvent, destinationId }) => {
|
||||
const event = getEvent();
|
||||
const userId = "@bob:example.org";
|
||||
|
||||
@@ -32,7 +32,10 @@ export class DummyTransport<D extends RendezvousTransportDetails, T> implements
|
||||
ready = false;
|
||||
cancelled = false;
|
||||
|
||||
constructor(private name: string, private mockDetails: D) {}
|
||||
constructor(
|
||||
private name: string,
|
||||
private mockDetails: D,
|
||||
) {}
|
||||
onCancelled?: RendezvousFailureListener;
|
||||
|
||||
details(): Promise<RendezvousTransportDetails> {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user