Compare commits
477 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3702ac56f4 | |||
| af4811b327 | |||
| d4601d9910 | |||
| 2ced5e1aa4 | |||
| 45e19e51c1 | |||
| 3f1c3392d4 | |||
| f86f67f5a5 | |||
| 4dcf54f448 | |||
| d43e664594 | |||
| 0e322848f9 | |||
| b454318684 | |||
| 692f1d49b9 | |||
| b40cf75c9d | |||
| ba6a001d67 | |||
| d0c71ec516 | |||
| 67f343d6f0 | |||
| a7f0ba97cd | |||
| 54d11e1745 | |||
| 14744fd4dc | |||
| 9b0919350c | |||
| b53ad2c081 | |||
| 6222d238e4 | |||
| c6ee258789 | |||
| a584324a0d | |||
| 059b07cfa0 | |||
| b628cabe58 | |||
| b7d925f5ec | |||
| 47b729f085 | |||
| 05d980608a | |||
| 1c901e3137 | |||
| bd4589fcc4 | |||
| 0fbd0b3685 | |||
| 1646ea05bc | |||
| 885ec1fc73 | |||
| df2b65f111 | |||
| f09853ccb1 | |||
| 6c543382e6 | |||
| 52932f59ab | |||
| 4f63ff21ea | |||
| c8dc71eb69 | |||
| 2dda837db6 | |||
| fff4cdab7c | |||
| 4f00566b9f | |||
| c1a3b95073 | |||
| 38adbaf923 | |||
| 9459a95134 | |||
| 433b7afd71 | |||
| 777cf1f135 | |||
| 8235b65d71 | |||
| 4a33e584b0 | |||
| 6ee185e93f | |||
| 5df9705bae | |||
| 76458d3a40 | |||
| 27bb79a29a | |||
| 82d942dcc5 | |||
| ce6d0e2cb1 | |||
| db49cd8d13 | |||
| 42b08eca57 | |||
| a92c148f15 | |||
| 6cd60e32dc | |||
| b6633ad4b0 | |||
| e4dd7bcc87 | |||
| b6e97fcecb | |||
| dee2b60c3d | |||
| a07fe44565 | |||
| 0a35f2e2c7 | |||
| 32d535c2b1 | |||
| 7fb313c17c | |||
| d8f6449422 | |||
| c043e36f50 | |||
| e6524239bd | |||
| daed4b9dcc | |||
| 135d2da143 | |||
| fef53be5b4 | |||
| 7ec726e10b | |||
| 476f6f78b1 | |||
| cb8123dec7 | |||
| 6729c7d421 | |||
| 99af67a963 | |||
| b77f5a5598 | |||
| 76377c7cc4 | |||
| 7ddd198df8 | |||
| 81681f4090 | |||
| 52830a2a50 | |||
| 9fa6616052 | |||
| 94072a096d | |||
| 1f3ae4bde2 | |||
| 646b3a69fe | |||
| 6c475d9b54 | |||
| b9cccf9109 | |||
| f0d4ef7f99 | |||
| 068fbb7660 | |||
| 849e3d67c2 | |||
| 4c6e1e5c21 | |||
| 9ff6b357fc | |||
| 77ef8558bd | |||
| d979302e9b | |||
| dbdaa1540a | |||
| b44787192d | |||
| 6f2390a765 | |||
| dddc0aeccb | |||
| 9f6b42d3ae | |||
| 13c751c060 | |||
| 2e56c34df0 | |||
| 87115d181d | |||
| 5679c86ca6 | |||
| 4cd50e4871 | |||
| 0f1012278a | |||
| 0d211dfbad | |||
| 384116c8f5 | |||
| c374ba2367 | |||
| 9f2f08dfd3 | |||
| 4b3e6939d6 | |||
| f2ae3bc8ef | |||
| 11cc30f345 | |||
| 24a9562b07 | |||
| 8f10c0d921 | |||
| 450ff00c3e | |||
| b4ab7fc0b3 | |||
| 35f697a04b | |||
| 193d8a429a | |||
| 8cd5aac128 | |||
| 9bdeea0a8d | |||
| ade2e81d3d | |||
| 2fe434f3ae | |||
| eddd0cafe8 | |||
| 3a6561af36 | |||
| 403286cb81 | |||
| 219eab9139 | |||
| a12e6185f9 | |||
| d9eac57e9c | |||
| 9a9009d838 | |||
| 6f729ad7fd | |||
| cd33bafa04 | |||
| 867a0ca7ee | |||
| fdbbd9bca4 | |||
| bf1137fc58 | |||
| 5a0787349d | |||
| c57c8978cf | |||
| 508bb5841c | |||
| 35227e3a75 | |||
| 620a8d9c7f | |||
| 17e16b9b1a | |||
| 671dedca1c | |||
| 2464a691ef | |||
| d548b04d06 | |||
| 7ffdf17213 | |||
| 1c3dd0e51e | |||
| 0231d40277 | |||
| b1e9f39d65 | |||
| dfe535bc07 | |||
| 3c33c422e6 | |||
| d521f97411 | |||
| c0a5299704 | |||
| ce3b72c850 | |||
| 935517746a | |||
| e48d919cd4 | |||
| ab39ee37d6 | |||
| af6f9d49f4 | |||
| a2981efac3 | |||
| 4625ed73cf | |||
| 6f7a72d69e | |||
| 2a0ffe1223 | |||
| 72a6ec0dd3 | |||
| 72b89fde6e | |||
| c400dd4ff8 | |||
| f41b7706e4 | |||
| de694459be | |||
| 6fc9827b10 | |||
| f52c5eb667 | |||
| c05cb3ad2b | |||
| 586a313c8d | |||
| c605310b87 | |||
| 41cee6f1cc | |||
| 3e1e99f8e5 | |||
| 276849f068 | |||
| 37118991f5 | |||
| 00629e6dc9 | |||
| 02f6a09bcf | |||
| 36a6117ee2 | |||
| aebe26db96 | |||
| 60e175a0e0 | |||
| d950cda05c | |||
| 83c848093f | |||
| fa6f70f708 | |||
| 98d119d6e1 | |||
| aca51fd8a3 | |||
| c78631bdee | |||
| 0d6a93b5f6 | |||
| 40ecfa7932 | |||
| d656b848f8 | |||
| 0981652de4 | |||
| db32420d16 | |||
| d5b82e343a | |||
| 965f4fb13b | |||
| 9e1b126854 | |||
| c527f85fb1 | |||
| 4a294c9dd3 | |||
| be94f5ea93 | |||
| 5f9369abee | |||
| e7a7ec0673 | |||
| 92cd84fc0c | |||
| 45e56f8cc3 | |||
| e95947dc73 | |||
| 448a5c9a77 | |||
| 9589a97952 | |||
| 2566c40e96 | |||
| 099cac0162 | |||
| e4cf5b26ee | |||
| c698317f3f | |||
| e8f682f452 | |||
| 020743141b | |||
| 5f5a9b1a43 | |||
| 3334c01191 | |||
| 0b8de251bf | |||
| 88ce017333 | |||
| 471f174889 | |||
| c0dacb5037 | |||
| 2cc51e0db7 | |||
| 22c5999fed | |||
| b711781f16 | |||
| 8ba2d257ae | |||
| 9e2e144530 | |||
| 38a6949e5d | |||
| e876482e62 | |||
| 544b1c6742 | |||
| 984dd26a13 | |||
| bdb91b3806 | |||
| 9a15094374 | |||
| e980c88901 | |||
| 6ea2885796 | |||
| ca5ac79927 | |||
| f9672cf307 | |||
| e7493fd417 | |||
| f553854730 | |||
| c89bbf4bf5 | |||
| ebcb26f1b3 | |||
| 5b4263bf55 | |||
| df9ffdc408 | |||
| 70449ea003 | |||
| 9192b876d2 | |||
| 04d0d61a0e | |||
| 404f8e130e | |||
| b97b862fb6 | |||
| 5e766978b8 | |||
| 34ef7bc64a | |||
| 18e2052af2 | |||
| aa0d3bd1f5 | |||
| 942a28ddf6 | |||
| 87791cd391 | |||
| 38e54ae7f2 | |||
| acef1d7dd0 | |||
| da615fd512 | |||
| f4f05550ef | |||
| f475251ddd | |||
| 83f61c96f3 | |||
| 85a6a552b5 | |||
| 9702e8a5fa | |||
| d82c041b99 | |||
| 8d9cd0fcb3 | |||
| 96ba061732 | |||
| ee4cbd1ec9 | |||
| 2a0dc39eec | |||
| 6e25b13312 | |||
| 94c5e37570 | |||
| 09fee4a2d9 | |||
| 49994ac4fc | |||
| e68cabc70e | |||
| c819ac634f | |||
| 17f5ab4191 | |||
| e270f075a4 | |||
| 0ef6c2e35f | |||
| 7a249e3ef5 | |||
| 353d6bab47 | |||
| 7f21f569d5 | |||
| fa5eae70dd | |||
| 3db056ad3e | |||
| a2a127d9a4 | |||
| d12bccd211 | |||
| d8e597ccdf | |||
| c801690e28 | |||
| b4fe00a3a8 | |||
| d42e2fe2c0 | |||
| 4a4465b9fc | |||
| 1a78301adb | |||
| bbf7020755 | |||
| 592fb0cf10 | |||
| 015eb5d5c4 | |||
| 42fef0e7aa | |||
| 28f3169a28 | |||
| d8285aad00 | |||
| eeacf8c22c | |||
| ee995cb39b | |||
| 7529af43e4 | |||
| 3fac6d7180 | |||
| 487bfc88ef | |||
| c91617a799 | |||
| 87bf115967 | |||
| 18bb5c3079 | |||
| f3f9e41787 | |||
| 7993dd7630 | |||
| bef557976b | |||
| 549f9b7e29 | |||
| 06d9d6207c | |||
| e336aceaba | |||
| fcc4b71f06 | |||
| d1a62eddfc | |||
| ffbd10a7b8 | |||
| d0e37ee323 | |||
| 96ef535ebb | |||
| 0683133d5b | |||
| 64c3ac55a4 | |||
| 5f06df8a87 | |||
| 3291846714 | |||
| 139904f297 | |||
| c2fe2ab270 | |||
| 4e26f29032 | |||
| 31391121dc | |||
| 7d48a8394d | |||
| 28da62c01c | |||
| e880cece93 | |||
| 97e8fcea75 | |||
| f28cb48fe1 | |||
| a2e255c2c9 | |||
| 74c5a20371 | |||
| 4b87907b92 | |||
| f76f708c96 | |||
| 17f7dc5463 | |||
| b253ad9e81 | |||
| c1f56ba3c4 | |||
| 7998817f7e | |||
| bdc12a2544 | |||
| 5a92597abd | |||
| 6f695c1b82 | |||
| d99428f2c1 | |||
| 4c9648a23b | |||
| 8c5f88c4a7 | |||
| 923e9c4ada | |||
| 13d62e71b6 | |||
| 32aca09f47 | |||
| 067ac62271 | |||
| 841e6e999d | |||
| a48546f60d | |||
| 2f09e9641c | |||
| f46355e7c0 | |||
| 53397ee0d1 | |||
| 5a83635ef5 | |||
| 56c0c9be4d | |||
| 24406d2411 | |||
| aeeed6ecd7 | |||
| 9f3f9990ef | |||
| 119ce2e46f | |||
| fc8a867e8e | |||
| b4d8c0b603 | |||
| 3b0d1b2696 | |||
| 5110e0b91e | |||
| 305de54106 | |||
| 0555f9db1c | |||
| 159e825877 | |||
| 8131b3900d | |||
| 431d7a0933 | |||
| e9b52e23d2 | |||
| 0148ad0766 | |||
| 213f1134b6 | |||
| 50e6a8f6b1 | |||
| 4a82e1bf05 | |||
| 843973c4da | |||
| debeb66d6f | |||
| 015d0f9135 | |||
| 5c8e7f2be0 | |||
| 411b5f111c | |||
| 2d231c0ae2 | |||
| ec37eb8b6f | |||
| 1cdcebb5db | |||
| a0f6eea363 | |||
| 18b1a44df7 | |||
| 4b6b1599a2 | |||
| a582b19435 | |||
| 4a8c3d273f | |||
| 8dc608d917 | |||
| 7ef38ed1b2 | |||
| 593f62c1c4 | |||
| 04d674b8c7 | |||
| 27eb88f4a1 | |||
| 1409a4f814 | |||
| 8232896c85 | |||
| e2ed80ffa0 | |||
| 8ac3841a2f | |||
| ba57736bf6 | |||
| 8be4ca909e | |||
| 0d964523a9 | |||
| bb504bc001 | |||
| 326aec9f9e | |||
| 688327dab5 | |||
| 3f4522ba88 | |||
| 625983a2b2 | |||
| 137fd2bd40 | |||
| 1e65bfd316 | |||
| 5da072712d | |||
| 529d61b5f4 | |||
| 5111ca622a | |||
| f627507b86 | |||
| aee4459201 | |||
| 1a824750dd | |||
| 73cb5e1ee9 | |||
| 96bde1f706 | |||
| 5251dcf67f | |||
| ce0b0ea182 | |||
| 7a142e9102 | |||
| f85aa44f28 | |||
| efbf252e22 | |||
| d873f14b6d | |||
| cf1ba12232 | |||
| df208e4de8 | |||
| d8ef7f9f63 | |||
| 2515ba31a0 | |||
| 715c4577d0 | |||
| a2f23900c9 | |||
| e9e65cf484 | |||
| 205c80ea28 | |||
| 678023717b | |||
| b535969845 | |||
| 027bc6bfc9 | |||
| 71ca424712 | |||
| 3280394bf9 | |||
| fc07530434 | |||
| f592d4dbc5 | |||
| 96f48929ac | |||
| 454da84f6e | |||
| 89bda6c2e5 | |||
| ac70dcfc91 | |||
| 9c7cb3cbea | |||
| d8d7bd548f | |||
| 55ef57ead8 | |||
| 9996afed03 | |||
| 61a80a11c9 | |||
| 6a8e8ed0a6 | |||
| 5895ce32fa | |||
| fe0a268991 | |||
| 7f189b0abd | |||
| 6e07c9e900 | |||
| bbeea51a36 | |||
| 151b54ed65 | |||
| 18986cb33a | |||
| aef5d73de4 | |||
| e4fc1f3628 | |||
| 8b1c173659 | |||
| f0916f14d1 | |||
| a291f5ab05 | |||
| 2d7e07f4ed | |||
| 2427f75f98 | |||
| d25fb71eba | |||
| c81b9d2fd9 | |||
| fb3ca90bc9 | |||
| eb2a47623f | |||
| f18d8ead08 | |||
| 2da14bd6e9 | |||
| 1dbb776e12 | |||
| 07b2c57064 | |||
| 7021f70a66 | |||
| 503e954671 | |||
| 2add1fcbcb | |||
| 4fe115b2c4 | |||
| 60e168806d | |||
| 03dfab1282 | |||
| 19302ea4fb | |||
| d5aaed67ba | |||
| 8fe6afd9ab | |||
| 782fbb115f | |||
| 3971bf34ed | |||
| 6dac6e53f7 | |||
| 7ec84e92a0 | |||
| 154e5c45a6 | |||
| 2cd5c813ac | |||
| 1c5101aa1a | |||
| 76f11bee9e | |||
| 91f409e8f4 |
+7
-7
@@ -20,13 +20,13 @@ module.exports = {
|
||||
// NOTE: These rules are frozen and new rules should not be added here.
|
||||
// New changes belong in https://github.com/matrix-org/eslint-plugin-matrix-org/
|
||||
rules: {
|
||||
"no-var": ["warn"],
|
||||
"prefer-rest-params": ["warn"],
|
||||
"prefer-spread": ["warn"],
|
||||
"one-var": ["warn"],
|
||||
"padded-blocks": ["warn"],
|
||||
"no-extend-native": ["warn"],
|
||||
"camelcase": ["warn"],
|
||||
"no-var": ["error"],
|
||||
"prefer-rest-params": ["error"],
|
||||
"prefer-spread": ["error"],
|
||||
"one-var": ["error"],
|
||||
"padded-blocks": ["error"],
|
||||
"no-extend-native": ["error"],
|
||||
"camelcase": ["error"],
|
||||
"no-multi-spaces": ["error", { "ignoreEOLComments": true }],
|
||||
"space-before-function-paren": ["error", {
|
||||
"anonymous": "never",
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
* @matrix-org/element-web
|
||||
|
||||
/src/webrtc @matrix-org/element-call-reviewers
|
||||
/spec/*/webrtc @matrix-org/element-call-reviewers
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
name: Deploy documentation PR preview
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ "Static Analysis" ]
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
netlify:
|
||||
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@b12b127cf24433d14b4f93cee62f5465076ba82a # v2.24.1
|
||||
with:
|
||||
workflow: static_analysis.yml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: docs
|
||||
path: docs
|
||||
|
||||
- name: 📤 Deploy to Netlify
|
||||
uses: matrix-org/netlify-pr-preview@v1
|
||||
with:
|
||||
path: docs
|
||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
revision: ${{ github.event.workflow_run.head_sha }}
|
||||
token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
site_id: ${{ secrets.NETLIFY_SITE_ID }}
|
||||
desc: Documentation preview
|
||||
deployment_env: PR Documentation Preview
|
||||
@@ -24,10 +24,7 @@ jobs:
|
||||
|
||||
- name: 📋 Copy to temp
|
||||
run: |
|
||||
tag="${{ github.ref_name }}"
|
||||
version="${tag#v}"
|
||||
echo "VERSION=$version" >> $GITHUB_ENV
|
||||
cp -a "./.jsdoc/matrix-js-sdk/$version" $RUNNER_TEMP
|
||||
cp -a "./_docs" "$RUNNER_TEMP/"
|
||||
|
||||
- name: 🧮 Checkout gh-pages
|
||||
uses: actions/checkout@v3
|
||||
@@ -36,7 +33,10 @@ jobs:
|
||||
|
||||
- name: 🔪 Prepare
|
||||
run: |
|
||||
cp -a "$RUNNER_TEMP/$VERSION" .
|
||||
tag="${{ github.ref_name }}"
|
||||
VERSION="${tag#v}"
|
||||
[ ! -e "$VERSION" ] || rm -r $VERSION
|
||||
cp -r $RUNNER_TEMP/docs/ $VERSION
|
||||
|
||||
# Add the new directory to the index if it isn't there already
|
||||
if ! grep -q ">Version $VERSION</a>" index.html; then
|
||||
|
||||
@@ -5,6 +5,11 @@ on:
|
||||
secrets:
|
||||
SONAR_TOKEN:
|
||||
required: true
|
||||
inputs:
|
||||
extra_args:
|
||||
type: string
|
||||
required: false
|
||||
description: "Extra args to pass to SonarCloud"
|
||||
jobs:
|
||||
sonarqube:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -22,7 +27,7 @@ jobs:
|
||||
|
||||
- name: "🩻 SonarCloud Scan"
|
||||
id: sonarcloud
|
||||
uses: matrix-org/sonarcloud-workflow-action@v2.2
|
||||
uses: matrix-org/sonarcloud-workflow-action@v2.3
|
||||
with:
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
@@ -33,8 +38,8 @@ jobs:
|
||||
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@v1
|
||||
if: always()
|
||||
with:
|
||||
|
||||
@@ -8,8 +8,36 @@ 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
|
||||
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@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
|
||||
needs: prepare
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
|
||||
secrets:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
with:
|
||||
extra_args: -Dsonar.javascript.lcov.reportPaths=${{ needs.prepare.outputs.reportPaths }} -Dsonar.testExecutionReportPaths=${{ needs.prepare.outputs.testExecutionReportPaths }}
|
||||
|
||||
@@ -64,38 +64,11 @@ jobs:
|
||||
|
||||
- name: Generate Docs
|
||||
run: "yarn run gendoc"
|
||||
|
||||
tsc-strict:
|
||||
name: Typescript Strict Error Checker
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
checks: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Get diff lines
|
||||
id: diff
|
||||
uses: Equip-Collaboration/diff-line-numbers@v1.0.0
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
include: '["\\.tsx?$"]'
|
||||
|
||||
- name: Detecting files changed
|
||||
id: files
|
||||
uses: futuratrepadeira/changed-files@v4.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pattern: '^.*\.tsx?$'
|
||||
|
||||
- uses: t3chguy/typescript-check-action@main
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
use-check: false
|
||||
check-fail-mode: added
|
||||
output-behaviour: annotate
|
||||
ts-extra-args: '--strict'
|
||||
files-changed: ${{ steps.files.outputs.files_updated }}
|
||||
files-added: ${{ steps.files.outputs.files_created }}
|
||||
files-deleted: ${{ steps.files.outputs.files_deleted }}
|
||||
line-numbers: ${{ steps.diff.outputs.lineNumbers }}
|
||||
name: docs
|
||||
path: _docs
|
||||
# We'll only use this in a workflow_run, then we're done with it
|
||||
retention-days: 1
|
||||
|
||||
@@ -8,25 +8,40 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
jest:
|
||||
name: Jest
|
||||
name: 'Jest [${{ matrix.specs }}] (Node ${{ matrix.node }})'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
matrix:
|
||||
specs: [browserify, integ, unit]
|
||||
node: [16, 18, latest]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Yarn cache
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: "yarn install"
|
||||
|
||||
- name: Build
|
||||
if: matrix.specs == 'browserify'
|
||||
run: "yarn build"
|
||||
|
||||
- name: Get number of CPU cores
|
||||
id: cpu-cores
|
||||
uses: SimenB/github-actions-cpu-cores@v1
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: "yarn coverage --ci --reporters github-actions"
|
||||
run: |
|
||||
yarn coverage --ci --reporters github-actions --max-workers ${{ steps.cpu-cores.outputs.count }} ./spec/${{ matrix.specs }}
|
||||
mv coverage/lcov.info coverage/${{ matrix.node }}-${{ matrix.specs }}.lcov.info
|
||||
env:
|
||||
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/.jsdocbuild
|
||||
/.jsdoc
|
||||
/_docs
|
||||
.DS_Store
|
||||
|
||||
node_modules
|
||||
/.npmrc
|
||||
|
||||
@@ -1,3 +1,76 @@
|
||||
Changes in [21.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v21.2.0) (2022-11-22)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Make calls go back to 'connecting' state when media lost ([\#2880](https://github.com/matrix-org/matrix-js-sdk/pull/2880)).
|
||||
* Add ability to send unthreaded receipt ([\#2878](https://github.com/matrix-org/matrix-js-sdk/pull/2878)).
|
||||
* Add way to abort search requests ([\#2877](https://github.com/matrix-org/matrix-js-sdk/pull/2877)).
|
||||
* sliding sync: add custom room subscriptions support ([\#2834](https://github.com/matrix-org/matrix-js-sdk/pull/2834)).
|
||||
* webrtc: add advanced audio settings ([\#2434](https://github.com/matrix-org/matrix-js-sdk/pull/2434)). Contributed by @MrAnno.
|
||||
* Add support for group calls using MSC3401 ([\#2553](https://github.com/matrix-org/matrix-js-sdk/pull/2553)).
|
||||
* Make the js-sdk conform to tsc --strict ([\#2835](https://github.com/matrix-org/matrix-js-sdk/pull/2835)). Fixes #2112 #2116 and #2124.
|
||||
* Let leave requests outlive the window ([\#2815](https://github.com/matrix-org/matrix-js-sdk/pull/2815)). Fixes vector-im/element-call#639.
|
||||
* Add event and message capabilities to RoomWidgetClient ([\#2797](https://github.com/matrix-org/matrix-js-sdk/pull/2797)).
|
||||
* Misc fixes for group call widgets ([\#2657](https://github.com/matrix-org/matrix-js-sdk/pull/2657)).
|
||||
* Support nested Matrix clients via the widget API ([\#2473](https://github.com/matrix-org/matrix-js-sdk/pull/2473)).
|
||||
* Set max average bitrate on PTT calls ([\#2499](https://github.com/matrix-org/matrix-js-sdk/pull/2499)). Fixes vector-im/element-call#440.
|
||||
* Add config option for e2e group call signalling ([\#2492](https://github.com/matrix-org/matrix-js-sdk/pull/2492)).
|
||||
* Enable DTX on audio tracks in calls ([\#2482](https://github.com/matrix-org/matrix-js-sdk/pull/2482)).
|
||||
* Don't ignore call member events with a distant future expiration date ([\#2466](https://github.com/matrix-org/matrix-js-sdk/pull/2466)).
|
||||
* Expire call member state events after 1 hour ([\#2446](https://github.com/matrix-org/matrix-js-sdk/pull/2446)).
|
||||
* Emit unknown device errors for group call participants without e2e ([\#2447](https://github.com/matrix-org/matrix-js-sdk/pull/2447)).
|
||||
* Mute disconnected peers in PTT mode ([\#2421](https://github.com/matrix-org/matrix-js-sdk/pull/2421)).
|
||||
* Add support for sending encrypted to-device events with OLM ([\#2322](https://github.com/matrix-org/matrix-js-sdk/pull/2322)). Contributed by @robertlong.
|
||||
* Support for PTT group call mode ([\#2338](https://github.com/matrix-org/matrix-js-sdk/pull/2338)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix registration add phone number not working ([\#2876](https://github.com/matrix-org/matrix-js-sdk/pull/2876)). Contributed by @bagvand.
|
||||
* Use an underride rule for Element Call notifications ([\#2873](https://github.com/matrix-org/matrix-js-sdk/pull/2873)). Fixes vector-im/element-web#23691.
|
||||
* Fixes unwanted highlight notifications with encrypted threads ([\#2862](https://github.com/matrix-org/matrix-js-sdk/pull/2862)).
|
||||
* Extra insurance that we don't mix events in the wrong timelines - v2 ([\#2856](https://github.com/matrix-org/matrix-js-sdk/pull/2856)). Contributed by @MadLittleMods.
|
||||
* Hide pending events in thread timelines ([\#2843](https://github.com/matrix-org/matrix-js-sdk/pull/2843)). Fixes vector-im/element-web#23684.
|
||||
* Fix pagination token tracking for mixed room timelines ([\#2855](https://github.com/matrix-org/matrix-js-sdk/pull/2855)). Fixes vector-im/element-web#23695.
|
||||
* Extra insurance that we don't mix events in the wrong timelines ([\#2848](https://github.com/matrix-org/matrix-js-sdk/pull/2848)). Contributed by @MadLittleMods.
|
||||
* Do not freeze state in `initialiseState()` ([\#2846](https://github.com/matrix-org/matrix-js-sdk/pull/2846)).
|
||||
* Don't remove our own member for a split second when entering a call ([\#2844](https://github.com/matrix-org/matrix-js-sdk/pull/2844)).
|
||||
* Resolve races between `initLocalCallFeed` and `leave` ([\#2826](https://github.com/matrix-org/matrix-js-sdk/pull/2826)).
|
||||
* Add throwOnFail to groupCall.setScreensharingEnabled ([\#2787](https://github.com/matrix-org/matrix-js-sdk/pull/2787)).
|
||||
* Fix connectivity regressions ([\#2780](https://github.com/matrix-org/matrix-js-sdk/pull/2780)).
|
||||
* Fix screenshare failing after several attempts ([\#2771](https://github.com/matrix-org/matrix-js-sdk/pull/2771)). Fixes vector-im/element-call#625.
|
||||
* Don't block muting/unmuting on network requests ([\#2754](https://github.com/matrix-org/matrix-js-sdk/pull/2754)). Fixes vector-im/element-call#592.
|
||||
* Fix ICE restarts ([\#2702](https://github.com/matrix-org/matrix-js-sdk/pull/2702)).
|
||||
* Target widget actions at a specific room ([\#2670](https://github.com/matrix-org/matrix-js-sdk/pull/2670)).
|
||||
* Add tests for ice candidate sending ([\#2674](https://github.com/matrix-org/matrix-js-sdk/pull/2674)).
|
||||
* Prevent exception when muting ([\#2667](https://github.com/matrix-org/matrix-js-sdk/pull/2667)). Fixes vector-im/element-call#578.
|
||||
* Fix race in creating calls ([\#2662](https://github.com/matrix-org/matrix-js-sdk/pull/2662)).
|
||||
* Add client.waitUntilRoomReadyForGroupCalls() ([\#2641](https://github.com/matrix-org/matrix-js-sdk/pull/2641)).
|
||||
* Wait for client to start syncing before making group calls ([\#2632](https://github.com/matrix-org/matrix-js-sdk/pull/2632)). Fixes #2589.
|
||||
* Add GroupCallEventHandlerEvent.Room ([\#2631](https://github.com/matrix-org/matrix-js-sdk/pull/2631)).
|
||||
* Add missing events from reemitter to GroupCall ([\#2527](https://github.com/matrix-org/matrix-js-sdk/pull/2527)). Contributed by @toger5.
|
||||
* Prevent double mute status changed events ([\#2502](https://github.com/matrix-org/matrix-js-sdk/pull/2502)).
|
||||
* Don't mute the remote side immediately in PTT calls ([\#2487](https://github.com/matrix-org/matrix-js-sdk/pull/2487)). Fixes vector-im/element-call#425.
|
||||
* Fix some MatrixCall leaks and use a shared AudioContext ([\#2484](https://github.com/matrix-org/matrix-js-sdk/pull/2484)). Fixes vector-im/element-call#412.
|
||||
* Don't block muting on determining whether the device exists ([\#2461](https://github.com/matrix-org/matrix-js-sdk/pull/2461)).
|
||||
* Only clone streams on Safari ([\#2450](https://github.com/matrix-org/matrix-js-sdk/pull/2450)). Fixes vector-im/element-call#267.
|
||||
* Set PTT mode on call correctly ([\#2445](https://github.com/matrix-org/matrix-js-sdk/pull/2445)). Fixes vector-im/element-call#382.
|
||||
* Wait for mute event to send in PTT mode ([\#2401](https://github.com/matrix-org/matrix-js-sdk/pull/2401)).
|
||||
* Handle other members having no e2e keys ([\#2383](https://github.com/matrix-org/matrix-js-sdk/pull/2383)). Fixes vector-im/element-call#338.
|
||||
* Fix races when muting/unmuting ([\#2370](https://github.com/matrix-org/matrix-js-sdk/pull/2370)).
|
||||
|
||||
Changes in [21.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v21.1.0) (2022-11-08)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Loading threads with server-side assistance ([\#2735](https://github.com/matrix-org/matrix-js-sdk/pull/2735)). Contributed by @justjanne.
|
||||
* Support sign in + E2EE set up using QR code implementing MSC3886, MSC3903 and MSC3906 ([\#2747](https://github.com/matrix-org/matrix-js-sdk/pull/2747)). Contributed by @hughns.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Replace `instanceof Array` with `Array.isArray` ([\#2812](https://github.com/matrix-org/matrix-js-sdk/pull/2812)). Fixes #2811.
|
||||
* Emit UnreadNotification event on notifications reset ([\#2804](https://github.com/matrix-org/matrix-js-sdk/pull/2804)). Fixes vector-im/element-web#23590.
|
||||
* Fix incorrect prevEv being sent in ClientEvent.AccountData events ([\#2794](https://github.com/matrix-org/matrix-js-sdk/pull/2794)).
|
||||
* Fix build error caused by wrong ts-strict improvements ([\#2783](https://github.com/matrix-org/matrix-js-sdk/pull/2783)). Contributed by @justjanne.
|
||||
* Encryption should not hinder verification ([\#2734](https://github.com/matrix-org/matrix-js-sdk/pull/2734)).
|
||||
|
||||
Changes in [21.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v21.0.1) (2022-11-01)
|
||||
==================================================================================================
|
||||
|
||||
|
||||
@@ -295,13 +295,13 @@ API Reference
|
||||
A hosted reference can be found at
|
||||
http://matrix-org.github.io/matrix-js-sdk/index.html
|
||||
|
||||
This SDK uses JSDoc3 style comments. You can manually build and
|
||||
This SDK uses [Typedoc](https://typedoc.org/guides/doccomments) doc comments. You can manually build and
|
||||
host the API reference from the source files like this:
|
||||
|
||||
```
|
||||
$ yarn gendoc
|
||||
$ cd .jsdoc
|
||||
$ python -m SimpleHTTPServer 8005
|
||||
$ cd _docs
|
||||
$ python -m http.server 8005
|
||||
```
|
||||
|
||||
Then visit ``http://localhost:8005`` to see the API docs.
|
||||
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"tags": {
|
||||
"allowUnknownTags": true
|
||||
},
|
||||
"plugins": [
|
||||
"node_modules/better-docs/category",
|
||||
"node_modules/better-docs/typescript"
|
||||
],
|
||||
"source": {
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"includePattern": ".(ts|js)$"
|
||||
},
|
||||
"opts": {
|
||||
"encoding": "utf8",
|
||||
"destination": ".jsdoc",
|
||||
"readme": "README.md",
|
||||
"recurse": true,
|
||||
"verbose": true,
|
||||
"template": "node_modules/docdash"
|
||||
},
|
||||
"docdash": {
|
||||
"static": true,
|
||||
"private": false,
|
||||
"search": true,
|
||||
"collapse": true,
|
||||
"typedefs": true
|
||||
}
|
||||
}
|
||||
+19
-12
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "21.0.1",
|
||||
"version": "21.2.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
@@ -16,7 +16,7 @@
|
||||
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
|
||||
"build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -p [ tsify -p ./tsconfig-build.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
|
||||
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
|
||||
"gendoc": "jsdoc -c jsdoc.json -P package.json",
|
||||
"gendoc": "typedoc",
|
||||
"lint": "yarn lint:types && yarn lint:js",
|
||||
"lint:js": "eslint --max-warnings 0 src spec",
|
||||
"lint:js-fix": "eslint --fix src spec",
|
||||
@@ -54,13 +54,16 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/sdp-transform": "^2.4.5",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^5.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
"loglevel": "^1.7.1",
|
||||
"matrix-events-sdk": "^0.0.1-beta.7",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-widget-api": "^1.0.0",
|
||||
"p-retry": "4",
|
||||
"qs": "^6.9.6",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"unhomoglyph": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -76,6 +79,7 @@
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/register": "^7.12.10",
|
||||
"@casualbot/jest-sonar-reporter": "^2.2.5",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.13.tgz",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/content-type": "^1.1.5",
|
||||
@@ -91,22 +95,24 @@
|
||||
"browserify": "^17.0.0",
|
||||
"docdash": "^1.2.0",
|
||||
"domexception": "^4.0.0",
|
||||
"eslint": "8.24.0",
|
||||
"eslint": "8.26.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-matrix-org": "^0.6.0",
|
||||
"eslint-plugin-matrix-org": "^0.7.0",
|
||||
"eslint-plugin-unicorn": "^44.0.2",
|
||||
"exorcist": "^2.0.0",
|
||||
"fake-indexeddb": "^4.0.0",
|
||||
"jest": "^29.0.0",
|
||||
"jest-environment-jsdom": "^28.1.3",
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jest-mock": "^27.5.1",
|
||||
"jest-sonar-reporter": "^2.0.0",
|
||||
"jsdoc": "^3.6.6",
|
||||
"jest-mock": "^29.0.0",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"terser": "^5.5.1",
|
||||
"tsify": "^5.0.2",
|
||||
"typedoc": "^0.23.20",
|
||||
"typedoc-plugin-missing-exports": "^1.0.0",
|
||||
"typescript": "^4.5.3"
|
||||
},
|
||||
"jest": {
|
||||
@@ -124,11 +130,12 @@
|
||||
"text-summary",
|
||||
"lcov"
|
||||
],
|
||||
"testResultsProcessor": "jest-sonar-reporter"
|
||||
"testResultsProcessor": "@casualbot/jest-sonar-reporter"
|
||||
},
|
||||
"jestSonar": {
|
||||
"reportPath": "coverage",
|
||||
"sonar56x": true
|
||||
"@casualbot/jest-sonar-reporter": {
|
||||
"outputDirectory": "coverage",
|
||||
"outputName": "jest-sonar-report.xml",
|
||||
"relativePaths": true
|
||||
},
|
||||
"typings": "./lib/index.d.ts"
|
||||
}
|
||||
|
||||
@@ -11,6 +11,6 @@ sonar.exclusions=docs,examples,git-hooks
|
||||
sonar.typescript.tsconfigPath=./tsconfig.json
|
||||
sonar.javascript.lcov.reportPaths=coverage/lcov.info
|
||||
sonar.coverage.exclusions=spec/**/*
|
||||
sonar.testExecutionReportPaths=coverage/test-report.xml
|
||||
sonar.testExecutionReportPaths=coverage/jest-sonar-report.xml
|
||||
|
||||
sonar.lang.patterns.ts=**/*.ts,**/*.tsx
|
||||
|
||||
+13
-13
@@ -38,8 +38,8 @@ import { IKeysUploadResponse, IUploadKeysRequest } from '../src/client';
|
||||
export class TestClient {
|
||||
public readonly httpBackend: MockHttpBackend;
|
||||
public readonly client: MatrixClient;
|
||||
public deviceKeys: IDeviceKeys;
|
||||
public oneTimeKeys: Record<string, IOneTimeKey>;
|
||||
public deviceKeys?: IDeviceKeys | null;
|
||||
public oneTimeKeys?: Record<string, IOneTimeKey>;
|
||||
|
||||
constructor(
|
||||
public readonly userId?: string,
|
||||
@@ -123,7 +123,7 @@ export class TestClient {
|
||||
|
||||
logger.log(this + ': received device keys');
|
||||
// we expect this to happen before any one-time keys are uploaded.
|
||||
expect(Object.keys(this.oneTimeKeys).length).toEqual(0);
|
||||
expect(Object.keys(this.oneTimeKeys!).length).toEqual(0);
|
||||
|
||||
this.deviceKeys = content.device_keys;
|
||||
return { one_time_key_counts: { signed_curve25519: 0 } };
|
||||
@@ -138,9 +138,9 @@ export class TestClient {
|
||||
* @returns {Promise} for the one-time keys
|
||||
*/
|
||||
public awaitOneTimeKeyUpload(): Promise<Record<string, IOneTimeKey>> {
|
||||
if (Object.keys(this.oneTimeKeys).length != 0) {
|
||||
if (Object.keys(this.oneTimeKeys!).length != 0) {
|
||||
// already got one-time keys
|
||||
return Promise.resolve(this.oneTimeKeys);
|
||||
return Promise.resolve(this.oneTimeKeys!);
|
||||
}
|
||||
|
||||
this.httpBackend.when("POST", "/keys/upload")
|
||||
@@ -148,7 +148,7 @@ export class TestClient {
|
||||
expect(content.device_keys).toBe(undefined);
|
||||
expect(content.one_time_keys).toBe(undefined);
|
||||
return { one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys).length,
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys!).length,
|
||||
} };
|
||||
});
|
||||
|
||||
@@ -158,17 +158,17 @@ export class TestClient {
|
||||
expect(content.one_time_keys).toBeTruthy();
|
||||
expect(content.one_time_keys).not.toEqual({});
|
||||
logger.log('%s: received %i one-time keys', this,
|
||||
Object.keys(content.one_time_keys).length);
|
||||
Object.keys(content.one_time_keys!).length);
|
||||
this.oneTimeKeys = content.one_time_keys;
|
||||
return { one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys).length,
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys!).length,
|
||||
} };
|
||||
});
|
||||
|
||||
// this can take ages
|
||||
return this.httpBackend.flush('/keys/upload', 2, 1000).then((flushed) => {
|
||||
expect(flushed).toEqual(2);
|
||||
return this.oneTimeKeys;
|
||||
return this.oneTimeKeys!;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ export class TestClient {
|
||||
this.httpBackend.when('POST', '/keys/query').respond<IDownloadKeyResult>(
|
||||
200, (_path, content) => {
|
||||
Object.keys(response.device_keys).forEach((userId) => {
|
||||
expect(content.device_keys[userId]).toEqual([]);
|
||||
expect(content.device_keys![userId]).toEqual([]);
|
||||
});
|
||||
return response;
|
||||
});
|
||||
@@ -206,7 +206,7 @@ export class TestClient {
|
||||
*/
|
||||
public getDeviceKey(): string {
|
||||
const keyId = 'curve25519:' + this.deviceId;
|
||||
return this.deviceKeys.keys[keyId];
|
||||
return this.deviceKeys!.keys[keyId];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -216,7 +216,7 @@ export class TestClient {
|
||||
*/
|
||||
public getSigningKey(): string {
|
||||
const keyId = 'ed25519:' + this.deviceId;
|
||||
return this.deviceKeys.keys[keyId];
|
||||
return this.deviceKeys!.keys[keyId];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -237,6 +237,6 @@ export class TestClient {
|
||||
}
|
||||
|
||||
public getUserId(): string {
|
||||
return this.userId;
|
||||
return this.userId!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,21 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
|
||||
import type { MatrixClient, ClientEvent } from "../../src";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace NodeJS {
|
||||
interface Global {
|
||||
matrixcs: {
|
||||
MatrixClient: typeof MatrixClient;
|
||||
ClientEvent: typeof ClientEvent;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stub for browser-matrix browserify tests
|
||||
// @ts-ignore
|
||||
global.XMLHttpRequest = jest.fn();
|
||||
@@ -23,3 +38,9 @@ afterAll(() => {
|
||||
// @ts-ignore
|
||||
global.XMLHttpRequest = undefined;
|
||||
});
|
||||
|
||||
// Akin to spec/setupTests.ts - but that won't affect the browser-matrix bundle
|
||||
global.matrixcs = {
|
||||
...global.matrixcs,
|
||||
timeoutSignal: () => new AbortController().signal,
|
||||
};
|
||||
|
||||
@@ -16,27 +16,14 @@ limitations under the License.
|
||||
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
|
||||
import "./setupTests";
|
||||
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
|
||||
import type { MatrixClient, ClientEvent } from "../../src";
|
||||
import "./setupTests";// uses browser-matrix instead of the src
|
||||
import type { MatrixClient } from "../../src";
|
||||
|
||||
const USER_ID = "@user:test.server";
|
||||
const DEVICE_ID = "device_id";
|
||||
const ACCESS_TOKEN = "access_token";
|
||||
const ROOM_ID = "!room_id:server.test";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace NodeJS {
|
||||
interface Global {
|
||||
matrixcs: {
|
||||
MatrixClient: typeof MatrixClient;
|
||||
ClientEvent: typeof ClientEvent;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("Browserify Test", function() {
|
||||
let client: MatrixClient;
|
||||
let httpBackend: HttpBackend;
|
||||
|
||||
@@ -59,7 +59,7 @@ async function bobUploadsDeviceKeys(): Promise<void> {
|
||||
bobTestClient.client.uploadKeys(),
|
||||
bobTestClient.httpBackend.flushAllExpected(),
|
||||
]);
|
||||
expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0);
|
||||
expect(Object.keys(bobTestClient.deviceKeys!).length).not.toEqual(0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,7 +99,7 @@ async function expectAliClaimKeys(): Promise<void> {
|
||||
expect(claimType).toEqual("signed_curve25519");
|
||||
let keyId = '';
|
||||
for (keyId in keys) {
|
||||
if (bobTestClient.oneTimeKeys.hasOwnProperty(keyId)) {
|
||||
if (bobTestClient.oneTimeKeys!.hasOwnProperty(keyId)) {
|
||||
if (keyId.indexOf(claimType + ":") === 0) {
|
||||
break;
|
||||
}
|
||||
@@ -137,7 +137,7 @@ async function aliDownloadsKeys(): Promise<void> {
|
||||
// @ts-ignore - protected
|
||||
aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const devices = data!.devices[bobUserId]!;
|
||||
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys.keys);
|
||||
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys!.keys);
|
||||
expect(devices[bobDeviceId].verified).
|
||||
toBe(DeviceInfo.DeviceVerification.UNVERIFIED);
|
||||
});
|
||||
@@ -223,7 +223,7 @@ async function expectBobSendMessageRequest(): Promise<OlmPayload> {
|
||||
const content = await expectSendMessageRequest(bobTestClient.httpBackend);
|
||||
bobMessages.push(content);
|
||||
const aliKeyId = "curve25519:" + aliDeviceId;
|
||||
const aliDeviceCurve25519Key = aliTestClient.deviceKeys.keys[aliKeyId];
|
||||
const aliDeviceCurve25519Key = aliTestClient.deviceKeys!.keys[aliKeyId];
|
||||
expect(Object.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
|
||||
const ciphertext = content.ciphertext[aliDeviceCurve25519Key];
|
||||
expect(ciphertext).toBeTruthy();
|
||||
@@ -393,7 +393,7 @@ describe("MatrixClient crypto", () => {
|
||||
it("Ali gets keys with an invalid signature", async () => {
|
||||
await bobUploadsDeviceKeys();
|
||||
// tamper bob's keys
|
||||
const bobDeviceKeys = bobTestClient.deviceKeys;
|
||||
const bobDeviceKeys = bobTestClient.deviceKeys!;
|
||||
expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeTruthy();
|
||||
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
|
||||
await Promise.all([
|
||||
@@ -479,7 +479,7 @@ describe("MatrixClient crypto", () => {
|
||||
await bobTestClient.start();
|
||||
const keys = await bobTestClient.awaitOneTimeKeyUpload();
|
||||
expect(Object.keys(keys).length).toEqual(5);
|
||||
expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0);
|
||||
expect(Object.keys(bobTestClient.deviceKeys!).length).not.toEqual(0);
|
||||
});
|
||||
|
||||
it("Ali sends a message", async () => {
|
||||
|
||||
@@ -18,12 +18,14 @@ import * as utils from "../test-utils/test-utils";
|
||||
import {
|
||||
ClientEvent,
|
||||
Direction,
|
||||
EventStatus,
|
||||
EventTimeline,
|
||||
EventTimelineSet,
|
||||
Filter,
|
||||
IEvent,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
PendingEventOrdering,
|
||||
Room,
|
||||
} from "../../src/matrix";
|
||||
import { logger } from "../../src/logger";
|
||||
@@ -342,8 +344,14 @@ describe("MatrixClient event timelines", function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
Thread.setServerSideSupport(FeatureSupport.None);
|
||||
Thread.setServerSideListSupport(FeatureSupport.None);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.None);
|
||||
});
|
||||
|
||||
async function flushHttp<T>(promise: Promise<T>): Promise<T> {
|
||||
return Promise.all([promise, httpBackend.flushAllExpected()]).then(([result]) => result);
|
||||
}
|
||||
|
||||
describe("getEventTimeline", function() {
|
||||
it("should create a new timeline for new events", function() {
|
||||
const room = client.getRoom(roomId)!;
|
||||
@@ -595,22 +603,8 @@ describe("MatrixClient event timelines", function() {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
client.stopClient(); // we don't need the client to be syncing at this time
|
||||
await client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId)!;
|
||||
const thread = room.createThread(THREAD_ROOT.event_id!, undefined, [], false);
|
||||
const timelineSet = thread.timelineSet;
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_REPLY.event_id!))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token0",
|
||||
events_before: [],
|
||||
event: THREAD_REPLY,
|
||||
events_after: [],
|
||||
end: "end_token0",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function() {
|
||||
@@ -619,7 +613,7 @@ describe("MatrixClient event timelines", function() {
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
|
||||
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?limit=20")
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?dir=b&limit=1")
|
||||
.respond(200, function() {
|
||||
return {
|
||||
original_event: THREAD_ROOT,
|
||||
@@ -628,9 +622,45 @@ describe("MatrixClient event timelines", function() {
|
||||
};
|
||||
});
|
||||
|
||||
const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!);
|
||||
const thread = room.createThread(THREAD_ROOT.event_id!, undefined, [], false);
|
||||
await httpBackend.flushAllExpected();
|
||||
const timelineSet = thread.timelineSet;
|
||||
|
||||
const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!);
|
||||
const timeline = await timelinePromise;
|
||||
|
||||
expect(timeline!.getEvents().find(e => e.getId() === THREAD_ROOT.event_id!)).toBeTruthy();
|
||||
expect(timeline!.getEvents().find(e => e.getId() === THREAD_REPLY.event_id!)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should handle thread replies with server support by fetching a contiguous thread timeline", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
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/relations/" +
|
||||
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?dir=b&limit=1")
|
||||
.respond(200, function() {
|
||||
return {
|
||||
original_event: THREAD_ROOT,
|
||||
chunk: [THREAD_REPLY],
|
||||
// no next batch as this is the oldest end of the timeline
|
||||
};
|
||||
});
|
||||
|
||||
const thread = room.createThread(THREAD_ROOT.event_id!, undefined, [], false);
|
||||
await httpBackend.flushAllExpected();
|
||||
const timelineSet = thread.timelineSet;
|
||||
|
||||
const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!);
|
||||
const timeline = await timelinePromise;
|
||||
|
||||
expect(timeline!.getEvents().find(e => e.getId() === THREAD_ROOT.event_id!)).toBeTruthy();
|
||||
@@ -1025,10 +1055,6 @@ describe("MatrixClient event timelines", function() {
|
||||
});
|
||||
|
||||
describe("paginateEventTimeline for thread list timeline", function() {
|
||||
async function flushHttp<T>(promise: Promise<T>): Promise<T> {
|
||||
return Promise.all([promise, httpBackend.flushAllExpected()]).then(([result]) => result);
|
||||
}
|
||||
|
||||
const RANDOM_TOKEN = "7280349c7bee430f91defe2a38a0a08c";
|
||||
|
||||
function respondToFilter(): ExpectedHttpRequest {
|
||||
@@ -1047,10 +1073,10 @@ describe("MatrixClient event timelines", function() {
|
||||
response = {
|
||||
chunk: [THREAD_ROOT],
|
||||
state: [],
|
||||
next_batch: RANDOM_TOKEN,
|
||||
next_batch: RANDOM_TOKEN as string | null,
|
||||
},
|
||||
): ExpectedHttpRequest {
|
||||
const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/threads", {
|
||||
const request = httpBackend.when("GET", encodeUri("/_matrix/client/v1/rooms/$roomId/threads", {
|
||||
$roomId: roomId,
|
||||
}));
|
||||
request.respond(200, response);
|
||||
@@ -1089,8 +1115,9 @@ describe("MatrixClient event timelines", function() {
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
|
||||
});
|
||||
|
||||
async function testPagination(timelineSet: EventTimelineSet, direction: Direction) {
|
||||
@@ -1111,7 +1138,7 @@ describe("MatrixClient event timelines", function() {
|
||||
|
||||
it("should allow you to paginate all threads backwards", async function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSets = await (room?.createThreadsTimelineSets());
|
||||
const timelineSets = await room!.createThreadsTimelineSets();
|
||||
expect(timelineSets).not.toBeNull();
|
||||
const [allThreads, myThreads] = timelineSets!;
|
||||
await testPagination(allThreads, Direction.Backward);
|
||||
@@ -1120,7 +1147,7 @@ describe("MatrixClient event timelines", function() {
|
||||
|
||||
it("should allow you to paginate all threads forwards", async function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSets = await (room?.createThreadsTimelineSets());
|
||||
const timelineSets = await room!.createThreadsTimelineSets();
|
||||
expect(timelineSets).not.toBeNull();
|
||||
const [allThreads, myThreads] = timelineSets!;
|
||||
|
||||
@@ -1130,13 +1157,32 @@ describe("MatrixClient event timelines", function() {
|
||||
|
||||
it("should allow fetching all threads", async function() {
|
||||
const room = client.getRoom(roomId)!;
|
||||
const timelineSets = await room.createThreadsTimelineSets();
|
||||
const timelineSets = await room!.createThreadsTimelineSets();
|
||||
expect(timelineSets).not.toBeNull();
|
||||
respondToThreads();
|
||||
respondToThreads();
|
||||
httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA);
|
||||
await flushHttp(room.fetchRoomThreads());
|
||||
});
|
||||
|
||||
it("should prevent displaying pending events", async function() {
|
||||
const room = new Room("room123", client, "john", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
const timelineSets = await room!.createThreadsTimelineSets();
|
||||
expect(timelineSets).not.toBeNull();
|
||||
|
||||
const event = utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "a body", event: true,
|
||||
});
|
||||
event.status = EventStatus.SENDING;
|
||||
room.addPendingEvent(event, "txn");
|
||||
|
||||
const [allThreads, myThreads] = timelineSets!;
|
||||
expect(allThreads.getPendingEvents()).toHaveLength(0);
|
||||
expect(myThreads.getPendingEvents()).toHaveLength(0);
|
||||
expect(room.getPendingEvents()).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("without server compatibility", function() {
|
||||
@@ -1418,74 +1464,115 @@ describe("MatrixClient event timelines", function() {
|
||||
});
|
||||
});
|
||||
|
||||
it("should re-insert room IDs for bundled thread relation events", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_4",
|
||||
rooms: {
|
||||
join: {
|
||||
[roomId]: {
|
||||
timeline: {
|
||||
events: [
|
||||
SYNC_THREAD_ROOT,
|
||||
],
|
||||
prev_batch: "f_1_1",
|
||||
describe("should re-insert room IDs for bundled thread relation events", () => {
|
||||
async function doTest() {
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_4",
|
||||
rooms: {
|
||||
join: {
|
||||
[roomId]: {
|
||||
timeline: {
|
||||
events: [
|
||||
SYNC_THREAD_ROOT,
|
||||
],
|
||||
prev_batch: "f_1_1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]);
|
||||
|
||||
const room = client.getRoom(roomId)!;
|
||||
const thread = room.getThread(THREAD_ROOT.event_id!)!;
|
||||
const timelineSet = thread.timelineSet;
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, {
|
||||
start: "start_token",
|
||||
events_before: [],
|
||||
event: THREAD_ROOT,
|
||||
events_after: [],
|
||||
state: [],
|
||||
end: "end_token",
|
||||
});
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
|
||||
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?limit=20")
|
||||
.respond(200, function() {
|
||||
return {
|
||||
original_event: THREAD_ROOT,
|
||||
chunk: [THREAD_REPLY],
|
||||
// no next batch as this is the oldest end of the timeline
|
||||
};
|
||||
});
|
||||
await Promise.all([
|
||||
client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]);
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_5",
|
||||
rooms: {
|
||||
join: {
|
||||
[roomId]: {
|
||||
timeline: {
|
||||
events: [
|
||||
SYNC_THREAD_REPLY,
|
||||
],
|
||||
prev_batch: "f_1_2",
|
||||
const room = client.getRoom(roomId)!;
|
||||
const thread = room.getThread(THREAD_ROOT.event_id!)!;
|
||||
const timelineSet = thread.timelineSet;
|
||||
|
||||
const buildParams = (direction: Direction, token: string): string => {
|
||||
if (Thread.hasServerSideFwdPaginationSupport === FeatureSupport.Experimental) {
|
||||
return `?from=${token}&org.matrix.msc3715.dir=${direction}`;
|
||||
} else {
|
||||
return `?dir=${direction}&from=${token}`;
|
||||
}
|
||||
};
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, {
|
||||
start: "start_token",
|
||||
events_before: [],
|
||||
event: THREAD_ROOT,
|
||||
events_after: [],
|
||||
state: [],
|
||||
end: "end_token",
|
||||
});
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
|
||||
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) + buildParams(Direction.Backward, "start_token"))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
original_event: THREAD_ROOT,
|
||||
chunk: [],
|
||||
};
|
||||
});
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
|
||||
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) + buildParams(Direction.Forward, "end_token"))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
original_event: THREAD_ROOT,
|
||||
chunk: [THREAD_REPLY],
|
||||
};
|
||||
});
|
||||
const timeline = await flushHttp(client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!));
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_5",
|
||||
rooms: {
|
||||
join: {
|
||||
[roomId]: {
|
||||
timeline: {
|
||||
events: [
|
||||
SYNC_THREAD_REPLY,
|
||||
],
|
||||
prev_batch: "f_1_2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]);
|
||||
|
||||
expect(timeline!.getEvents()[1]!.event).toEqual(THREAD_REPLY);
|
||||
}
|
||||
|
||||
it("in stable mode", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
|
||||
|
||||
return doTest();
|
||||
});
|
||||
|
||||
await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]);
|
||||
it("in backwards compatible unstable mode", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Experimental);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Experimental);
|
||||
|
||||
expect(thread.liveTimeline.getEvents()[1].event).toEqual(THREAD_REPLY);
|
||||
return doTest();
|
||||
});
|
||||
|
||||
it("in backwards compatible mode", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
Thread.setServerSideListSupport(FeatureSupport.None);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.None);
|
||||
|
||||
return doTest();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -139,7 +139,7 @@ describe("MatrixClient", function() {
|
||||
const r = client!.cancelUpload(prom);
|
||||
expect(r).toBe(true);
|
||||
await expect(prom).rejects.toThrow("Aborted");
|
||||
expect(client.getCurrentUploads()).toHaveLength(0);
|
||||
expect(client!.getCurrentUploads()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -178,7 +178,7 @@ describe("MatrixClient", function() {
|
||||
expect(request.data.third_party_signed).toEqual(signature);
|
||||
}).respond(200, { room_id: roomId });
|
||||
|
||||
const prom = client.joinRoom(roomId, {
|
||||
const prom = client!.joinRoom(roomId, {
|
||||
inviteSignUrl,
|
||||
viaServers,
|
||||
});
|
||||
@@ -658,7 +658,7 @@ describe("MatrixClient", function() {
|
||||
|
||||
// The vote event has been copied into the thread
|
||||
const eventRefWithThreadId = withThreadId(
|
||||
eventPollResponseReference, eventPollStartThreadRoot.getId());
|
||||
eventPollResponseReference, eventPollStartThreadRoot.getId()!);
|
||||
expect(eventRefWithThreadId.threadRootId).toBeTruthy();
|
||||
|
||||
expect(threaded).toEqual([
|
||||
@@ -695,7 +695,7 @@ describe("MatrixClient", function() {
|
||||
expect(threaded).toEqual([
|
||||
eventPollStartThreadRoot,
|
||||
eventMessageInThread,
|
||||
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
|
||||
withThreadId(eventReaction, eventPollStartThreadRoot.getId()!),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -725,7 +725,7 @@ describe("MatrixClient", function() {
|
||||
|
||||
expect(threaded).toEqual([
|
||||
eventPollStartThreadRoot,
|
||||
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
|
||||
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()!),
|
||||
eventMessageInThread,
|
||||
]);
|
||||
});
|
||||
@@ -757,7 +757,7 @@ describe("MatrixClient", function() {
|
||||
expect(threaded).toEqual([
|
||||
eventPollStartThreadRoot,
|
||||
eventMessageInThread,
|
||||
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
|
||||
withThreadId(eventReaction, eventPollStartThreadRoot.getId()!),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -813,7 +813,7 @@ describe("MatrixClient", function() {
|
||||
// Thread should contain only stuff that happened in the thread - no room state events
|
||||
expect(threaded).toEqual([
|
||||
eventPollStartThreadRoot,
|
||||
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
|
||||
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()!),
|
||||
eventMessageInThread,
|
||||
]);
|
||||
});
|
||||
@@ -1164,18 +1164,18 @@ describe("MatrixClient", function() {
|
||||
|
||||
describe("logout", () => {
|
||||
it("should abort pending requests when called with stopClient=true", async () => {
|
||||
httpBackend.when("POST", "/logout").respond(200, {});
|
||||
httpBackend!.when("POST", "/logout").respond(200, {});
|
||||
const fn = jest.fn();
|
||||
client.http.request(Method.Get, "/test").catch(fn);
|
||||
client.logout(true);
|
||||
await httpBackend.flush(undefined);
|
||||
client!.http.request(Method.Get, "/test").catch(fn);
|
||||
client!.logout(true);
|
||||
await httpBackend!.flush(undefined);
|
||||
expect(fn).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendHtmlEmote", () => {
|
||||
it("should send valid html emote", async () => {
|
||||
httpBackend.when("PUT", "/send").check(req => {
|
||||
httpBackend!.when("PUT", "/send").check(req => {
|
||||
expect(req.data).toStrictEqual({
|
||||
"msgtype": "m.emote",
|
||||
"body": "Body",
|
||||
@@ -1184,15 +1184,15 @@ describe("MatrixClient", function() {
|
||||
"org.matrix.msc1767.message": expect.anything(),
|
||||
});
|
||||
}).respond(200, { event_id: "$foobar" });
|
||||
const prom = client.sendHtmlEmote("!room:server", "Body", "<h1>Body</h1>");
|
||||
await httpBackend.flush(undefined);
|
||||
const prom = client!.sendHtmlEmote("!room:server", "Body", "<h1>Body</h1>");
|
||||
await httpBackend!.flush(undefined);
|
||||
await expect(prom).resolves.toStrictEqual({ event_id: "$foobar" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendHtmlMessage", () => {
|
||||
it("should send valid html message", async () => {
|
||||
httpBackend.when("PUT", "/send").check(req => {
|
||||
httpBackend!.when("PUT", "/send").check(req => {
|
||||
expect(req.data).toStrictEqual({
|
||||
"msgtype": "m.text",
|
||||
"body": "Body",
|
||||
@@ -1201,24 +1201,24 @@ describe("MatrixClient", function() {
|
||||
"org.matrix.msc1767.message": expect.anything(),
|
||||
});
|
||||
}).respond(200, { event_id: "$foobar" });
|
||||
const prom = client.sendHtmlMessage("!room:server", "Body", "<h1>Body</h1>");
|
||||
await httpBackend.flush(undefined);
|
||||
const prom = client!.sendHtmlMessage("!room:server", "Body", "<h1>Body</h1>");
|
||||
await httpBackend!.flush(undefined);
|
||||
await expect(prom).resolves.toStrictEqual({ event_id: "$foobar" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("forget", () => {
|
||||
it("should remove from store by default", async () => {
|
||||
const room = new Room("!roomId:server", client, userId);
|
||||
client.store.storeRoom(room);
|
||||
expect(client.store.getRooms()).toContain(room);
|
||||
const room = new Room("!roomId:server", client!, userId);
|
||||
client!.store.storeRoom(room);
|
||||
expect(client!.store.getRooms()).toContain(room);
|
||||
|
||||
httpBackend.when("POST", "/forget").respond(200, {});
|
||||
httpBackend!.when("POST", "/forget").respond(200, {});
|
||||
await Promise.all([
|
||||
client.forget(room.roomId),
|
||||
httpBackend.flushAllExpected(),
|
||||
client!.forget(room.roomId),
|
||||
httpBackend!.flushAllExpected(),
|
||||
]);
|
||||
expect(client.store.getRooms()).not.toContain(room);
|
||||
expect(client!.store.getRooms()).not.toContain(room);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1306,8 +1306,32 @@ describe("MatrixClient", function() {
|
||||
const resp = await prom;
|
||||
expect(resp.access_token).toBe(token);
|
||||
expect(resp.user_id).toBe(userId);
|
||||
expect(client.getUserId()).toBe(userId);
|
||||
expect(client.http.opts.accessToken).toBe(token);
|
||||
expect(client!.getUserId()).toBe(userId);
|
||||
expect(client!.http.opts.accessToken).toBe(token);
|
||||
});
|
||||
});
|
||||
|
||||
describe("registerWithIdentityServer", () => {
|
||||
it("should pass data to POST request", async () => {
|
||||
const token = {
|
||||
access_token: "access_token",
|
||||
token_type: "Bearer",
|
||||
matrix_server_name: "server_name",
|
||||
expires_in: 12345,
|
||||
};
|
||||
|
||||
httpBackend!.when("POST", "/account/register").check(req => {
|
||||
expect(req.data).toStrictEqual(token);
|
||||
}).respond(200, {
|
||||
access_token: "at",
|
||||
token: "tt",
|
||||
});
|
||||
|
||||
const prom = client!.registerWithIdentityServer(token);
|
||||
await httpBackend!.flushAllExpected();
|
||||
const resp = await prom;
|
||||
expect(resp.access_token).toBe("at");
|
||||
expect(resp.token).toBe("tt");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1351,7 +1375,7 @@ const buildEventMessageInThread = (root: MatrixEvent) => new MatrixEvent({
|
||||
"m.relates_to": {
|
||||
"event_id": root.getId(),
|
||||
"m.in_reply_to": {
|
||||
"event_id": root.getId(),
|
||||
"event_id": root.getId()!,
|
||||
},
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
@@ -1450,13 +1474,13 @@ const buildEventReply = (target: MatrixEvent) => new MatrixEvent({
|
||||
"device_id": "XISFUZSKHH",
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
"event_id": target.getId(),
|
||||
"event_id": target.getId()!,
|
||||
},
|
||||
},
|
||||
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
|
||||
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
|
||||
},
|
||||
"event_id": target.getId() + Math.random(),
|
||||
"event_id": target.getId()! + Math.random(),
|
||||
"origin_server_ts": 1643815466378,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
|
||||
@@ -60,7 +60,7 @@ describe("MatrixClient relations", () => {
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
|
||||
});
|
||||
|
||||
it("should read related events with relation type", async () => {
|
||||
@@ -72,7 +72,7 @@ describe("MatrixClient relations", () => {
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
|
||||
});
|
||||
|
||||
it("should read related events with relation type and event type", async () => {
|
||||
@@ -87,7 +87,7 @@ describe("MatrixClient relations", () => {
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
|
||||
});
|
||||
|
||||
it("should read related events with custom options", async () => {
|
||||
@@ -107,7 +107,7 @@ describe("MatrixClient relations", () => {
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
|
||||
});
|
||||
|
||||
it('should use default direction in the fetchRelations endpoint', async () => {
|
||||
|
||||
@@ -707,15 +707,11 @@ describe("MatrixClient syncing", () => {
|
||||
awaitSyncEvent(2),
|
||||
]).then(() => {
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
const stateAtStart = room.getLiveTimeline().getState(
|
||||
EventTimeline.BACKWARDS,
|
||||
);
|
||||
const stateAtStart = room.getLiveTimeline().getState(EventTimeline.BACKWARDS)!;
|
||||
const startRoomNameEvent = stateAtStart.getStateEvents('m.room.name', '');
|
||||
expect(startRoomNameEvent.getContent().name).toEqual('Old room name');
|
||||
|
||||
const stateAtEnd = room.getLiveTimeline().getState(
|
||||
EventTimeline.FORWARDS,
|
||||
);
|
||||
const stateAtEnd = room.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
||||
const endRoomNameEvent = stateAtEnd.getStateEvents('m.room.name', '');
|
||||
expect(endRoomNameEvent.getContent().name).toEqual('A new room name');
|
||||
});
|
||||
@@ -1551,6 +1547,107 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("peek", () => {
|
||||
beforeEach(() => {
|
||||
httpBackend!.expectedRequests = [];
|
||||
});
|
||||
|
||||
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",
|
||||
messages: {
|
||||
start: "start",
|
||||
end: "end",
|
||||
chunk: [{
|
||||
content: { body: "Message 1" },
|
||||
type: "m.room.message",
|
||||
event_id: "$eventId1",
|
||||
sender: userA,
|
||||
origin_server_ts: 12313525,
|
||||
room_id: roomOne,
|
||||
}, {
|
||||
content: { body: "Message 2" },
|
||||
type: "m.room.message",
|
||||
event_id: "$eventId2",
|
||||
sender: userB,
|
||||
origin_server_ts: 12315625,
|
||||
room_id: roomOne,
|
||||
}],
|
||||
},
|
||||
state: [{
|
||||
content: { name: "Room Name" },
|
||||
type: "m.room.name",
|
||||
event_id: "$eventId",
|
||||
sender: userA,
|
||||
origin_server_ts: 12314525,
|
||||
state_key: "",
|
||||
room_id: roomOne,
|
||||
}],
|
||||
presence: [{
|
||||
content: {},
|
||||
type: "m.presence",
|
||||
sender: userA,
|
||||
}],
|
||||
});
|
||||
httpBackend!.when("GET", "/events").respond(200, { chunk: [] });
|
||||
|
||||
const prom = client!.peekInRoom(roomOne);
|
||||
await httpBackend!.flushAllExpected();
|
||||
const room = await prom;
|
||||
|
||||
expect(room.roomId).toBe(roomOne);
|
||||
expect(room.getMyMembership()).toBe("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");
|
||||
expect(room.timeline[1].getContent().body).toBe("Message 2");
|
||||
client?.stopPeeking();
|
||||
httpBackend!.when("GET", "/events").respond(200, { chunk: [] });
|
||||
await httpBackend!.flushAllExpected();
|
||||
});
|
||||
});
|
||||
|
||||
describe("user account data", () => {
|
||||
it("should include correct prevEv in the ClientEvent.AccountData emit", async () => {
|
||||
const eventA1 = new MatrixEvent({ type: "a", content: { body: "1" } });
|
||||
const eventA2 = new MatrixEvent({ type: "a", content: { body: "2" } });
|
||||
const eventB1 = new MatrixEvent({ type: "b", content: { body: "1" } });
|
||||
const eventB2 = new MatrixEvent({ type: "b", content: { body: "2" } });
|
||||
|
||||
client!.store.storeAccountDataEvents([eventA1, eventB1]);
|
||||
const fn = jest.fn();
|
||||
client!.on(ClientEvent.AccountData, fn);
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, {
|
||||
next_batch: "batch_token",
|
||||
rooms: {},
|
||||
presence: {},
|
||||
account_data: {
|
||||
events: [eventA2.event, eventB2.event],
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
client!.startClient(),
|
||||
httpBackend!.flushAllExpected(),
|
||||
]);
|
||||
|
||||
const eventA = client?.getAccountData("a");
|
||||
expect(eventA).not.toBe(eventA1);
|
||||
const eventB = client?.getAccountData("b");
|
||||
expect(eventB).not.toBe(eventB1);
|
||||
|
||||
expect(fn).toHaveBeenCalledWith(eventA, eventA1);
|
||||
expect(fn).toHaveBeenCalledWith(eventB, eventB1);
|
||||
|
||||
expect(eventA?.getContent().body).toBe("2");
|
||||
expect(eventB?.getContent().body).toBe("2");
|
||||
|
||||
client!.off(ClientEvent.AccountData, fn);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* waits for the MatrixClient to emit one or more 'sync' events.
|
||||
*
|
||||
|
||||
@@ -1160,11 +1160,11 @@ describe("megolm", () => {
|
||||
"algorithm": 'm.megolm.v1.aes-sha2',
|
||||
"room_id": ROOM_ID,
|
||||
"sender_key": content.sender_key,
|
||||
"sender_claimed_ed25519_key": groupSessionKey.sender_claimed_ed25519_key,
|
||||
"sender_claimed_ed25519_key": groupSessionKey!.sender_claimed_ed25519_key,
|
||||
"session_id": content.session_id,
|
||||
"session_key": groupSessionKey.key,
|
||||
"chain_index": groupSessionKey.chain_index,
|
||||
"forwarding_curve25519_key_chain": groupSessionKey.forwarding_curve25519_key_chain,
|
||||
"session_key": groupSessionKey!.key,
|
||||
"chain_index": groupSessionKey!.chain_index,
|
||||
"forwarding_curve25519_key_chain": groupSessionKey!.forwarding_curve25519_key_chain,
|
||||
"org.matrix.msc3061.shared_history": true,
|
||||
},
|
||||
plaintype: 'm.forwarded_room_key',
|
||||
@@ -1298,11 +1298,11 @@ describe("megolm", () => {
|
||||
"algorithm": 'm.megolm.v1.aes-sha2',
|
||||
"room_id": ROOM_ID,
|
||||
"sender_key": content.sender_key,
|
||||
"sender_claimed_ed25519_key": groupSessionKey.sender_claimed_ed25519_key,
|
||||
"sender_claimed_ed25519_key": groupSessionKey!.sender_claimed_ed25519_key,
|
||||
"session_id": content.session_id,
|
||||
"session_key": groupSessionKey.key,
|
||||
"chain_index": groupSessionKey.chain_index,
|
||||
"forwarding_curve25519_key_chain": groupSessionKey.forwarding_curve25519_key_chain,
|
||||
"session_key": groupSessionKey!.key,
|
||||
"chain_index": groupSessionKey!.chain_index,
|
||||
"forwarding_curve25519_key_chain": groupSessionKey!.forwarding_curve25519_key_chain,
|
||||
"org.matrix.msc3061.shared_history": true,
|
||||
},
|
||||
plaintype: 'm.forwarded_room_key',
|
||||
|
||||
@@ -468,7 +468,7 @@ describe("SlidingSyncSdk", () => {
|
||||
it("emits SyncState.Reconnecting when < FAILED_SYNC_ERROR_THRESHOLD & SyncState.Error when over", async () => {
|
||||
mockSlidingSync!.emit(
|
||||
SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete,
|
||||
{ pos: "h", lists: [], rooms: {}, extensions: {} }, null,
|
||||
{ pos: "h", lists: [], rooms: {}, extensions: {} },
|
||||
);
|
||||
expect(sdk!.getSyncState()).toEqual(SyncState.Syncing);
|
||||
|
||||
@@ -490,7 +490,6 @@ describe("SlidingSyncSdk", () => {
|
||||
SlidingSyncEvent.Lifecycle,
|
||||
SlidingSyncState.Complete,
|
||||
{ pos: "i", lists: [], rooms: {}, extensions: {} },
|
||||
null,
|
||||
);
|
||||
expect(sdk!.getSyncState()).toEqual(SyncState.Syncing);
|
||||
});
|
||||
|
||||
+251
-16
@@ -82,7 +82,7 @@ describe("SlidingSync", () => {
|
||||
|
||||
it("should reset the connection on HTTP 400 and send everything again", async () => {
|
||||
// seed the connection with some lists, extensions and subscriptions to verify they are sent again
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
|
||||
const roomId = "!sub:localhost";
|
||||
const subInfo = {
|
||||
timeline_limit: 42,
|
||||
@@ -108,7 +108,7 @@ describe("SlidingSync", () => {
|
||||
|
||||
// expect everything to be sent
|
||||
let txnId;
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toEqual({
|
||||
@@ -117,7 +117,7 @@ describe("SlidingSync", () => {
|
||||
expect(body.lists[0]).toEqual(listInfo);
|
||||
expect(body.extensions).toBeTruthy();
|
||||
expect(body.extensions["custom_extension"]).toEqual({ initial: true });
|
||||
expect(req.queryParams["pos"]).toBeUndefined();
|
||||
expect(req.queryParams!["pos"]).toBeUndefined();
|
||||
txnId = body.txn_id;
|
||||
}).respond(200, function() {
|
||||
return {
|
||||
@@ -127,10 +127,10 @@ describe("SlidingSync", () => {
|
||||
txn_id: txnId,
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// expect nothing but ranges and non-initial extensions to be sent
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
@@ -139,7 +139,7 @@ describe("SlidingSync", () => {
|
||||
});
|
||||
expect(body.extensions).toBeTruthy();
|
||||
expect(body.extensions["custom_extension"]).toEqual({ initial: false });
|
||||
expect(req.queryParams["pos"]).toEqual("11");
|
||||
expect(req.queryParams!["pos"]).toEqual("11");
|
||||
}).respond(200, function() {
|
||||
return {
|
||||
pos: "12",
|
||||
@@ -147,19 +147,19 @@ describe("SlidingSync", () => {
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// now we expire the session
|
||||
httpBackend.when("POST", syncUrl).respond(400, function() {
|
||||
httpBackend!.when("POST", syncUrl).respond(400, function() {
|
||||
logger.debug("sending session expired 400");
|
||||
return {
|
||||
error: "HTTP 400 : session expired",
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// ...and everything should be sent again
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toEqual({
|
||||
@@ -168,7 +168,7 @@ describe("SlidingSync", () => {
|
||||
expect(body.lists[0]).toEqual(listInfo);
|
||||
expect(body.extensions).toBeTruthy();
|
||||
expect(body.extensions["custom_extension"]).toEqual({ initial: true });
|
||||
expect(req.queryParams["pos"]).toBeUndefined();
|
||||
expect(req.queryParams!["pos"]).toBeUndefined();
|
||||
}).respond(200, function() {
|
||||
return {
|
||||
pos: "1",
|
||||
@@ -176,7 +176,7 @@ describe("SlidingSync", () => {
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
slidingSync.stop();
|
||||
});
|
||||
});
|
||||
@@ -415,7 +415,7 @@ describe("SlidingSync", () => {
|
||||
expect(slidingSync.getList(0)).toBeDefined();
|
||||
expect(slidingSync.getList(5)).toBeNull();
|
||||
expect(slidingSync.getListData(5)).toBeNull();
|
||||
const syncData = slidingSync.getListData(0);
|
||||
const syncData = slidingSync.getListData(0)!;
|
||||
expect(syncData.joinedCount).toEqual(500); // from previous test
|
||||
expect(syncData.roomIndexToRoomId).toEqual({
|
||||
0: roomA,
|
||||
@@ -665,7 +665,7 @@ describe("SlidingSync", () => {
|
||||
0: roomB,
|
||||
1: roomC,
|
||||
};
|
||||
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual(indexToRoomId);
|
||||
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual(indexToRoomId);
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "f",
|
||||
// currently the list is [B,C] so we will insert D then immediately delete it
|
||||
@@ -703,7 +703,7 @@ describe("SlidingSync", () => {
|
||||
});
|
||||
|
||||
it("should handle deletions correctly", async () => {
|
||||
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual({
|
||||
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
|
||||
0: roomB,
|
||||
1: roomC,
|
||||
});
|
||||
@@ -739,7 +739,7 @@ describe("SlidingSync", () => {
|
||||
});
|
||||
|
||||
it("should handle insertions correctly", async () => {
|
||||
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual({
|
||||
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
|
||||
0: roomC,
|
||||
});
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
@@ -806,6 +806,91 @@ describe("SlidingSync", () => {
|
||||
await listPromise;
|
||||
slidingSync.stop();
|
||||
});
|
||||
|
||||
// Regression test to make sure things like DELETE 0 INSERT 0 work correctly and we don't
|
||||
// end up losing room IDs.
|
||||
it("should handle insertions with a spurious DELETE correctly", async () => {
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [
|
||||
{
|
||||
ranges: [[0, 20]],
|
||||
},
|
||||
], {}, client!, 1);
|
||||
// initially start with nothing
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "a",
|
||||
lists: [{
|
||||
count: 0,
|
||||
ops: [],
|
||||
}],
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({});
|
||||
|
||||
// insert a room
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "b",
|
||||
lists: [{
|
||||
count: 1,
|
||||
ops: [
|
||||
{
|
||||
op: "DELETE", index: 0,
|
||||
},
|
||||
{
|
||||
op: "INSERT", index: 0, room_id: roomA,
|
||||
},
|
||||
],
|
||||
}],
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
|
||||
0: roomA,
|
||||
});
|
||||
|
||||
// insert another room
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "c",
|
||||
lists: [{
|
||||
count: 1,
|
||||
ops: [
|
||||
{
|
||||
op: "DELETE", index: 1,
|
||||
},
|
||||
{
|
||||
op: "INSERT", index: 0, room_id: roomB,
|
||||
},
|
||||
],
|
||||
}],
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
|
||||
0: roomB,
|
||||
1: roomA,
|
||||
});
|
||||
|
||||
// insert a final room
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "c",
|
||||
lists: [{
|
||||
count: 1,
|
||||
ops: [
|
||||
{
|
||||
op: "DELETE", index: 2,
|
||||
},
|
||||
{
|
||||
op: "INSERT", index: 0, room_id: roomC,
|
||||
},
|
||||
],
|
||||
}],
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
|
||||
0: roomC,
|
||||
1: roomB,
|
||||
2: roomA,
|
||||
});
|
||||
slidingSync.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe("transaction IDs", () => {
|
||||
@@ -1027,6 +1112,156 @@ describe("SlidingSync", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("custom room subscriptions", () => {
|
||||
beforeAll(setupClient);
|
||||
afterAll(teardownClient);
|
||||
|
||||
const roomA = "!a";
|
||||
const roomB = "!b";
|
||||
const roomC = "!c";
|
||||
const roomD = "!d";
|
||||
|
||||
const defaultSub = {
|
||||
timeline_limit: 1,
|
||||
required_state: [["m.room.create", ""]],
|
||||
};
|
||||
|
||||
const customSubName1 = "sub1";
|
||||
const customSub1 = {
|
||||
timeline_limit: 2,
|
||||
required_state: [["*", "*"]],
|
||||
};
|
||||
|
||||
const customSubName2 = "sub2";
|
||||
const customSub2 = {
|
||||
timeline_limit: 3,
|
||||
required_state: [["*", "*"]],
|
||||
};
|
||||
|
||||
it("should be possible to use custom subscriptions on startup", async () => {
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
|
||||
// the intention is for clients to set this up at startup
|
||||
slidingSync.addCustomSubscription(customSubName1, customSub1);
|
||||
slidingSync.addCustomSubscription(customSubName2, customSub2);
|
||||
// then call these depending on the kind of room / context
|
||||
slidingSync.useCustomSubscription(roomA, customSubName1);
|
||||
slidingSync.useCustomSubscription(roomB, customSubName1);
|
||||
slidingSync.useCustomSubscription(roomC, customSubName2);
|
||||
slidingSync.modifyRoomSubscriptions(new Set<string>([roomA, roomB, roomC, roomD]));
|
||||
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("custom subs", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
expect(body.room_subscriptions[roomA]).toEqual(customSub1);
|
||||
expect(body.room_subscriptions[roomB]).toEqual(customSub1);
|
||||
expect(body.room_subscriptions[roomC]).toEqual(customSub2);
|
||||
expect(body.room_subscriptions[roomD]).toEqual(defaultSub);
|
||||
}).respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend!.flushAllExpected();
|
||||
slidingSync.stop();
|
||||
});
|
||||
|
||||
it("should be possible to use custom subscriptions mid-connection", async () => {
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
|
||||
// the intention is for clients to set this up at startup
|
||||
slidingSync.addCustomSubscription(customSubName1, customSub1);
|
||||
slidingSync.addCustomSubscription(customSubName2, customSub2);
|
||||
// initially no subs
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("custom subs", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
}).respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// now the user clicks on a room which uses the default sub
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("custom subs", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
expect(body.room_subscriptions[roomA]).toEqual(defaultSub);
|
||||
}).respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
slidingSync.modifyRoomSubscriptions(new Set<string>([roomA]));
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// now the user clicks on a room which uses a custom sub
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("custom subs", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
expect(body.room_subscriptions[roomB]).toEqual(customSub1);
|
||||
expect(body.unsubscribe_rooms).toEqual([roomA]);
|
||||
}).respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
slidingSync.useCustomSubscription(roomB, customSubName1);
|
||||
slidingSync.modifyRoomSubscriptions(new Set<string>([roomB]));
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// now the user uses a different sub for the same room: we don't unsub but just resend
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("custom subs", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
expect(body.room_subscriptions[roomB]).toEqual(customSub2);
|
||||
expect(body.unsubscribe_rooms).toBeFalsy();
|
||||
}).respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
slidingSync.useCustomSubscription(roomB, customSubName2);
|
||||
slidingSync.modifyRoomSubscriptions(new Set<string>([roomB]));
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
slidingSync.stop();
|
||||
});
|
||||
|
||||
it("uses the default subscription for unknown subscription names", async () => {
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
|
||||
slidingSync.addCustomSubscription(customSubName1, customSub1);
|
||||
slidingSync.useCustomSubscription(roomA, "unknown name");
|
||||
slidingSync.modifyRoomSubscriptions(new Set<string>([roomA]));
|
||||
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("custom subs", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
expect(body.room_subscriptions[roomA]).toEqual(defaultSub);
|
||||
}).respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend!.flushAllExpected();
|
||||
slidingSync.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe("extensions", () => {
|
||||
beforeAll(setupClient);
|
||||
afterAll(teardownClient);
|
||||
|
||||
@@ -17,3 +17,9 @@ limitations under the License.
|
||||
import DOMException from "domexception";
|
||||
|
||||
global.DOMException = DOMException;
|
||||
|
||||
jest.mock("../src/http-api/utils", () => ({
|
||||
...jest.requireActual("../src/http-api/utils"),
|
||||
// We mock timeoutSignal otherwise it causes tests to leave timers running
|
||||
timeoutSignal: () => new AbortController().signal,
|
||||
}));
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MethodKeysOf, mocked, MockedObject } from "jest-mock";
|
||||
import { MethodLikeKeys, mocked, MockedObject } from "jest-mock";
|
||||
|
||||
import { ClientEventHandlerMap, EmittedEvents, MatrixClient } from "../../src/client";
|
||||
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
|
||||
@@ -26,7 +26,7 @@ import { User } from "../../src/models/user";
|
||||
* to MatrixClient events
|
||||
*/
|
||||
export class MockClientWithEventEmitter extends TypedEventEmitter<EmittedEvents, ClientEventHandlerMap> {
|
||||
constructor(mockProperties: Partial<Record<MethodKeysOf<MatrixClient>, unknown>> = {}) {
|
||||
constructor(mockProperties: Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> = {}) {
|
||||
super();
|
||||
Object.assign(this, mockProperties);
|
||||
}
|
||||
@@ -44,7 +44,7 @@ export class MockClientWithEventEmitter extends TypedEventEmitter<EmittedEvents,
|
||||
* ```
|
||||
*/
|
||||
export const getMockClientWithEventEmitter = (
|
||||
mockProperties: Partial<Record<MethodKeysOf<MatrixClient>, unknown>>,
|
||||
mockProperties: Partial<Record<MethodLikeKeys<MatrixClient>, unknown>>,
|
||||
): MockedObject<MatrixClient> => {
|
||||
const mock = mocked(new MockClientWithEventEmitter(mockProperties) as unknown as MatrixClient);
|
||||
return mock;
|
||||
@@ -84,7 +84,7 @@ export const mockClientMethodsEvents = () => ({
|
||||
/**
|
||||
* Returns basic mocked client methods related to server support
|
||||
*/
|
||||
export const mockClientMethodsServer = (): Partial<Record<MethodKeysOf<MatrixClient>, unknown>> => ({
|
||||
export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({
|
||||
doesServerSupportSeparateAddAndBind: jest.fn(),
|
||||
getIdentityServerUrl: jest.fn(),
|
||||
getHomeserverUrl: jest.fn(),
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
Copyright 2022 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.
|
||||
*/
|
||||
|
||||
// Jest now uses @sinonjs/fake-timers which exposes tickAsync() and a number of
|
||||
// other async methods which break the event loop, letting scheduled promise
|
||||
// callbacks run. Unfortunately, Jest doesn't expose these, so we have to do
|
||||
// it manually (this is what sinon does under the hood). We do both in a loop
|
||||
// until the thing we expect happens: hopefully this is the least flakey way
|
||||
// and avoids assuming anything about the app's behaviour.
|
||||
const realSetTimeout = setTimeout;
|
||||
export function flushPromises() {
|
||||
return new Promise(r => {
|
||||
realSetTimeout(r, 1);
|
||||
});
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import EventEmitter from "events";
|
||||
import '../olm-loader';
|
||||
|
||||
import { logger } from '../../src/logger';
|
||||
import { IContent, IEvent, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { IContent, IEvent, IEventRelation, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { ClientEvent, EventType, IPusher, MatrixClient, MsgType } from "../../src";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { eventMapperFor } from "../../src/event-mapper";
|
||||
@@ -78,6 +78,7 @@ interface IEventOpts {
|
||||
user?: string;
|
||||
unsigned?: IUnsigned;
|
||||
redacts?: string;
|
||||
ts?: number;
|
||||
}
|
||||
|
||||
let testEventIndex = 1; // counter for events, easier for comparison of randomly generated events
|
||||
@@ -109,6 +110,7 @@ export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixC
|
||||
event_id: "$" + testEventIndex++ + "-" + Math.random() + "-" + Math.random(),
|
||||
txn_id: "~" + Math.random(),
|
||||
redacts: opts.redacts,
|
||||
origin_server_ts: opts.ts ?? 0,
|
||||
};
|
||||
if (opts.skey !== undefined) {
|
||||
event.state_key = opts.skey;
|
||||
@@ -237,11 +239,13 @@ export function mkMembershipCustom<T>(
|
||||
});
|
||||
}
|
||||
|
||||
interface IMessageOpts {
|
||||
export interface IMessageOpts {
|
||||
room?: string;
|
||||
user: string;
|
||||
msg?: string;
|
||||
event?: boolean;
|
||||
relatesTo?: IEventRelation;
|
||||
ts?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -269,6 +273,10 @@ export function mkMessage(
|
||||
},
|
||||
};
|
||||
|
||||
if (opts.relatesTo) {
|
||||
eventOpts.content["m.relates_to"] = opts.relatesTo;
|
||||
}
|
||||
|
||||
if (!eventOpts.content.body) {
|
||||
eventOpts.content.body = "Random->" + Math.random();
|
||||
}
|
||||
@@ -307,7 +315,7 @@ export function mkReplyMessage(
|
||||
"rel_type": "m.in_reply_to",
|
||||
"event_id": opts.replyToMessage.getId(),
|
||||
"m.in_reply_to": {
|
||||
"event_id": opts.replyToMessage.getId(),
|
||||
"event_id": opts.replyToMessage.getId()!,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
Copyright 2022 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 { RelationType } from "../../src/@types/event";
|
||||
import { MatrixClient } from "../../src/client";
|
||||
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { Thread } from "../../src/models/thread";
|
||||
import { mkMessage } from "./test-utils";
|
||||
|
||||
export const makeThreadEvent = ({ rootEventId, replyToEventId, ...props }: any & {
|
||||
rootEventId: string; replyToEventId: string; event?: boolean;
|
||||
}): MatrixEvent => mkMessage({
|
||||
...props,
|
||||
relatesTo: {
|
||||
event_id: rootEventId,
|
||||
rel_type: "m.thread",
|
||||
['m.in_reply_to']: {
|
||||
event_id: replyToEventId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
type MakeThreadEventsProps = {
|
||||
roomId: Room["roomId"];
|
||||
// root message user id
|
||||
authorId: string;
|
||||
// user ids of thread replies
|
||||
// cycled through until thread length is fulfilled
|
||||
participantUserIds: string[];
|
||||
// number of messages in the thread, root message included
|
||||
// optional, default 2
|
||||
length?: number;
|
||||
ts?: number;
|
||||
// provide to set current_user_participated accurately
|
||||
currentUserId?: string;
|
||||
};
|
||||
|
||||
export const makeThreadEvents = ({
|
||||
roomId, authorId, participantUserIds, length = 2, ts = 1, currentUserId,
|
||||
}: MakeThreadEventsProps): { rootEvent: MatrixEvent, events: MatrixEvent[] } => {
|
||||
const rootEvent = mkMessage({
|
||||
user: authorId,
|
||||
room: roomId,
|
||||
msg: 'root event message ' + Math.random(),
|
||||
ts,
|
||||
event: true,
|
||||
});
|
||||
|
||||
const rootEventId = rootEvent.getId();
|
||||
const events = [rootEvent];
|
||||
|
||||
for (let i = 1; i < length; i++) {
|
||||
const prevEvent = events[i - 1];
|
||||
const replyToEventId = prevEvent.getId();
|
||||
const user = participantUserIds[i % participantUserIds.length];
|
||||
events.push(makeThreadEvent({
|
||||
user,
|
||||
room: roomId,
|
||||
event: true,
|
||||
msg: `reply ${i} by ${user}`,
|
||||
rootEventId,
|
||||
replyToEventId,
|
||||
// replies are 1ms after each other
|
||||
ts: ts + i,
|
||||
}));
|
||||
}
|
||||
|
||||
rootEvent.setUnsigned({
|
||||
"m.relations": {
|
||||
[RelationType.Thread]: {
|
||||
latest_event: events[events.length - 1],
|
||||
count: length,
|
||||
current_user_participated: [...participantUserIds, authorId].includes(currentUserId ?? ""),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return { rootEvent, events };
|
||||
};
|
||||
|
||||
type MakeThreadProps = {
|
||||
room: Room;
|
||||
client: MatrixClient;
|
||||
authorId: string;
|
||||
participantUserIds: string[];
|
||||
length?: number;
|
||||
ts?: number;
|
||||
};
|
||||
|
||||
export const mkThread = ({
|
||||
room,
|
||||
client,
|
||||
authorId,
|
||||
participantUserIds,
|
||||
length = 2,
|
||||
ts = 1,
|
||||
}: MakeThreadProps): { thread: Thread, rootEvent: MatrixEvent, events: MatrixEvent[] } => {
|
||||
const { rootEvent, events } = makeThreadEvents({
|
||||
roomId: room.roomId,
|
||||
authorId,
|
||||
participantUserIds,
|
||||
length,
|
||||
ts,
|
||||
currentUserId: client.getUserId() ?? "",
|
||||
});
|
||||
expect(rootEvent).toBeTruthy();
|
||||
|
||||
for (const evt of events) {
|
||||
room?.reEmitter.reEmit(evt, [
|
||||
MatrixEventEvent.BeforeRedaction,
|
||||
]);
|
||||
}
|
||||
|
||||
const thread = room.createThread(rootEvent.getId() ?? "", rootEvent, events, true);
|
||||
// So that we do not have to mock the thread loading
|
||||
thread.initialEventsFetched = true;
|
||||
thread.addEvents(events, true);
|
||||
|
||||
return { thread, rootEvent, events };
|
||||
};
|
||||
+386
-29
@@ -14,6 +14,32 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
ClientEvent,
|
||||
ClientEventHandlerMap,
|
||||
EventType,
|
||||
GroupCall,
|
||||
GroupCallIntent,
|
||||
GroupCallType,
|
||||
IContent,
|
||||
ISendEventResponse,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
RoomState,
|
||||
RoomStateEvent,
|
||||
RoomStateEventHandlerMap,
|
||||
} from "../../src";
|
||||
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
|
||||
import { ReEmitter } from "../../src/ReEmitter";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { CallEvent, CallEventHandlerMap, MatrixCall } from "../../src/webrtc/call";
|
||||
import { CallEventHandlerEvent, CallEventHandlerEventHandlerMap } from "../../src/webrtc/callEventHandler";
|
||||
import { CallFeed } from "../../src/webrtc/callFeed";
|
||||
import { GroupCallEventHandlerMap } from "../../src/webrtc/groupCall";
|
||||
import { GroupCallEventHandlerEvent } from "../../src/webrtc/groupCallEventHandler";
|
||||
import { IScreensharingOpts, MediaHandler } from "../../src/webrtc/mediaHandler";
|
||||
|
||||
export const DUMMY_SDP = (
|
||||
"v=0\r\n" +
|
||||
"o=- 5022425983810148698 2 IN IP4 127.0.0.1\r\n" +
|
||||
@@ -54,8 +80,50 @@ export const DUMMY_SDP = (
|
||||
"a=ssrc:3619738545 cname:2RWtmqhXLdoF4sOi\r\n"
|
||||
);
|
||||
|
||||
export const USERMEDIA_STREAM_ID = "mock_stream_from_media_handler";
|
||||
export const SCREENSHARE_STREAM_ID = "mock_screen_stream_from_media_handler";
|
||||
|
||||
class MockMediaStreamAudioSourceNode {
|
||||
public connect() {}
|
||||
}
|
||||
|
||||
class MockAnalyser {
|
||||
public getFloatFrequencyData() { return 0.0; }
|
||||
}
|
||||
|
||||
export class MockAudioContext {
|
||||
constructor() {}
|
||||
public createAnalyser() { return new MockAnalyser(); }
|
||||
public createMediaStreamSource() { return new MockMediaStreamAudioSourceNode(); }
|
||||
public close() {}
|
||||
}
|
||||
|
||||
export class MockRTCPeerConnection {
|
||||
localDescription: RTCSessionDescription;
|
||||
private static instances: MockRTCPeerConnection[] = [];
|
||||
|
||||
private negotiationNeededListener?: () => void;
|
||||
public iceCandidateListener?: (e: RTCPeerConnectionIceEvent) => void;
|
||||
public onTrackListener?: (e: RTCTrackEvent) => void;
|
||||
public needsNegotiation = false;
|
||||
public readyToNegotiate: Promise<void>;
|
||||
private onReadyToNegotiate?: () => void;
|
||||
public localDescription: RTCSessionDescription;
|
||||
public signalingState: RTCSignalingState = "stable";
|
||||
public transceivers: MockRTCRtpTransceiver[] = [];
|
||||
|
||||
public static triggerAllNegotiations(): void {
|
||||
for (const inst of this.instances) {
|
||||
inst.doNegotiation();
|
||||
}
|
||||
}
|
||||
|
||||
public static hasAnyPendingNegotiations(): boolean {
|
||||
return this.instances.some(i => i.needsNegotiation);
|
||||
}
|
||||
|
||||
public static resetInstances() {
|
||||
this.instances = [];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.localDescription = {
|
||||
@@ -63,34 +131,133 @@ export class MockRTCPeerConnection {
|
||||
type: 'offer',
|
||||
toJSON: function() { },
|
||||
};
|
||||
|
||||
this.readyToNegotiate = new Promise<void>(resolve => {
|
||||
this.onReadyToNegotiate = resolve;
|
||||
});
|
||||
|
||||
MockRTCPeerConnection.instances.push(this);
|
||||
}
|
||||
|
||||
addEventListener() { }
|
||||
createDataChannel(label: string, opts: RTCDataChannelInit) { return { label, ...opts }; }
|
||||
createOffer() {
|
||||
return Promise.resolve({});
|
||||
public addEventListener(type: string, listener: () => void) {
|
||||
if (type === 'negotiationneeded') {
|
||||
this.negotiationNeededListener = listener;
|
||||
} else if (type == 'icecandidate') {
|
||||
this.iceCandidateListener = listener;
|
||||
} else if (type == 'track') {
|
||||
this.onTrackListener = listener;
|
||||
}
|
||||
}
|
||||
setRemoteDescription() {
|
||||
public createDataChannel(label: string, opts: RTCDataChannelInit) { return { label, ...opts }; }
|
||||
public createOffer() {
|
||||
return Promise.resolve({
|
||||
type: 'offer',
|
||||
sdp: DUMMY_SDP,
|
||||
});
|
||||
}
|
||||
public createAnswer() {
|
||||
return Promise.resolve({
|
||||
type: 'answer',
|
||||
sdp: DUMMY_SDP,
|
||||
});
|
||||
}
|
||||
public setRemoteDescription() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
setLocalDescription() {
|
||||
public setLocalDescription() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
close() { }
|
||||
getStats() { return []; }
|
||||
addTrack(track: MockMediaStreamTrack) { return new MockRTCRtpSender(track); }
|
||||
public close() { }
|
||||
public getStats() { return []; }
|
||||
public addTransceiver(track: MockMediaStreamTrack): MockRTCRtpTransceiver {
|
||||
this.needsNegotiation = true;
|
||||
if (this.onReadyToNegotiate) this.onReadyToNegotiate();
|
||||
|
||||
const newSender = new MockRTCRtpSender(track);
|
||||
const newReceiver = new MockRTCRtpReceiver(track);
|
||||
|
||||
const newTransceiver = new MockRTCRtpTransceiver(this);
|
||||
newTransceiver.sender = newSender as unknown as RTCRtpSender;
|
||||
newTransceiver.receiver = newReceiver as unknown as RTCRtpReceiver;
|
||||
|
||||
this.transceivers.push(newTransceiver);
|
||||
|
||||
return newTransceiver;
|
||||
}
|
||||
public addTrack(track: MockMediaStreamTrack): MockRTCRtpSender {
|
||||
return this.addTransceiver(track).sender as unknown as MockRTCRtpSender;
|
||||
}
|
||||
|
||||
public removeTrack() {
|
||||
this.needsNegotiation = true;
|
||||
if (this.onReadyToNegotiate) this.onReadyToNegotiate();
|
||||
}
|
||||
|
||||
public getTransceivers(): MockRTCRtpTransceiver[] { return this.transceivers; }
|
||||
public getSenders(): MockRTCRtpSender[] {
|
||||
return this.transceivers.map(t => t.sender as unknown as MockRTCRtpSender);
|
||||
}
|
||||
|
||||
public doNegotiation() {
|
||||
if (this.needsNegotiation && this.negotiationNeededListener) {
|
||||
this.needsNegotiation = false;
|
||||
this.negotiationNeededListener();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class MockRTCRtpSender {
|
||||
constructor(public track: MockMediaStreamTrack) { }
|
||||
|
||||
replaceTrack(track: MockMediaStreamTrack) { this.track = track; }
|
||||
public replaceTrack(track: MockMediaStreamTrack) { this.track = track; }
|
||||
}
|
||||
|
||||
export class MockRTCRtpReceiver {
|
||||
constructor(public track: MockMediaStreamTrack) { }
|
||||
}
|
||||
|
||||
export class MockRTCRtpTransceiver {
|
||||
constructor(private peerConn: MockRTCPeerConnection) {}
|
||||
|
||||
public sender?: RTCRtpSender;
|
||||
public receiver?: RTCRtpReceiver;
|
||||
|
||||
public set direction(_: string) {
|
||||
this.peerConn.needsNegotiation = true;
|
||||
}
|
||||
|
||||
public setCodecPreferences = jest.fn<void, RTCRtpCodecCapability[]>();
|
||||
}
|
||||
|
||||
export class MockMediaStreamTrack {
|
||||
constructor(public readonly id: string, public readonly kind: "audio" | "video", public enabled = true) { }
|
||||
|
||||
stop() { }
|
||||
public stop = jest.fn<void, []>();
|
||||
|
||||
public listeners: [string, (...args: any[]) => any][] = [];
|
||||
public isStopped = false;
|
||||
public settings?: MediaTrackSettings;
|
||||
|
||||
public getSettings(): MediaTrackSettings { return this.settings!; }
|
||||
|
||||
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
|
||||
// implementation
|
||||
public dispatchEvent(eventType: string) {
|
||||
this.listeners.forEach(([t, c]) => {
|
||||
if (t !== eventType) return;
|
||||
c();
|
||||
});
|
||||
}
|
||||
public addEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
this.listeners.push([eventType, callback]);
|
||||
}
|
||||
public removeEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
this.listeners.filter(([t, c]) => {
|
||||
return t !== eventType || c !== callback;
|
||||
});
|
||||
}
|
||||
|
||||
public typed(): MediaStreamTrack { return this as unknown as MediaStreamTrack; }
|
||||
}
|
||||
|
||||
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
|
||||
@@ -101,46 +268,236 @@ export class MockMediaStream {
|
||||
private tracks: MockMediaStreamTrack[] = [],
|
||||
) {}
|
||||
|
||||
listeners: [string, (...args: any[]) => any][] = [];
|
||||
public listeners: [string, (...args: any[]) => any][] = [];
|
||||
public isStopped = false;
|
||||
|
||||
dispatchEvent(eventType: string) {
|
||||
public dispatchEvent(eventType: string) {
|
||||
this.listeners.forEach(([t, c]) => {
|
||||
if (t !== eventType) return;
|
||||
c();
|
||||
});
|
||||
}
|
||||
getTracks() { return this.tracks; }
|
||||
getAudioTracks() { return this.tracks.filter((track) => track.kind === "audio"); }
|
||||
getVideoTracks() { return this.tracks.filter((track) => track.kind === "video"); }
|
||||
addEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
public getTracks() { return this.tracks; }
|
||||
public getAudioTracks() { return this.tracks.filter((track) => track.kind === "audio"); }
|
||||
public getVideoTracks() { return this.tracks.filter((track) => track.kind === "video"); }
|
||||
public addEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
this.listeners.push([eventType, callback]);
|
||||
}
|
||||
removeEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
public removeEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
this.listeners.filter(([t, c]) => {
|
||||
return t !== eventType || c !== callback;
|
||||
});
|
||||
}
|
||||
addTrack(track: MockMediaStreamTrack) {
|
||||
public addTrack(track: MockMediaStreamTrack) {
|
||||
this.tracks.push(track);
|
||||
this.dispatchEvent("addtrack");
|
||||
}
|
||||
removeTrack(track: MockMediaStreamTrack) { this.tracks.splice(this.tracks.indexOf(track), 1); }
|
||||
public removeTrack(track: MockMediaStreamTrack) { this.tracks.splice(this.tracks.indexOf(track), 1); }
|
||||
|
||||
public clone(): MediaStream {
|
||||
return new MockMediaStream(this.id + ".clone", this.tracks).typed();
|
||||
}
|
||||
|
||||
public isCloneOf(stream: MediaStream) {
|
||||
return this.id === stream.id + ".clone";
|
||||
}
|
||||
|
||||
// syntactic sugar for typing
|
||||
public typed(): MediaStream {
|
||||
return this as unknown as MediaStream;
|
||||
}
|
||||
}
|
||||
|
||||
export class MockMediaDeviceInfo {
|
||||
constructor(
|
||||
public kind: "audio" | "video",
|
||||
public kind: "audioinput" | "videoinput" | "audiooutput",
|
||||
) { }
|
||||
|
||||
public typed(): MediaDeviceInfo { return this as unknown as MediaDeviceInfo; }
|
||||
}
|
||||
|
||||
export class MockMediaHandler {
|
||||
getUserMediaStream(audio: boolean, video: boolean) {
|
||||
const tracks = [];
|
||||
if (audio) tracks.push(new MockMediaStreamTrack("audio_track", "audio"));
|
||||
if (video) tracks.push(new MockMediaStreamTrack("video_track", "video"));
|
||||
public userMediaStreams: MockMediaStream[] = [];
|
||||
public screensharingStreams: MockMediaStream[] = [];
|
||||
|
||||
return new MockMediaStream("mock_stream_from_media_handler", tracks);
|
||||
public getUserMediaStream(audio: boolean, video: boolean) {
|
||||
const tracks: MockMediaStreamTrack[] = [];
|
||||
if (audio) tracks.push(new MockMediaStreamTrack("usermedia_audio_track", "audio"));
|
||||
if (video) tracks.push(new MockMediaStreamTrack("usermedia_video_track", "video"));
|
||||
|
||||
const stream = new MockMediaStream(USERMEDIA_STREAM_ID, tracks);
|
||||
this.userMediaStreams.push(stream);
|
||||
return stream;
|
||||
}
|
||||
stopUserMediaStream() { }
|
||||
hasAudioDevice() { return true; }
|
||||
public stopUserMediaStream(stream: MockMediaStream) {
|
||||
stream.isStopped = true;
|
||||
}
|
||||
public getScreensharingStream = jest.fn((opts?: IScreensharingOpts) => {
|
||||
const tracks = [new MockMediaStreamTrack("screenshare_video_track", "video")];
|
||||
if (opts?.audio) tracks.push(new MockMediaStreamTrack("screenshare_audio_track", "audio"));
|
||||
|
||||
const stream = new MockMediaStream(SCREENSHARE_STREAM_ID, tracks);
|
||||
this.screensharingStreams.push(stream);
|
||||
return stream;
|
||||
});
|
||||
public stopScreensharingStream(stream: MockMediaStream) {
|
||||
stream.isStopped = true;
|
||||
}
|
||||
public hasAudioDevice() { return true; }
|
||||
public hasVideoDevice() { return true; }
|
||||
public stopAllStreams() {}
|
||||
|
||||
public typed(): MediaHandler { return this as unknown as MediaHandler; }
|
||||
}
|
||||
|
||||
export class MockMediaDevices {
|
||||
public enumerateDevices = jest.fn<Promise<MediaDeviceInfo[]>, []>().mockResolvedValue([
|
||||
new MockMediaDeviceInfo("audioinput").typed(),
|
||||
new MockMediaDeviceInfo("videoinput").typed(),
|
||||
]);
|
||||
|
||||
public getUserMedia = jest.fn<Promise<MediaStream>, [MediaStreamConstraints]>().mockReturnValue(
|
||||
Promise.resolve(new MockMediaStream("local_stream").typed()),
|
||||
);
|
||||
|
||||
public getDisplayMedia = jest.fn<Promise<MediaStream>, [DisplayMediaStreamConstraints]>().mockReturnValue(
|
||||
Promise.resolve(new MockMediaStream("local_display_stream").typed()),
|
||||
);
|
||||
|
||||
public typed(): MediaDevices { return this as unknown as MediaDevices; }
|
||||
}
|
||||
|
||||
type EmittedEvents = CallEventHandlerEvent | CallEvent | ClientEvent | RoomStateEvent | GroupCallEventHandlerEvent;
|
||||
type EmittedEventMap = CallEventHandlerEventHandlerMap &
|
||||
CallEventHandlerMap &
|
||||
ClientEventHandlerMap &
|
||||
RoomStateEventHandlerMap &
|
||||
GroupCallEventHandlerMap;
|
||||
|
||||
export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, EmittedEventMap> {
|
||||
public mediaHandler = new MockMediaHandler();
|
||||
|
||||
constructor(public userId: string, public deviceId: string, public sessionId: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
public groupCallEventHandler = {
|
||||
groupCalls: new Map<string, GroupCall>(),
|
||||
};
|
||||
|
||||
public callEventHandler = {
|
||||
calls: new Map<string, MatrixCall>(),
|
||||
};
|
||||
|
||||
public sendStateEvent = jest.fn<Promise<ISendEventResponse>, [
|
||||
roomId: string, eventType: EventType, content: any, statekey: string,
|
||||
]>();
|
||||
public sendToDevice = jest.fn<Promise<{}>, [
|
||||
eventType: string,
|
||||
contentMap: { [userId: string]: { [deviceId: string]: Record<string, any> } },
|
||||
txnId?: string,
|
||||
]>();
|
||||
|
||||
public getMediaHandler(): MediaHandler { return this.mediaHandler.typed(); }
|
||||
|
||||
public getUserId(): string { return this.userId; }
|
||||
|
||||
public getDeviceId(): string { return this.deviceId; }
|
||||
public getSessionId(): string { return this.sessionId; }
|
||||
|
||||
public getTurnServers = () => [];
|
||||
public isFallbackICEServerAllowed = () => false;
|
||||
public reEmitter = new ReEmitter(new TypedEventEmitter());
|
||||
public getUseE2eForGroupCall = () => false;
|
||||
public checkTurnServers = () => null;
|
||||
|
||||
public getSyncState = jest.fn<SyncState | null, []>().mockReturnValue(SyncState.Syncing);
|
||||
|
||||
public getRooms = jest.fn<Room[], []>().mockReturnValue([]);
|
||||
public getRoom = jest.fn();
|
||||
|
||||
public typed(): MatrixClient { return this as unknown as MatrixClient; }
|
||||
|
||||
public emitRoomState(event: MatrixEvent, state: RoomState): void {
|
||||
this.emit(
|
||||
RoomStateEvent.Events,
|
||||
event,
|
||||
state,
|
||||
null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class MockCallFeed {
|
||||
constructor(
|
||||
public userId: string,
|
||||
public stream: MockMediaStream,
|
||||
) {}
|
||||
|
||||
public measureVolumeActivity(val: boolean) {}
|
||||
public dispose() {}
|
||||
|
||||
public typed(): CallFeed {
|
||||
return this as unknown as CallFeed;
|
||||
}
|
||||
}
|
||||
|
||||
export function installWebRTCMocks() {
|
||||
global.navigator = {
|
||||
mediaDevices: new MockMediaDevices().typed(),
|
||||
} as unknown as Navigator;
|
||||
|
||||
global.window = {
|
||||
// @ts-ignore Mock
|
||||
RTCPeerConnection: MockRTCPeerConnection,
|
||||
// @ts-ignore Mock
|
||||
RTCSessionDescription: {},
|
||||
// @ts-ignore Mock
|
||||
RTCIceCandidate: {},
|
||||
getUserMedia: () => new MockMediaStream("local_stream"),
|
||||
};
|
||||
// @ts-ignore Mock
|
||||
global.document = {};
|
||||
|
||||
// @ts-ignore Mock
|
||||
global.AudioContext = MockAudioContext;
|
||||
|
||||
// @ts-ignore Mock
|
||||
global.RTCRtpReceiver = {
|
||||
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
|
||||
codecs: [],
|
||||
headerExtensions: [],
|
||||
}),
|
||||
};
|
||||
|
||||
// @ts-ignore Mock
|
||||
global.RTCRtpSender = {
|
||||
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
|
||||
codecs: [],
|
||||
headerExtensions: [],
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function makeMockGroupCallStateEvent(roomId: string, groupCallId: string, content: IContent = {
|
||||
"m.type": GroupCallType.Video,
|
||||
"m.intent": GroupCallIntent.Prompt,
|
||||
}): MatrixEvent {
|
||||
return {
|
||||
getType: jest.fn().mockReturnValue(EventType.GroupCallPrefix),
|
||||
getRoomId: jest.fn().mockReturnValue(roomId),
|
||||
getTs: jest.fn().mockReturnValue(0),
|
||||
getContent: jest.fn().mockReturnValue(content),
|
||||
getStateKey: jest.fn().mockReturnValue(groupCallId),
|
||||
} as unknown as MatrixEvent;
|
||||
}
|
||||
|
||||
export function makeMockGroupCallMemberStateEvent(roomId: string, groupCallId: string): MatrixEvent {
|
||||
return {
|
||||
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
||||
getRoomId: jest.fn().mockReturnValue(roomId),
|
||||
getTs: jest.fn().mockReturnValue(0),
|
||||
getContent: jest.fn().mockReturnValue({}),
|
||||
getStateKey: jest.fn().mockReturnValue(groupCallId),
|
||||
} as unknown as MatrixEvent;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ describe("NamespacedValue", () => {
|
||||
});
|
||||
|
||||
it("should have a falsey unstable if needed", () => {
|
||||
const ns = new NamespacedValue("stable", null);
|
||||
const ns = new NamespacedValue("stable");
|
||||
expect(ns.name).toBe(ns.stable);
|
||||
expect(ns.altName).toBeFalsy();
|
||||
expect(ns.names).toEqual([ns.stable]);
|
||||
@@ -41,17 +41,17 @@ describe("NamespacedValue", () => {
|
||||
it("should match against either stable or unstable", () => {
|
||||
const ns = new NamespacedValue("stable", "unstable");
|
||||
expect(ns.matches("no")).toBe(false);
|
||||
expect(ns.matches(ns.stable)).toBe(true);
|
||||
expect(ns.matches(ns.unstable)).toBe(true);
|
||||
expect(ns.matches(ns.stable!)).toBe(true);
|
||||
expect(ns.matches(ns.unstable!)).toBe(true);
|
||||
});
|
||||
|
||||
it("should not permit falsey values for both parts", () => {
|
||||
try {
|
||||
new UnstableValue(null, null);
|
||||
new UnstableValue(null!, null!);
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Failed to fail");
|
||||
} catch (e) {
|
||||
expect(e.message).toBe("One of stable or unstable values must be supplied");
|
||||
expect((<Error>e).message).toBe("One of stable or unstable values must be supplied");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -65,7 +65,7 @@ describe("UnstableValue", () => {
|
||||
});
|
||||
|
||||
it("should return unstable if there is no stable", () => {
|
||||
const ns = new UnstableValue(null, "unstable");
|
||||
const ns = new UnstableValue(null!, "unstable");
|
||||
expect(ns.name).toBe(ns.unstable);
|
||||
expect(ns.altName).toBeFalsy();
|
||||
expect(ns.names).toEqual([ns.unstable]);
|
||||
@@ -73,11 +73,11 @@ describe("UnstableValue", () => {
|
||||
|
||||
it("should not permit falsey unstable values", () => {
|
||||
try {
|
||||
new UnstableValue("stable", null);
|
||||
new UnstableValue("stable", null!);
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Failed to fail");
|
||||
} catch (e) {
|
||||
expect(e.message).toBe("Unstable value must be supplied");
|
||||
expect((<Error>e).message).toBe("Unstable value must be supplied");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -678,7 +678,7 @@ describe("AutoDiscovery", function() {
|
||||
|
||||
it("should return FAIL_PROMPT for connection errors", () => {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").fail(0, undefined);
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").fail(0, undefined!);
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
Copyright 2022 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 { getHttpUriForMxc } from "../../src/content-repo";
|
||||
|
||||
describe("ContentRepo", function() {
|
||||
|
||||
+140
-75
@@ -2,6 +2,7 @@ import '../olm-loader';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import type { PkDecryption, PkSigning } from "@matrix-org/olm";
|
||||
import { MatrixClient } from "../../src/client";
|
||||
import { Crypto } from "../../src/crypto";
|
||||
import { MemoryCryptoStore } from "../../src/crypto/store/memory-crypto-store";
|
||||
@@ -32,7 +33,7 @@ function awaitEvent(emitter, event) {
|
||||
async function keyshareEventForEvent(client, event, index): Promise<MatrixEvent> {
|
||||
const roomId = event.getRoomId();
|
||||
const eventContent = event.getWireContent();
|
||||
const key = await client.crypto.olmDevice.getInboundGroupSessionKey(
|
||||
const key = await client.crypto!.olmDevice.getInboundGroupSessionKey(
|
||||
roomId,
|
||||
eventContent.sender_key,
|
||||
eventContent.session_id,
|
||||
@@ -68,10 +69,10 @@ async function keyshareEventForEvent(client, event, index): Promise<MatrixEvent>
|
||||
function roomKeyEventForEvent(client: MatrixClient, event: MatrixEvent): MatrixEvent {
|
||||
const roomId = event.getRoomId();
|
||||
const eventContent = event.getWireContent();
|
||||
const key = client.crypto.olmDevice.getOutboundGroupSessionKey(eventContent.session_id);
|
||||
const key = client.crypto!.olmDevice.getOutboundGroupSessionKey(eventContent.session_id);
|
||||
const ksEvent = new MatrixEvent({
|
||||
type: "m.room_key",
|
||||
sender: client.getUserId(),
|
||||
sender: client.getUserId()!,
|
||||
content: {
|
||||
"algorithm": olmlib.MEGOLM_ALGORITHM,
|
||||
"room_id": roomId,
|
||||
@@ -146,7 +147,7 @@ describe("Crypto", function() {
|
||||
'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
|
||||
device.keys["ed25519:FLIBBLE"] =
|
||||
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
|
||||
client.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||
client.crypto!.deviceList.getDeviceByIdentityKey = () => device;
|
||||
|
||||
encryptionInfo = client.getEventEncryptionInfo(event);
|
||||
expect(encryptionInfo.encrypted).toBeTruthy();
|
||||
@@ -334,7 +335,7 @@ describe("Crypto", function() {
|
||||
await Promise.all(events.map(async (event) => {
|
||||
// alice encrypts each event, and then bob tries to decrypt
|
||||
// them without any keys, so that they'll be in pending
|
||||
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||
await aliceClient.crypto!.encryptEvent(event, aliceRoom);
|
||||
// remove keys from the event
|
||||
// @ts-ignore private properties
|
||||
event.clearEvent = undefined;
|
||||
@@ -343,17 +344,17 @@ describe("Crypto", function() {
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto.decryptEvent(event);
|
||||
await bobClient.crypto!.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
}));
|
||||
|
||||
const device = new DeviceInfo(aliceClient.deviceId);
|
||||
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||
const device = new DeviceInfo(aliceClient.deviceId!);
|
||||
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
|
||||
|
||||
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
|
||||
@@ -365,14 +366,14 @@ describe("Crypto", function() {
|
||||
// the first message can't be decrypted yet, but the second one
|
||||
// can
|
||||
let ksEvent = await keyshareEventForEvent(aliceClient, events[1], 1);
|
||||
bobClient.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
bobClient.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
await decryptEventsPromise;
|
||||
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
|
||||
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
|
||||
const cryptoStore = bobClient.crypto.cryptoStore;
|
||||
const cryptoStore = bobClient.crypto!.cryptoStore;
|
||||
const eventContent = events[0].getWireContent();
|
||||
const senderKey = eventContent.sender_key;
|
||||
const sessionId = eventContent.session_id;
|
||||
@@ -437,7 +438,7 @@ describe("Crypto", function() {
|
||||
});
|
||||
// alice encrypts each event, and then bob tries to decrypt
|
||||
// them without any keys, so that they'll be in pending
|
||||
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||
await aliceClient.crypto!.encryptEvent(event, aliceRoom);
|
||||
// remove keys from the event
|
||||
// @ts-ignore private property
|
||||
event.clearEvent = undefined;
|
||||
@@ -446,24 +447,24 @@ describe("Crypto", function() {
|
||||
// @ts-ignore private property
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto.decryptEvent(event);
|
||||
await bobClient.crypto!.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
|
||||
const device = new DeviceInfo(aliceClient.deviceId);
|
||||
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||
const device = new DeviceInfo(aliceClient.deviceId!);
|
||||
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
|
||||
|
||||
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
|
||||
const ksEvent = await keyshareEventForEvent(aliceClient, event, 1);
|
||||
ksEvent.getContent().sender_key = undefined; // test
|
||||
bobClient.crypto.olmDevice.addInboundGroupSession = jest.fn();
|
||||
bobClient.crypto!.olmDevice.addInboundGroupSession = jest.fn();
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
expect(bobClient.crypto.olmDevice.addInboundGroupSession).not.toHaveBeenCalled();
|
||||
expect(bobClient.crypto!.olmDevice.addInboundGroupSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("creates a new keyshare request if we request a keyshare", async function() {
|
||||
@@ -479,7 +480,7 @@ describe("Crypto", function() {
|
||||
},
|
||||
});
|
||||
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
|
||||
const cryptoStore = aliceClient.crypto.cryptoStore;
|
||||
const cryptoStore = aliceClient.crypto!.cryptoStore;
|
||||
const roomKeyRequestBody = {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: "!someroom",
|
||||
@@ -514,7 +515,7 @@ describe("Crypto", function() {
|
||||
// let the client set up enough for that to happen, so gut-wrench a bit
|
||||
// to force it to send now.
|
||||
// @ts-ignore
|
||||
aliceClient.crypto.outgoingRoomKeyRequestManager.sendQueuedRequests();
|
||||
aliceClient.crypto!.outgoingRoomKeyRequestManager.sendQueuedRequests();
|
||||
jest.runAllTimers();
|
||||
await Promise.resolve();
|
||||
expect(aliceSendToDevice).toBeCalledTimes(1);
|
||||
@@ -571,7 +572,7 @@ describe("Crypto", function() {
|
||||
await Promise.all(events.map(async (event) => {
|
||||
// alice encrypts each event, and then bob tries to decrypt
|
||||
// them without any keys, so that they'll be in pending
|
||||
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||
await aliceClient.crypto!.encryptEvent(event, aliceRoom);
|
||||
// remove keys from the event
|
||||
// @ts-ignore private properties
|
||||
event.clearEvent = undefined;
|
||||
@@ -580,18 +581,18 @@ describe("Crypto", function() {
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto.decryptEvent(event);
|
||||
await bobClient.crypto!.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
}));
|
||||
|
||||
const device = new DeviceInfo(aliceClient.deviceId);
|
||||
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
const device = new DeviceInfo(aliceClient.deviceId!);
|
||||
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
|
||||
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
|
||||
const cryptoStore = bobClient.crypto.cryptoStore;
|
||||
const cryptoStore = bobClient.crypto!.cryptoStore;
|
||||
const eventContent = events[0].getWireContent();
|
||||
const senderKey = eventContent.sender_key;
|
||||
const sessionId = eventContent.session_id;
|
||||
@@ -604,11 +605,11 @@ describe("Crypto", function() {
|
||||
const outgoingReq = await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody);
|
||||
expect(outgoingReq).toBeDefined();
|
||||
await cryptoStore.updateOutgoingRoomKeyRequest(
|
||||
outgoingReq.requestId, RoomKeyRequestState.Unsent,
|
||||
outgoingReq!.requestId, RoomKeyRequestState.Unsent,
|
||||
{ state: RoomKeyRequestState.Sent },
|
||||
);
|
||||
|
||||
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
|
||||
@@ -617,7 +618,7 @@ describe("Crypto", function() {
|
||||
}));
|
||||
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
|
||||
const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
|
||||
roomId,
|
||||
events[0].getWireContent().sender_key,
|
||||
events[0].getWireContent().session_id,
|
||||
@@ -675,7 +676,7 @@ describe("Crypto", function() {
|
||||
await Promise.all(events.map(async (event) => {
|
||||
// alice encrypts each event, and then bob tries to decrypt
|
||||
// them without any keys, so that they'll be in pending
|
||||
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||
await aliceClient.crypto!.encryptEvent(event, aliceRoom);
|
||||
// remove keys from the event
|
||||
// @ts-ignore private properties
|
||||
event.clearEvent = undefined;
|
||||
@@ -684,18 +685,18 @@ describe("Crypto", function() {
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto.decryptEvent(event);
|
||||
await bobClient.crypto!.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
}));
|
||||
|
||||
const device = new DeviceInfo(claraClient.deviceId);
|
||||
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@clara:example.com";
|
||||
const device = new DeviceInfo(claraClient.deviceId!);
|
||||
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
|
||||
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@clara:example.com";
|
||||
|
||||
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
|
||||
@@ -703,10 +704,10 @@ describe("Crypto", function() {
|
||||
return awaitEvent(ev, "Event.decrypted");
|
||||
}));
|
||||
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
|
||||
ksEvent.event.sender = claraClient.getUserId(),
|
||||
ksEvent.sender = new RoomMember(roomId, claraClient.getUserId());
|
||||
ksEvent.event.sender = claraClient.getUserId()!;
|
||||
ksEvent.sender = new RoomMember(roomId, claraClient.getUserId()!);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
|
||||
const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
|
||||
roomId,
|
||||
events[0].getWireContent().sender_key,
|
||||
events[0].getWireContent().session_id,
|
||||
@@ -753,7 +754,7 @@ describe("Crypto", function() {
|
||||
await Promise.all(events.map(async (event) => {
|
||||
// alice encrypts each event, and then bob tries to decrypt
|
||||
// them without any keys, so that they'll be in pending
|
||||
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||
await aliceClient.crypto!.encryptEvent(event, aliceRoom);
|
||||
// remove keys from the event
|
||||
// @ts-ignore private properties
|
||||
event.clearEvent = undefined;
|
||||
@@ -762,19 +763,19 @@ describe("Crypto", function() {
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto.decryptEvent(event);
|
||||
await bobClient.crypto!.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
}));
|
||||
|
||||
const device = new DeviceInfo(claraClient.deviceId);
|
||||
const device = new DeviceInfo(claraClient.deviceId!);
|
||||
device.verified = DeviceInfo.DeviceVerification.VERIFIED;
|
||||
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@bob:example.com";
|
||||
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
|
||||
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@bob:example.com";
|
||||
|
||||
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
|
||||
@@ -782,10 +783,10 @@ describe("Crypto", function() {
|
||||
return awaitEvent(ev, "Event.decrypted");
|
||||
}));
|
||||
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
|
||||
ksEvent.event.sender = bobClient.getUserId(),
|
||||
ksEvent.sender = new RoomMember(roomId, bobClient.getUserId());
|
||||
ksEvent.event.sender = bobClient.getUserId()!;
|
||||
ksEvent.sender = new RoomMember(roomId, bobClient.getUserId()!);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
|
||||
const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
|
||||
roomId,
|
||||
events[0].getWireContent().sender_key,
|
||||
events[0].getWireContent().session_id,
|
||||
@@ -835,7 +836,7 @@ describe("Crypto", function() {
|
||||
await Promise.all(events.map(async (event) => {
|
||||
// alice encrypts each event, and then bob tries to decrypt
|
||||
// them without any keys, so that they'll be in pending
|
||||
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||
await aliceClient.crypto!.encryptEvent(event, aliceRoom);
|
||||
// remove keys from the event
|
||||
// @ts-ignore private properties
|
||||
event.clearEvent = undefined;
|
||||
@@ -844,26 +845,26 @@ describe("Crypto", function() {
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto.decryptEvent(event);
|
||||
await bobClient.crypto!.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
}));
|
||||
|
||||
const device = new DeviceInfo(claraClient.deviceId);
|
||||
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
const device = new DeviceInfo(claraClient.deviceId!);
|
||||
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
|
||||
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
|
||||
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
|
||||
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
|
||||
ksEvent.event.sender = claraClient.getUserId(),
|
||||
ksEvent.sender = new RoomMember(roomId, claraClient.getUserId());
|
||||
ksEvent.event.sender = claraClient.getUserId()!;
|
||||
ksEvent.sender = new RoomMember(roomId, claraClient.getUserId()!);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
|
||||
const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
|
||||
roomId,
|
||||
events[0].getWireContent().sender_key,
|
||||
events[0].getWireContent().session_id,
|
||||
@@ -904,7 +905,7 @@ describe("Crypto", function() {
|
||||
await Promise.all(events.map(async (event) => {
|
||||
// alice encrypts each event, and then bob tries to decrypt
|
||||
// them without any keys, so that they'll be in pending
|
||||
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||
await aliceClient.crypto!.encryptEvent(event, aliceRoom);
|
||||
// remove keys from the event
|
||||
// @ts-ignore private properties
|
||||
event.clearEvent = undefined;
|
||||
@@ -914,11 +915,11 @@ describe("Crypto", function() {
|
||||
event.claimedEd25519Key = null;
|
||||
}));
|
||||
|
||||
const device = new DeviceInfo(aliceClient.deviceId);
|
||||
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
const device = new DeviceInfo(aliceClient.deviceId!);
|
||||
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
|
||||
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
|
||||
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
|
||||
@@ -926,25 +927,25 @@ describe("Crypto", function() {
|
||||
|
||||
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
const bobKey = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
|
||||
const bobKey = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
|
||||
roomId,
|
||||
content.sender_key,
|
||||
content.session_id,
|
||||
);
|
||||
expect(bobKey).toBeNull();
|
||||
|
||||
const aliceKey = await aliceClient.crypto.olmDevice.getInboundGroupSessionKey(
|
||||
const aliceKey = await aliceClient.crypto!.olmDevice.getInboundGroupSessionKey(
|
||||
roomId,
|
||||
content.sender_key,
|
||||
content.session_id,
|
||||
);
|
||||
const parked = await bobClient.crypto.cryptoStore.takeParkedSharedHistory(roomId);
|
||||
const parked = await bobClient.crypto!.cryptoStore.takeParkedSharedHistory(roomId);
|
||||
expect(parked).toEqual([{
|
||||
senderId: aliceClient.getUserId(),
|
||||
senderKey: content.sender_key,
|
||||
sessionId: content.session_id,
|
||||
sessionKey: aliceKey.key,
|
||||
keysClaimed: { ed25519: aliceKey.sender_claimed_ed25519_key },
|
||||
sessionKey: aliceKey!.key,
|
||||
keysClaimed: { ed25519: aliceKey!.sender_claimed_ed25519_key },
|
||||
forwardingCurve25519KeyChain: ["akey"],
|
||||
}]);
|
||||
});
|
||||
@@ -956,19 +957,19 @@ describe("Crypto", function() {
|
||||
jest.setTimeout(10000);
|
||||
const client = (new TestClient("@a:example.com", "dev")).client;
|
||||
await client.initCrypto();
|
||||
client.crypto.getSecretStorageKey = jest.fn().mockResolvedValue(null);
|
||||
client.crypto.isCrossSigningReady = async () => false;
|
||||
client.crypto.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null);
|
||||
client.crypto.baseApis.setAccountData = jest.fn().mockResolvedValue(null);
|
||||
client.crypto.baseApis.uploadKeySignatures = jest.fn();
|
||||
client.crypto.baseApis.http.authedRequest = jest.fn();
|
||||
client.crypto!.getSecretStorageKey = jest.fn().mockResolvedValue(null);
|
||||
client.crypto!.isCrossSigningReady = async () => false;
|
||||
client.crypto!.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null);
|
||||
client.crypto!.baseApis.setAccountData = jest.fn().mockResolvedValue(null);
|
||||
client.crypto!.baseApis.uploadKeySignatures = jest.fn();
|
||||
client.crypto!.baseApis.http.authedRequest = jest.fn();
|
||||
const createSecretStorageKey = async () => {
|
||||
return {
|
||||
keyInfo: undefined, // Returning undefined here used to cause a crash
|
||||
privateKey: Uint8Array.of(32, 33),
|
||||
};
|
||||
};
|
||||
await client.crypto.bootstrapSecretStorage({
|
||||
await client.crypto!.bootstrapSecretStorage({
|
||||
createSecretStorageKey,
|
||||
});
|
||||
client.stopClient();
|
||||
@@ -995,7 +996,7 @@ describe("Crypto", function() {
|
||||
|
||||
encryptedPayload = {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
sender_key: client.client.crypto.olmDevice.deviceCurve25519Key,
|
||||
sender_key: client.client.crypto!.olmDevice.deviceCurve25519Key,
|
||||
ciphertext: { plaintext: JSON.stringify(payload) },
|
||||
};
|
||||
});
|
||||
@@ -1075,4 +1076,68 @@ describe("Crypto", function() {
|
||||
client.httpBackend.verifyNoOutstandingRequests();
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkSecretStoragePrivateKey", () => {
|
||||
let client: TestClient;
|
||||
|
||||
beforeEach(async () => {
|
||||
client = new TestClient("@alice:example.org", "aliceweb");
|
||||
await client.client.initCrypto();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await client.stop();
|
||||
});
|
||||
|
||||
it("should free PkDecryption", () => {
|
||||
const free = jest.fn();
|
||||
jest.spyOn(Olm, "PkDecryption").mockImplementation(() => ({
|
||||
init_with_private_key: jest.fn(),
|
||||
free,
|
||||
}) as unknown as PkDecryption);
|
||||
client.client.checkSecretStoragePrivateKey(new Uint8Array(), "");
|
||||
expect(free).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkCrossSigningPrivateKey", () => {
|
||||
let client: TestClient;
|
||||
|
||||
beforeEach(async () => {
|
||||
client = new TestClient("@alice:example.org", "aliceweb");
|
||||
await client.client.initCrypto();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await client.stop();
|
||||
});
|
||||
|
||||
it("should free PkSigning", () => {
|
||||
const free = jest.fn();
|
||||
jest.spyOn(Olm, "PkSigning").mockImplementation(() => ({
|
||||
init_with_seed: jest.fn(),
|
||||
free,
|
||||
}) as unknown as PkSigning);
|
||||
client.client.checkCrossSigningPrivateKey(new Uint8Array(), "");
|
||||
expect(free).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("start", () => {
|
||||
let client: TestClient;
|
||||
|
||||
beforeEach(async () => {
|
||||
client = new TestClient("@alice:example.org", "aliceweb");
|
||||
await client.client.initCrypto();
|
||||
});
|
||||
|
||||
afterEach(async function() {
|
||||
await client!.stop();
|
||||
});
|
||||
|
||||
// start() is a no-op nowadays, so there's not much to test here.
|
||||
it("should complete successfully", async () => {
|
||||
await client!.client.crypto!.start();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -222,9 +222,9 @@ describe.each([
|
||||
["IndexedDBCryptoStore",
|
||||
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
|
||||
["LocalStorageCryptoStore",
|
||||
() => new IndexedDBCryptoStore(undefined, "tests")],
|
||||
() => new IndexedDBCryptoStore(undefined!, "tests")],
|
||||
["MemoryCryptoStore", () => {
|
||||
const store = new IndexedDBCryptoStore(undefined, "tests");
|
||||
const store = new IndexedDBCryptoStore(undefined!, "tests");
|
||||
// @ts-ignore set private properties
|
||||
store._backend = new MemoryCryptoStore();
|
||||
// @ts-ignore
|
||||
@@ -247,14 +247,14 @@ describe.each([
|
||||
const olmDevice = new OlmDevice(store);
|
||||
const { getCrossSigningKeyCache, storeCrossSigningKeyCache } =
|
||||
createCryptoStoreCacheCallbacks(store, olmDevice);
|
||||
await storeCrossSigningKeyCache("self_signing", testKey);
|
||||
await storeCrossSigningKeyCache!("self_signing", testKey);
|
||||
|
||||
// If we've not saved anything, don't expect anything
|
||||
// Definitely don't accidentally return the wrong key for the type
|
||||
const nokey = await getCrossSigningKeyCache("self", "");
|
||||
const nokey = await getCrossSigningKeyCache!("self", "");
|
||||
expect(nokey).toBeNull();
|
||||
|
||||
const key = await getCrossSigningKeyCache("self_signing", "");
|
||||
expect(new Uint8Array(key)).toEqual(testKey);
|
||||
const key = await getCrossSigningKeyCache!("self_signing", "");
|
||||
expect(new Uint8Array(key!)).toEqual(testKey);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -90,7 +90,7 @@ const signedDeviceList2: IDownloadKeyResult = {
|
||||
describe('DeviceList', function() {
|
||||
let downloadSpy;
|
||||
let cryptoStore;
|
||||
let deviceLists = [];
|
||||
let deviceLists: DeviceList[] = [];
|
||||
|
||||
beforeEach(function() {
|
||||
deviceLists = [];
|
||||
|
||||
@@ -32,8 +32,8 @@ import { ClientEvent, MatrixClient, RoomMember } from '../../../../src';
|
||||
import { DeviceInfo, IDevice } from '../../../../src/crypto/deviceinfo';
|
||||
import { DeviceTrustLevel } from '../../../../src/crypto/CrossSigning';
|
||||
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
|
||||
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2')!;
|
||||
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get('m.megolm.v1.aes-sha2')!;
|
||||
|
||||
const ROOM_ID = '!ROOM:ID';
|
||||
|
||||
@@ -331,7 +331,7 @@ describe("MegolmDecryption", function() {
|
||||
},
|
||||
},
|
||||
});
|
||||
mockBaseApis.sendToDevice.mockResolvedValue(undefined);
|
||||
mockBaseApis.sendToDevice.mockResolvedValue({});
|
||||
mockBaseApis.queueToDevice.mockResolvedValue(undefined);
|
||||
|
||||
aliceDeviceInfo = {
|
||||
@@ -493,9 +493,9 @@ describe("MegolmDecryption", function() {
|
||||
bobClient1.initCrypto(),
|
||||
bobClient2.initCrypto(),
|
||||
]);
|
||||
const aliceDevice = aliceClient.crypto.olmDevice;
|
||||
const bobDevice1 = bobClient1.crypto.olmDevice;
|
||||
const bobDevice2 = bobClient2.crypto.olmDevice;
|
||||
const aliceDevice = aliceClient.crypto!.olmDevice;
|
||||
const bobDevice1 = bobClient1.crypto!.olmDevice;
|
||||
const bobDevice2 = bobClient2.crypto!.olmDevice;
|
||||
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
@@ -515,8 +515,8 @@ describe("MegolmDecryption", function() {
|
||||
bobdevice1: {
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
keys: {
|
||||
"ed25519:Dynabook": bobDevice1.deviceEd25519Key,
|
||||
"curve25519:Dynabook": bobDevice1.deviceCurve25519Key,
|
||||
"ed25519:Dynabook": bobDevice1.deviceEd25519Key!,
|
||||
"curve25519:Dynabook": bobDevice1.deviceCurve25519Key!,
|
||||
},
|
||||
verified: 0,
|
||||
known: false,
|
||||
@@ -524,18 +524,19 @@ describe("MegolmDecryption", function() {
|
||||
bobdevice2: {
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
keys: {
|
||||
"ed25519:Dynabook": bobDevice2.deviceEd25519Key,
|
||||
"curve25519:Dynabook": bobDevice2.deviceCurve25519Key,
|
||||
"ed25519:Dynabook": bobDevice2.deviceEd25519Key!,
|
||||
"curve25519:Dynabook": bobDevice2.deviceCurve25519Key!,
|
||||
},
|
||||
verified: -1,
|
||||
known: false,
|
||||
},
|
||||
};
|
||||
|
||||
aliceClient.crypto.deviceList.storeDevicesForUser(
|
||||
aliceClient.crypto!.deviceList.storeDevicesForUser(
|
||||
"@bob:example.com", BOB_DEVICES,
|
||||
);
|
||||
aliceClient.crypto.deviceList.downloadKeys = async function(userIds) {
|
||||
aliceClient.crypto!.deviceList.downloadKeys = async function(userIds) {
|
||||
// @ts-ignore short-circuiting private method
|
||||
return this.getDevicesFromStore(userIds);
|
||||
};
|
||||
|
||||
@@ -551,7 +552,7 @@ describe("MegolmDecryption", function() {
|
||||
body: "secret",
|
||||
},
|
||||
});
|
||||
await aliceClient.crypto.encryptEvent(event, room);
|
||||
await aliceClient.crypto!.encryptEvent(event, room);
|
||||
|
||||
expect(aliceClient.sendToDevice).toHaveBeenCalled();
|
||||
const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0];
|
||||
@@ -583,6 +584,100 @@ describe("MegolmDecryption", function() {
|
||||
bobClient2.stopClient();
|
||||
});
|
||||
|
||||
it("does not block unverified devices when sending verification events", async function() {
|
||||
const aliceClient = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
)).client;
|
||||
const bobClient = (new TestClient(
|
||||
"@bob:example.com", "bobdevice",
|
||||
)).client;
|
||||
await Promise.all([
|
||||
aliceClient.initCrypto(),
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
const bobDevice = bobClient.crypto!.olmDevice;
|
||||
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
};
|
||||
const roomId = "!someroom";
|
||||
const room = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||
|
||||
const bobMember = new RoomMember(roomId, "@bob:example.com");
|
||||
room.getEncryptionTargetMembers = async function() {
|
||||
return [bobMember];
|
||||
};
|
||||
room.setBlacklistUnverifiedDevices(true);
|
||||
aliceClient.store.storeRoom(room);
|
||||
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
|
||||
const BOB_DEVICES: Record<string, IDevice> = {
|
||||
bobdevice: {
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
keys: {
|
||||
"ed25519:bobdevice": bobDevice.deviceEd25519Key!,
|
||||
"curve25519:bobdevice": bobDevice.deviceCurve25519Key!,
|
||||
},
|
||||
verified: 0,
|
||||
known: true,
|
||||
},
|
||||
};
|
||||
|
||||
aliceClient.crypto!.deviceList.storeDevicesForUser(
|
||||
"@bob:example.com", BOB_DEVICES,
|
||||
);
|
||||
aliceClient.crypto!.deviceList.downloadKeys = async function(userIds) {
|
||||
// @ts-ignore private
|
||||
return this.getDevicesFromStore(userIds);
|
||||
};
|
||||
|
||||
await bobDevice.generateOneTimeKeys(1);
|
||||
const oneTimeKeys = await bobDevice.getOneTimeKeys();
|
||||
const signedOneTimeKeys: Record<string, { key: string, signatures: object }> = {};
|
||||
for (const keyId in oneTimeKeys.curve25519) {
|
||||
if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) {
|
||||
const k = {
|
||||
key: oneTimeKeys.curve25519[keyId],
|
||||
signatures: {},
|
||||
};
|
||||
signedOneTimeKeys["signed_curve25519:" + keyId] = k;
|
||||
await bobClient.crypto!.signObject(k);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
aliceClient.claimOneTimeKeys = jest.fn().mockResolvedValue({
|
||||
one_time_keys: {
|
||||
'@bob:example.com': {
|
||||
bobdevice: signedOneTimeKeys,
|
||||
},
|
||||
},
|
||||
failures: {},
|
||||
});
|
||||
|
||||
aliceClient.sendToDevice = jest.fn().mockResolvedValue({});
|
||||
|
||||
const event = new MatrixEvent({
|
||||
type: "m.key.verification.start",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$event",
|
||||
content: {
|
||||
from_device: "alicedevice",
|
||||
method: "m.sas.v1",
|
||||
transaction_id: "transactionid",
|
||||
},
|
||||
});
|
||||
await aliceClient.crypto!.encryptEvent(event, room);
|
||||
|
||||
expect(aliceClient.sendToDevice).toHaveBeenCalled();
|
||||
const [msgtype] = mocked(aliceClient.sendToDevice).mock.calls[0];
|
||||
expect(msgtype).toEqual("m.room.encrypted");
|
||||
|
||||
aliceClient.stopClient();
|
||||
bobClient.stopClient();
|
||||
});
|
||||
|
||||
it("notifies devices when unable to create olm session", async function() {
|
||||
const aliceClient = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
@@ -594,8 +689,8 @@ describe("MegolmDecryption", function() {
|
||||
aliceClient.initCrypto(),
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
const aliceDevice = aliceClient.crypto.olmDevice;
|
||||
const bobDevice = bobClient.crypto.olmDevice;
|
||||
const aliceDevice = aliceClient.crypto!.olmDevice;
|
||||
const bobDevice = bobClient.crypto!.olmDevice;
|
||||
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
@@ -624,18 +719,19 @@ describe("MegolmDecryption", function() {
|
||||
device_id: "bobdevice",
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
keys: {
|
||||
"ed25519:bobdevice": bobDevice.deviceEd25519Key,
|
||||
"curve25519:bobdevice": bobDevice.deviceCurve25519Key,
|
||||
"ed25519:bobdevice": bobDevice.deviceEd25519Key!,
|
||||
"curve25519:bobdevice": bobDevice.deviceCurve25519Key!,
|
||||
},
|
||||
known: true,
|
||||
verified: 1,
|
||||
},
|
||||
};
|
||||
|
||||
aliceClient.crypto.deviceList.storeDevicesForUser(
|
||||
aliceClient.crypto!.deviceList.storeDevicesForUser(
|
||||
"@bob:example.com", BOB_DEVICES,
|
||||
);
|
||||
aliceClient.crypto.deviceList.downloadKeys = async function(userIds) {
|
||||
aliceClient.crypto!.deviceList.downloadKeys = async function(userIds) {
|
||||
// @ts-ignore private
|
||||
return this.getDevicesFromStore(userIds);
|
||||
};
|
||||
|
||||
@@ -654,7 +750,7 @@ describe("MegolmDecryption", function() {
|
||||
event_id: "$event",
|
||||
content: {},
|
||||
});
|
||||
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||
await aliceClient.crypto!.encryptEvent(event, aliceRoom);
|
||||
|
||||
expect(aliceClient.sendToDevice).toHaveBeenCalled();
|
||||
const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0];
|
||||
@@ -685,10 +781,10 @@ describe("MegolmDecryption", function() {
|
||||
aliceClient.initCrypto(),
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
const bobDevice = bobClient.crypto.olmDevice;
|
||||
const bobDevice = bobClient.crypto!.olmDevice;
|
||||
|
||||
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
|
||||
aliceClient.crypto.registerEventHandlers(aliceEventEmitter);
|
||||
aliceClient.crypto!.registerEventHandlers(aliceEventEmitter);
|
||||
|
||||
const roomId = "!someroom";
|
||||
|
||||
@@ -705,7 +801,7 @@ describe("MegolmDecryption", function() {
|
||||
},
|
||||
}));
|
||||
|
||||
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
|
||||
await expect(aliceClient.crypto!.decryptEvent(new MatrixEvent({
|
||||
type: "m.room.encrypted",
|
||||
sender: "@bob:example.com",
|
||||
event_id: "$event",
|
||||
@@ -732,7 +828,7 @@ describe("MegolmDecryption", function() {
|
||||
},
|
||||
}));
|
||||
|
||||
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
|
||||
await expect(aliceClient.crypto!.decryptEvent(new MatrixEvent({
|
||||
type: "m.room.encrypted",
|
||||
sender: "@bob:example.com",
|
||||
event_id: "$event",
|
||||
@@ -762,10 +858,10 @@ describe("MegolmDecryption", function() {
|
||||
]);
|
||||
|
||||
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
|
||||
aliceClient.crypto.registerEventHandlers(aliceEventEmitter);
|
||||
aliceClient.crypto!.registerEventHandlers(aliceEventEmitter);
|
||||
|
||||
aliceClient.crypto.downloadKeys = jest.fn();
|
||||
const bobDevice = bobClient.crypto.olmDevice;
|
||||
aliceClient.crypto!.downloadKeys = jest.fn();
|
||||
const bobDevice = bobClient.crypto!.olmDevice;
|
||||
|
||||
const roomId = "!someroom";
|
||||
|
||||
@@ -788,7 +884,7 @@ describe("MegolmDecryption", function() {
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
|
||||
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
|
||||
await expect(aliceClient.crypto!.decryptEvent(new MatrixEvent({
|
||||
type: "m.room.encrypted",
|
||||
sender: "@bob:example.com",
|
||||
event_id: "$event",
|
||||
@@ -820,7 +916,7 @@ describe("MegolmDecryption", function() {
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
|
||||
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
|
||||
await expect(aliceClient.crypto!.decryptEvent(new MatrixEvent({
|
||||
type: "m.room.encrypted",
|
||||
sender: "@bob:example.com",
|
||||
event_id: "$event",
|
||||
@@ -850,10 +946,10 @@ describe("MegolmDecryption", function() {
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
|
||||
aliceClient.crypto.registerEventHandlers(aliceEventEmitter);
|
||||
aliceClient.crypto!.registerEventHandlers(aliceEventEmitter);
|
||||
|
||||
const bobDevice = bobClient.crypto.olmDevice;
|
||||
aliceClient.crypto.downloadKeys = jest.fn();
|
||||
const bobDevice = bobClient.crypto!.olmDevice;
|
||||
aliceClient.crypto!.downloadKeys = jest.fn();
|
||||
|
||||
const roomId = "!someroom";
|
||||
|
||||
@@ -875,7 +971,7 @@ describe("MegolmDecryption", function() {
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
|
||||
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
|
||||
await expect(aliceClient.crypto!.decryptEvent(new MatrixEvent({
|
||||
type: "m.room.encrypted",
|
||||
sender: "@bob:example.com",
|
||||
event_id: "$event",
|
||||
|
||||
@@ -67,13 +67,13 @@ describe("OlmDevice", function() {
|
||||
const sid = await setupSession(aliceOlmDevice, bobOlmDevice);
|
||||
|
||||
const ciphertext = await aliceOlmDevice.encryptMessage(
|
||||
bobOlmDevice.deviceCurve25519Key,
|
||||
bobOlmDevice.deviceCurve25519Key!,
|
||||
sid,
|
||||
"The olm or proteus is an aquatic salamander in the family Proteidae",
|
||||
) as any; // OlmDevice.encryptMessage has incorrect return type
|
||||
|
||||
const result = await bobOlmDevice.createInboundSession(
|
||||
aliceOlmDevice.deviceCurve25519Key,
|
||||
aliceOlmDevice.deviceCurve25519Key!,
|
||||
ciphertext.type,
|
||||
ciphertext.body,
|
||||
);
|
||||
@@ -94,7 +94,7 @@ describe("OlmDevice", function() {
|
||||
+ " in the family Proteidae"
|
||||
);
|
||||
const ciphertext = await aliceOlmDevice.encryptMessage(
|
||||
bobOlmDevice.deviceCurve25519Key,
|
||||
bobOlmDevice.deviceCurve25519Key!,
|
||||
sessionId,
|
||||
MESSAGE,
|
||||
) as any; // OlmDevice.encryptMessage has incorrect return type
|
||||
@@ -103,7 +103,7 @@ describe("OlmDevice", function() {
|
||||
bobRecreatedOlmDevice.init({ fromExportedDevice: exported });
|
||||
|
||||
const decrypted = await bobRecreatedOlmDevice.createInboundSession(
|
||||
aliceOlmDevice.deviceCurve25519Key,
|
||||
aliceOlmDevice.deviceCurve25519Key!,
|
||||
ciphertext.type,
|
||||
ciphertext.body,
|
||||
);
|
||||
@@ -118,7 +118,7 @@ describe("OlmDevice", function() {
|
||||
+ " the olm is entirely aquatic"
|
||||
);
|
||||
const ciphertext2 = await aliceOlmDevice.encryptMessage(
|
||||
bobOlmDevice.deviceCurve25519Key,
|
||||
bobOlmDevice.deviceCurve25519Key!,
|
||||
sessionId,
|
||||
MESSAGE_2,
|
||||
) as any; // OlmDevice.encryptMessage has incorrect return type
|
||||
@@ -128,7 +128,7 @@ describe("OlmDevice", function() {
|
||||
|
||||
// Note: "decrypted_2" does not have the same structure as "decrypted"
|
||||
const decrypted2 = await bobRecreatedAgainOlmDevice.decryptMessage(
|
||||
aliceOlmDevice.deviceCurve25519Key,
|
||||
aliceOlmDevice.deviceCurve25519Key!,
|
||||
decrypted.session_id,
|
||||
ciphertext2.type,
|
||||
ciphertext2.body,
|
||||
|
||||
@@ -34,7 +34,7 @@ import { MatrixScheduler } from '../../../src';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2')!;
|
||||
|
||||
const ROOM_ID = '!ROOM:ID';
|
||||
|
||||
@@ -197,7 +197,7 @@ describe("MegolmBackup", function() {
|
||||
// to tick the clock between the first try and the retry.
|
||||
const realSetTimeout = global.setTimeout;
|
||||
jest.spyOn(global, 'setTimeout').mockImplementation(function(f, n) {
|
||||
return realSetTimeout(f!, n/100);
|
||||
return realSetTimeout(f!, n!/100);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -318,7 +318,7 @@ describe("MegolmBackup", function() {
|
||||
resolve();
|
||||
return Promise.resolve({} as T);
|
||||
};
|
||||
client.crypto.backupManager.backupGroupSession(
|
||||
client.crypto!.backupManager.backupGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
groupSession.session_id(),
|
||||
);
|
||||
@@ -349,7 +349,7 @@ describe("MegolmBackup", function() {
|
||||
|
||||
return client.initCrypto()
|
||||
.then(() => {
|
||||
return client.crypto.storeSessionBackupPrivateKey(new Uint8Array(32));
|
||||
return client.crypto!.storeSessionBackupPrivateKey(new Uint8Array(32));
|
||||
})
|
||||
.then(() => {
|
||||
return cryptoStore.doTxn(
|
||||
@@ -401,7 +401,7 @@ describe("MegolmBackup", function() {
|
||||
resolve();
|
||||
return Promise.resolve({} as T);
|
||||
};
|
||||
client.crypto.backupManager.backupGroupSession(
|
||||
client.crypto!.backupManager.backupGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
groupSession.session_id(),
|
||||
);
|
||||
@@ -449,7 +449,7 @@ describe("MegolmBackup", function() {
|
||||
try {
|
||||
// make sure auth_data is signed by the master key
|
||||
olmlib.pkVerify(
|
||||
(data as Record<string, any>).auth_data, client.getCrossSigningId(), "@alice:bar",
|
||||
(data as Record<string, any>).auth_data, client.getCrossSigningId()!, "@alice:bar",
|
||||
);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
@@ -568,7 +568,7 @@ describe("MegolmBackup", function() {
|
||||
);
|
||||
}
|
||||
};
|
||||
return client.crypto.backupManager.backupGroupSession(
|
||||
return client.crypto!.backupManager.backupGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
groupSession.session_id(),
|
||||
);
|
||||
@@ -699,4 +699,30 @@ describe("MegolmBackup", function() {
|
||||
)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("flagAllGroupSessionsForBackup", () => {
|
||||
it("should return number of sesions needing backup", async () => {
|
||||
const scheduler = [
|
||||
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
|
||||
"setProcessFunction",
|
||||
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}) as MockedObject<MatrixScheduler>;
|
||||
const store = new StubStore();
|
||||
const client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: "https://identity.server",
|
||||
accessToken: "my.access.token",
|
||||
fetchFn: jest.fn(), // NOP
|
||||
store,
|
||||
scheduler,
|
||||
userId: "@alice:bar",
|
||||
deviceId: "device",
|
||||
cryptoStore,
|
||||
});
|
||||
await client.initCrypto();
|
||||
|
||||
cryptoStore.countSessionsNeedingBackup = jest.fn().mockReturnValue(6);
|
||||
await expect(client.flagAllGroupSessionsForBackup()).resolves.toBe(6);
|
||||
client.stopClient();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -93,8 +93,8 @@ describe("Cross Signing", function() {
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = jest.fn().mockImplementation(async (auth, keys) => {
|
||||
await olmlib.verifySignature(
|
||||
alice.crypto.olmDevice, keys.master_key, "@alice:example.com",
|
||||
"Osborne2", alice.crypto.olmDevice.deviceEd25519Key,
|
||||
alice.crypto!.olmDevice, keys.master_key, "@alice:example.com",
|
||||
"Osborne2", alice.crypto!.olmDevice.deviceEd25519Key!,
|
||||
);
|
||||
});
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
@@ -152,7 +152,7 @@ describe("Cross Signing", function() {
|
||||
authUploadDeviceSigningKeys,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.errcode === "M_FORBIDDEN") {
|
||||
if ((<MatrixError>e).errcode === "M_FORBIDDEN") {
|
||||
bootstrapDidThrow = true;
|
||||
}
|
||||
}
|
||||
@@ -169,7 +169,7 @@ describe("Cross Signing", function() {
|
||||
// set Alice's cross-signing key
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's device key
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@bob:example.com",
|
||||
@@ -238,12 +238,12 @@ describe("Cross Signing", function() {
|
||||
alice.uploadKeySignatures = jest.fn().mockImplementation(async (content) => {
|
||||
try {
|
||||
await olmlib.verifySignature(
|
||||
alice.crypto.olmDevice,
|
||||
alice.crypto!.olmDevice,
|
||||
content["@alice:example.com"][
|
||||
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
|
||||
],
|
||||
"@alice:example.com",
|
||||
"Osborne2", alice.crypto.olmDevice.deviceEd25519Key,
|
||||
"Osborne2", alice.crypto!.olmDevice.deviceEd25519Key!,
|
||||
);
|
||||
olmlib.pkVerify(
|
||||
content["@alice:example.com"]["Osborne2"],
|
||||
@@ -258,7 +258,7 @@ describe("Cross Signing", function() {
|
||||
});
|
||||
|
||||
// @ts-ignore private property
|
||||
const deviceInfo = alice.crypto.deviceList.devices["@alice:example.com"]
|
||||
const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"]
|
||||
.Osborne2;
|
||||
const aliceDevice = {
|
||||
user_id: "@alice:example.com",
|
||||
@@ -266,7 +266,7 @@ describe("Cross Signing", function() {
|
||||
keys: deviceInfo.keys,
|
||||
algorithms: deviceInfo.algorithms,
|
||||
};
|
||||
await alice.crypto.signObject(aliceDevice);
|
||||
await alice.crypto!.signObject(aliceDevice);
|
||||
olmlib.pkSign(
|
||||
aliceDevice as ISignedKey,
|
||||
selfSigningKey as unknown as PkSigning,
|
||||
@@ -401,7 +401,7 @@ describe("Cross Signing", function() {
|
||||
["ed25519:" + bobMasterPubkey]: sskSig,
|
||||
},
|
||||
};
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@bob:example.com",
|
||||
@@ -435,7 +435,7 @@ describe("Cross Signing", function() {
|
||||
verified: 0,
|
||||
known: false,
|
||||
};
|
||||
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: bobDevice,
|
||||
});
|
||||
// Bob's device key should be TOFU
|
||||
@@ -467,11 +467,11 @@ describe("Cross Signing", function() {
|
||||
const aliceKeys: Record<string, PkSigning> = {};
|
||||
const { client: alice, httpBackend } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
null,
|
||||
undefined,
|
||||
aliceKeys,
|
||||
);
|
||||
alice.crypto.deviceList.startTrackingDeviceList("@bob:example.com");
|
||||
alice.crypto.deviceList.stopTrackingAllDeviceLists = () => {};
|
||||
alice.crypto!.deviceList.startTrackingDeviceList("@bob:example.com");
|
||||
alice.crypto!.deviceList.stopTrackingAllDeviceLists = () => {};
|
||||
alice.uploadDeviceSigningKeys = async () => ({});
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
|
||||
@@ -486,7 +486,7 @@ describe("Cross Signing", function() {
|
||||
]);
|
||||
|
||||
const keyChangePromise = new Promise<void>((resolve, reject) => {
|
||||
alice.crypto.deviceList.once(CryptoEvent.UserCrossSigningUpdated, (userId) => {
|
||||
alice.crypto!.deviceList.once(CryptoEvent.UserCrossSigningUpdated, (userId) => {
|
||||
if (userId === "@bob:example.com") {
|
||||
resolve();
|
||||
}
|
||||
@@ -494,7 +494,7 @@ describe("Cross Signing", function() {
|
||||
});
|
||||
|
||||
// @ts-ignore private property
|
||||
const deviceInfo = alice.crypto.deviceList.devices["@alice:example.com"]
|
||||
const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"]
|
||||
.Osborne2;
|
||||
const aliceDevice = {
|
||||
user_id: "@alice:example.com",
|
||||
@@ -502,7 +502,7 @@ describe("Cross Signing", function() {
|
||||
keys: deviceInfo.keys,
|
||||
algorithms: deviceInfo.algorithms,
|
||||
};
|
||||
await alice.crypto.signObject(aliceDevice);
|
||||
await alice.crypto!.signObject(aliceDevice);
|
||||
|
||||
const bobOlmAccount = new global.Olm.Account();
|
||||
bobOlmAccount.create();
|
||||
@@ -667,7 +667,7 @@ describe("Cross Signing", function() {
|
||||
["ed25519:" + bobMasterPubkey]: sskSig,
|
||||
},
|
||||
};
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@bob:example.com",
|
||||
@@ -690,7 +690,7 @@ describe("Cross Signing", function() {
|
||||
"ed25519:Dynabook": "someOtherPubkey",
|
||||
},
|
||||
};
|
||||
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: bobDevice as unknown as IDevice,
|
||||
});
|
||||
// Bob's device key should be untrusted
|
||||
@@ -735,7 +735,7 @@ describe("Cross Signing", function() {
|
||||
["ed25519:" + bobMasterPubkey]: sskSig,
|
||||
},
|
||||
};
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@bob:example.com",
|
||||
@@ -770,7 +770,7 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
},
|
||||
};
|
||||
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: bobDevice,
|
||||
});
|
||||
// Alice verifies Bob's SSK
|
||||
@@ -802,7 +802,7 @@ describe("Cross Signing", function() {
|
||||
["ed25519:" + bobMasterPubkey2]: sskSig2,
|
||||
},
|
||||
};
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@bob:example.com",
|
||||
@@ -838,8 +838,8 @@ describe("Cross Signing", function() {
|
||||
|
||||
// Alice gets new signature for device
|
||||
const sig2 = bobSigning2.sign(bobDeviceString);
|
||||
bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey2] = sig2;
|
||||
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
bobDevice.signatures!["@bob:example.com"]["ed25519:" + bobPubkey2] = sig2;
|
||||
alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: bobDevice,
|
||||
});
|
||||
|
||||
@@ -876,20 +876,20 @@ describe("Cross Signing", function() {
|
||||
bob.uploadKeySignatures = async () => ({ failures: {} });
|
||||
// set Bob's cross-signing key
|
||||
await resetCrossSigningKeys(bob);
|
||||
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: {
|
||||
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
|
||||
keys: {
|
||||
"curve25519:Dynabook": bob.crypto.olmDevice.deviceCurve25519Key,
|
||||
"ed25519:Dynabook": bob.crypto.olmDevice.deviceEd25519Key,
|
||||
"curve25519:Dynabook": bob.crypto!.olmDevice.deviceCurve25519Key!,
|
||||
"ed25519:Dynabook": bob.crypto!.olmDevice.deviceEd25519Key!,
|
||||
},
|
||||
verified: 1,
|
||||
known: true,
|
||||
},
|
||||
});
|
||||
alice.crypto.deviceList.storeCrossSigningForUser(
|
||||
alice.crypto!.deviceList.storeCrossSigningForUser(
|
||||
"@bob:example.com",
|
||||
bob.crypto.crossSigningInfo.toStorage(),
|
||||
bob.crypto!.crossSigningInfo.toStorage(),
|
||||
);
|
||||
|
||||
alice.uploadDeviceSigningKeys = async () => ({});
|
||||
@@ -909,8 +909,8 @@ describe("Cross Signing", function() {
|
||||
expect(bobTrust.isTofu()).toBeTruthy();
|
||||
|
||||
// "forget" that Bob is trusted
|
||||
delete alice.crypto.deviceList.crossSigningInfo["@bob:example.com"]
|
||||
.keys.master.signatures["@alice:example.com"];
|
||||
delete alice.crypto!.deviceList.crossSigningInfo["@bob:example.com"]
|
||||
.keys.master.signatures!["@alice:example.com"];
|
||||
|
||||
const bobTrust2 = alice.checkUserTrust("@bob:example.com");
|
||||
expect(bobTrust2.isCrossSigningVerified()).toBeFalsy();
|
||||
@@ -919,9 +919,9 @@ describe("Cross Signing", function() {
|
||||
upgradePromise = new Promise((resolve) => {
|
||||
upgradeResolveFunc = resolve;
|
||||
});
|
||||
alice.crypto.deviceList.emit(CryptoEvent.UserCrossSigningUpdated, "@bob:example.com");
|
||||
alice.crypto!.deviceList.emit(CryptoEvent.UserCrossSigningUpdated, "@bob:example.com");
|
||||
await new Promise((resolve) => {
|
||||
alice.crypto.on(CryptoEvent.UserTrustStatusChanged, resolve);
|
||||
alice.crypto!.on(CryptoEvent.UserTrustStatusChanged, resolve);
|
||||
});
|
||||
await upgradePromise;
|
||||
|
||||
@@ -963,7 +963,7 @@ describe("Cross Signing", function() {
|
||||
};
|
||||
|
||||
// Alice's device downloads the keys, but doesn't trust them yet
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@alice:example.com",
|
||||
@@ -999,7 +999,7 @@ describe("Cross Signing", function() {
|
||||
["ed25519:" + alicePubkey]: sig,
|
||||
},
|
||||
} };
|
||||
alice.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
alice.crypto!.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
[aliceDeviceId]: aliceCrossSignedDevice,
|
||||
});
|
||||
|
||||
@@ -1042,7 +1042,7 @@ describe("Cross Signing", function() {
|
||||
};
|
||||
|
||||
// Alice's device downloads the keys
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@alice:example.com",
|
||||
@@ -1067,11 +1067,65 @@ describe("Cross Signing", function() {
|
||||
"ed25519:Dynabook": "someOtherPubkey",
|
||||
},
|
||||
};
|
||||
alice.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
alice.crypto!.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
[deviceId]: aliceNotCrossSignedDevice,
|
||||
});
|
||||
|
||||
expect(alice.checkIfOwnDeviceCrossSigned(deviceId)).toBeFalsy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("checkIfOwnDeviceCrossSigned should sanely handle unknown devices", async () => {
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => ({});
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
|
||||
// Generate Alice's SSK etc
|
||||
const aliceMasterSigning = new global.Olm.PkSigning();
|
||||
const aliceMasterPrivkey = aliceMasterSigning.generate_seed();
|
||||
const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey);
|
||||
const aliceSigning = new global.Olm.PkSigning();
|
||||
const alicePrivkey = aliceSigning.generate_seed();
|
||||
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
|
||||
const aliceSSK: ICrossSigningKey = {
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
["ed25519:" + alicePubkey]: alicePubkey,
|
||||
},
|
||||
};
|
||||
const sskSig = aliceMasterSigning.sign(anotherjson.stringify(aliceSSK));
|
||||
aliceSSK.signatures = {
|
||||
"@alice:example.com": {
|
||||
["ed25519:" + aliceMasterPubkey]: sskSig,
|
||||
},
|
||||
};
|
||||
|
||||
// Alice's device downloads the keys
|
||||
alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
["ed25519:" + aliceMasterPubkey]: aliceMasterPubkey,
|
||||
},
|
||||
},
|
||||
self_signing: aliceSSK,
|
||||
},
|
||||
firstUse: true,
|
||||
crossSigningVerifiedBefore: false,
|
||||
});
|
||||
|
||||
expect(alice.checkIfOwnDeviceCrossSigned("notadevice")).toBeFalsy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("checkIfOwnDeviceCrossSigned should sanely handle unknown users", async () => {
|
||||
const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
|
||||
expect(alice.checkIfOwnDeviceCrossSigned("notadevice")).toBeFalsy();
|
||||
alice.stopClient();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@ export async function createSecretStorageKey(): Promise<IRecoveryKey> {
|
||||
decryption.free();
|
||||
return {
|
||||
// `pubkey` not used anymore with symmetric 4S
|
||||
keyInfo: { pubkey: storagePublicKey, key: undefined },
|
||||
keyInfo: { pubkey: storagePublicKey, key: undefined! },
|
||||
privateKey: storagePrivateKey,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
Copyright 2022 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 '../../olm-loader';
|
||||
import { TestClient } from '../../TestClient';
|
||||
import { logger } from '../../../src/logger';
|
||||
import { DEHYDRATION_ALGORITHM } from '../../../src/crypto/dehydration';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
describe("Dehydration", () => {
|
||||
if (!global.Olm) {
|
||||
logger.warn('Not running dehydration unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
return global.Olm.init();
|
||||
});
|
||||
|
||||
it("should rehydrate a dehydrated device", async () => {
|
||||
const key = new Uint8Array([1, 2, 3]);
|
||||
const alice = new TestClient(
|
||||
"@alice:example.com", "Osborne2", undefined, undefined,
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getDehydrationKey: async t => key,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const dehydratedDevice = new Olm.Account();
|
||||
dehydratedDevice.create();
|
||||
|
||||
alice.httpBackend.when("GET", "/dehydrated_device").respond(200, {
|
||||
device_id: "ABCDEFG",
|
||||
device_data: {
|
||||
algorithm: DEHYDRATION_ALGORITHM,
|
||||
account: dehydratedDevice.pickle(new Uint8Array(key)),
|
||||
},
|
||||
});
|
||||
alice.httpBackend.when("POST", "/dehydrated_device/claim").respond(200, {
|
||||
success: true,
|
||||
});
|
||||
|
||||
expect((await Promise.all([
|
||||
alice.client.rehydrateDevice(),
|
||||
alice.httpBackend.flushAllExpected(),
|
||||
]))[0])
|
||||
.toEqual("ABCDEFG");
|
||||
|
||||
expect(alice.client.getDeviceId()).toEqual("ABCDEFG");
|
||||
});
|
||||
|
||||
it("should dehydrate a device", async () => {
|
||||
const key = new Uint8Array([1, 2, 3]);
|
||||
const alice = new TestClient(
|
||||
"@alice:example.com", "Osborne2", undefined, undefined,
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getDehydrationKey: async t => key,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await alice.client.initCrypto();
|
||||
|
||||
alice.httpBackend.when("GET", "/room_keys/version").respond(404, {
|
||||
errcode: "M_NOT_FOUND",
|
||||
});
|
||||
|
||||
let pickledAccount = "";
|
||||
|
||||
alice.httpBackend.when("PUT", "/dehydrated_device")
|
||||
.check((req) => {
|
||||
expect(req.data.device_data).toMatchObject({
|
||||
algorithm: DEHYDRATION_ALGORITHM,
|
||||
account: expect.any(String),
|
||||
});
|
||||
pickledAccount = req.data.device_data.account;
|
||||
})
|
||||
.respond(200, {
|
||||
device_id: "ABCDEFG",
|
||||
});
|
||||
alice.httpBackend.when("POST", "/keys/upload/ABCDEFG")
|
||||
.check((req) => {
|
||||
expect(req.data).toMatchObject({
|
||||
"device_keys": expect.objectContaining({
|
||||
algorithms: expect.any(Array),
|
||||
device_id: "ABCDEFG",
|
||||
user_id: "@alice:example.com",
|
||||
keys: expect.objectContaining({
|
||||
"ed25519:ABCDEFG": expect.any(String),
|
||||
"curve25519:ABCDEFG": expect.any(String),
|
||||
}),
|
||||
signatures: expect.objectContaining({
|
||||
"@alice:example.com": expect.objectContaining({
|
||||
"ed25519:ABCDEFG": expect.any(String),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
"one_time_keys": expect.any(Object),
|
||||
"org.matrix.msc2732.fallback_keys": expect.any(Object),
|
||||
});
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
try {
|
||||
const deviceId =
|
||||
(await Promise.all([
|
||||
alice.client.createDehydratedDevice(new Uint8Array(key), {}),
|
||||
alice.httpBackend.flushAllExpected(),
|
||||
]))[0];
|
||||
|
||||
expect(deviceId).toEqual("ABCDEFG");
|
||||
expect(deviceId).not.toEqual("");
|
||||
|
||||
// try to rehydrate the dehydrated device
|
||||
const rehydrated = new Olm.Account();
|
||||
try {
|
||||
rehydrated.unpickle(new Uint8Array(key), pickledAccount);
|
||||
} finally {
|
||||
rehydrated.free();
|
||||
}
|
||||
} finally {
|
||||
alice.client?.crypto?.dehydrationManager?.stop();
|
||||
alice.client?.crypto?.deviceList.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -93,7 +93,7 @@ describe.each([
|
||||
await store.getOutgoingRoomKeyRequestByState([RoomKeyRequestState.Sent]);
|
||||
expect(r).not.toBeNull();
|
||||
expect(r).not.toBeUndefined();
|
||||
expect(r.state).toEqual(RoomKeyRequestState.Sent);
|
||||
expect(r!.state).toEqual(RoomKeyRequestState.Sent);
|
||||
expect(requests).toContainEqual(r);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,9 +21,9 @@ import { MatrixEvent } from "../../../src/models/event";
|
||||
import { TestClient } from '../../TestClient';
|
||||
import { makeTestClients } from './verification/util';
|
||||
import { encryptAES } from "../../../src/crypto/aes";
|
||||
import { resetCrossSigningKeys, createSecretStorageKey } from "./crypto-utils";
|
||||
import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils";
|
||||
import { logger } from '../../../src/logger';
|
||||
import { ICreateClientOpts } from '../../../src/client';
|
||||
import { ClientEvent, ICreateClientOpts } from '../../../src/client';
|
||||
import { ISecretStorageKeyInfo } from '../../../src/crypto/api';
|
||||
import { DeviceInfo } from '../../../src/crypto/deviceinfo';
|
||||
|
||||
@@ -41,7 +41,7 @@ async function makeTestClient(userInfo: { userId: string, deviceId: string}, opt
|
||||
await client.initCrypto();
|
||||
|
||||
// No need to download keys for these tests
|
||||
jest.spyOn(client.crypto, 'downloadKeys').mockResolvedValue({});
|
||||
jest.spyOn(client.crypto!, 'downloadKeys').mockResolvedValue({});
|
||||
|
||||
return client;
|
||||
}
|
||||
@@ -93,11 +93,11 @@ describe("Secrets", function() {
|
||||
},
|
||||
},
|
||||
);
|
||||
alice.crypto.crossSigningInfo.setKeys({
|
||||
alice.crypto!.crossSigningInfo.setKeys({
|
||||
master: signingkeyInfo,
|
||||
});
|
||||
|
||||
const secretStorage = alice.crypto.secretStorage;
|
||||
const secretStorage = alice.crypto!.secretStorage;
|
||||
|
||||
jest.spyOn(alice, 'setAccountData').mockImplementation(
|
||||
async function(eventType, contents) {
|
||||
@@ -113,7 +113,7 @@ describe("Secrets", function() {
|
||||
const keyAccountData = {
|
||||
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
};
|
||||
await alice.crypto.crossSigningInfo.signObject(keyAccountData, 'master');
|
||||
await alice.crypto!.crossSigningInfo.signObject(keyAccountData, 'master');
|
||||
|
||||
alice.store.storeAccountDataEvents([
|
||||
new MatrixEvent({
|
||||
@@ -200,7 +200,7 @@ describe("Secrets", function() {
|
||||
await alice.storeSecret("foo", "bar");
|
||||
|
||||
const accountData = alice.getAccountData('foo');
|
||||
expect(accountData.getContent().encrypted).toBeTruthy();
|
||||
expect(accountData!.getContent().encrypted).toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
@@ -233,29 +233,29 @@ describe("Secrets", function() {
|
||||
},
|
||||
);
|
||||
|
||||
const vaxDevice = vax.client.crypto.olmDevice;
|
||||
const osborne2Device = osborne2.client.crypto.olmDevice;
|
||||
const secretStorage = osborne2.client.crypto.secretStorage;
|
||||
const vaxDevice = vax.client.crypto!.olmDevice;
|
||||
const osborne2Device = osborne2.client.crypto!.olmDevice;
|
||||
const secretStorage = osborne2.client.crypto!.secretStorage;
|
||||
|
||||
osborne2.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
osborne2.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
"VAX": {
|
||||
known: false,
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
keys: {
|
||||
"ed25519:VAX": vaxDevice.deviceEd25519Key,
|
||||
"curve25519:VAX": vaxDevice.deviceCurve25519Key,
|
||||
"ed25519:VAX": vaxDevice.deviceEd25519Key!,
|
||||
"curve25519:VAX": vaxDevice.deviceCurve25519Key!,
|
||||
},
|
||||
verified: DeviceInfo.DeviceVerification.VERIFIED,
|
||||
},
|
||||
});
|
||||
vax.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
vax.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
"Osborne2": {
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
verified: 0,
|
||||
known: false,
|
||||
keys: {
|
||||
"ed25519:Osborne2": osborne2Device.deviceEd25519Key,
|
||||
"curve25519:Osborne2": osborne2Device.deviceCurve25519Key,
|
||||
"ed25519:Osborne2": osborne2Device.deviceEd25519Key!,
|
||||
"curve25519:Osborne2": osborne2Device.deviceCurve25519Key!,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -264,13 +264,13 @@ describe("Secrets", function() {
|
||||
const otks = (await osborne2Device.getOneTimeKeys()).curve25519;
|
||||
await osborne2Device.markKeysAsPublished();
|
||||
|
||||
await vax.client.crypto.olmDevice.createOutboundSession(
|
||||
osborne2Device.deviceCurve25519Key,
|
||||
await vax.client.crypto!.olmDevice.createOutboundSession(
|
||||
osborne2Device.deviceCurve25519Key!,
|
||||
Object.values(otks)[0],
|
||||
);
|
||||
|
||||
osborne2.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
osborne2.client.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
osborne2.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
osborne2.client.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
|
||||
const request = await secretStorage.request("foo", ["VAX"]);
|
||||
await request.promise; // return value not used
|
||||
@@ -328,7 +328,7 @@ describe("Secrets", function() {
|
||||
this.store.storeAccountDataEvents([
|
||||
event,
|
||||
]);
|
||||
this.emit("accountData", event);
|
||||
this.emit(ClientEvent.AccountData, event);
|
||||
return {};
|
||||
};
|
||||
|
||||
@@ -339,8 +339,8 @@ describe("Secrets", function() {
|
||||
createSecretStorageKey,
|
||||
});
|
||||
|
||||
const crossSigning = bob.crypto.crossSigningInfo;
|
||||
const secretStorage = bob.crypto.secretStorage;
|
||||
const crossSigning = bob.crypto!.crossSigningInfo;
|
||||
const secretStorage = bob.crypto!.secretStorage;
|
||||
|
||||
expect(crossSigning.getId()).toBeTruthy();
|
||||
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
|
||||
@@ -437,6 +437,7 @@ describe("Secrets", function() {
|
||||
return [keyId, secretStorageKeys[keyId]];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -486,7 +487,7 @@ describe("Secrets", function() {
|
||||
},
|
||||
}),
|
||||
]);
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
firstUse: false,
|
||||
crossSigningVerifiedBefore: false,
|
||||
keys: {
|
||||
@@ -528,16 +529,15 @@ describe("Secrets", function() {
|
||||
content: data,
|
||||
});
|
||||
alice.store.storeAccountDataEvents([event]);
|
||||
this.emit("accountData", event);
|
||||
this.emit(ClientEvent.AccountData, event);
|
||||
return {};
|
||||
};
|
||||
|
||||
await alice.bootstrapSecretStorage({});
|
||||
|
||||
expect(alice.getAccountData("m.secret_storage.default_key").getContent())
|
||||
expect(alice.getAccountData("m.secret_storage.default_key")!.getContent())
|
||||
.toEqual({ key: "key_id" });
|
||||
const keyInfo = alice.getAccountData("m.secret_storage.key.key_id")
|
||||
.getContent() as ISecretStorageKeyInfo;
|
||||
const keyInfo = alice.getAccountData("m.secret_storage.key.key_id")!.getContent<ISecretStorageKeyInfo>();
|
||||
expect(keyInfo.algorithm)
|
||||
.toEqual("m.secret_storage.v1.aes-hmac-sha2");
|
||||
expect(keyInfo.passphrase).toEqual({
|
||||
@@ -572,6 +572,7 @@ describe("Secrets", function() {
|
||||
return [keyId, secretStorageKeys[keyId]];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -630,7 +631,7 @@ describe("Secrets", function() {
|
||||
},
|
||||
}),
|
||||
]);
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
firstUse: false,
|
||||
crossSigningVerifiedBefore: false,
|
||||
keys: {
|
||||
@@ -672,14 +673,13 @@ describe("Secrets", function() {
|
||||
content: data,
|
||||
});
|
||||
alice.store.storeAccountDataEvents([event]);
|
||||
this.emit("accountData", event);
|
||||
this.emit(ClientEvent.AccountData, event);
|
||||
return {};
|
||||
};
|
||||
|
||||
await alice.bootstrapSecretStorage({});
|
||||
|
||||
const backupKey = alice.getAccountData("m.megolm_backup.v1")
|
||||
.getContent();
|
||||
const backupKey = alice.getAccountData("m.megolm_backup.v1")!.getContent();
|
||||
expect(backupKey.encrypted).toHaveProperty("key_id");
|
||||
expect(await alice.getSecret("m.megolm_backup.v1"))
|
||||
.toEqual("ey0GB1kB6jhOWgwiBUMIWg==");
|
||||
|
||||
@@ -18,7 +18,7 @@ import "../../../olm-loader";
|
||||
import { CryptoEvent, verificationMethods } from "../../../../src/crypto";
|
||||
import { logger } from "../../../../src/logger";
|
||||
import { SAS } from "../../../../src/crypto/verification/SAS";
|
||||
import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util';
|
||||
import { makeTestClients } from './util';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -31,14 +31,9 @@ describe("verification request integration tests with crypto layer", function()
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
setupWebcrypto();
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
teardownWebcrypto();
|
||||
});
|
||||
|
||||
it("should request and accept a verification", async function() {
|
||||
const [[alice, bob], clearTestClientTimeouts] = await makeTestClients(
|
||||
[
|
||||
@@ -49,7 +44,7 @@ describe("verification request integration tests with crypto layer", function()
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
},
|
||||
);
|
||||
alice.client.crypto.deviceList.getRawStoredDevicesForUser = function() {
|
||||
alice.client.crypto!.deviceList.getRawStoredDevicesForUser = function() {
|
||||
return {
|
||||
Dynabook: {
|
||||
algorithms: [],
|
||||
|
||||
@@ -15,18 +15,19 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import "../../../olm-loader";
|
||||
import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util';
|
||||
import { makeTestClients } from './util';
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import { SAS } from "../../../../src/crypto/verification/SAS";
|
||||
import { ISasEvent, SAS, SasEvent } from "../../../../src/crypto/verification/SAS";
|
||||
import { DeviceInfo } from "../../../../src/crypto/deviceinfo";
|
||||
import { CryptoEvent, verificationMethods } from "../../../../src/crypto";
|
||||
import * as olmlib from "../../../../src/crypto/olmlib";
|
||||
import { logger } from "../../../../src/logger";
|
||||
import { resetCrossSigningKeys } from "../crypto-utils";
|
||||
import { VerificationBase } from "../../../../src/crypto/verification/Base";
|
||||
import { VerificationBase as Verification, VerificationBase } from "../../../../src/crypto/verification/Base";
|
||||
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
|
||||
import { MatrixClient } from "../../../../src";
|
||||
import { VerificationRequest } from "../../../../src/crypto/verification/request/VerificationRequest";
|
||||
import { TestClient } from "../../../TestClient";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -40,14 +41,9 @@ describe("SAS verification", function() {
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
setupWebcrypto();
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
teardownWebcrypto();
|
||||
});
|
||||
|
||||
it("should error on an unexpected event", async function() {
|
||||
//channel, baseApis, userId, deviceId, startEvent, request
|
||||
const request = {
|
||||
@@ -75,13 +71,13 @@ describe("SAS verification", function() {
|
||||
});
|
||||
|
||||
describe("verification", () => {
|
||||
let alice;
|
||||
let bob;
|
||||
let aliceSasEvent;
|
||||
let bobSasEvent;
|
||||
let aliceVerifier;
|
||||
let bobPromise;
|
||||
let clearTestClientTimeouts;
|
||||
let alice: TestClient;
|
||||
let bob: TestClient;
|
||||
let aliceSasEvent: ISasEvent | null;
|
||||
let bobSasEvent: ISasEvent | null;
|
||||
let aliceVerifier: Verification<any, any>;
|
||||
let bobPromise: Promise<VerificationBase<any, any>>;
|
||||
let clearTestClientTimeouts: () => void;
|
||||
|
||||
beforeEach(async () => {
|
||||
[[alice, bob], clearTestClientTimeouts] = await makeTestClients(
|
||||
@@ -94,8 +90,8 @@ describe("SAS verification", function() {
|
||||
},
|
||||
);
|
||||
|
||||
const aliceDevice = alice.client.crypto.olmDevice;
|
||||
const bobDevice = bob.client.crypto.olmDevice;
|
||||
const aliceDevice = alice.client.crypto!.olmDevice;
|
||||
const bobDevice = bob.client.crypto!.olmDevice;
|
||||
|
||||
ALICE_DEVICES = {
|
||||
Osborne2: {
|
||||
@@ -121,26 +117,26 @@ describe("SAS verification", function() {
|
||||
},
|
||||
};
|
||||
|
||||
alice.client.crypto.deviceList.storeDevicesForUser(
|
||||
alice.client.crypto!.deviceList.storeDevicesForUser(
|
||||
"@bob:example.com", BOB_DEVICES,
|
||||
);
|
||||
alice.client.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
return Promise.resolve({});
|
||||
};
|
||||
|
||||
bob.client.crypto.deviceList.storeDevicesForUser(
|
||||
bob.client.crypto!.deviceList.storeDevicesForUser(
|
||||
"@alice:example.com", ALICE_DEVICES,
|
||||
);
|
||||
bob.client.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
return Promise.resolve({});
|
||||
};
|
||||
|
||||
aliceSasEvent = null;
|
||||
bobSasEvent = null;
|
||||
|
||||
bobPromise = new Promise((resolve, reject) => {
|
||||
bob.client.on("crypto.verification.request", request => {
|
||||
request.verifier.on("show_sas", (e) => {
|
||||
bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => {
|
||||
bob.client.on(CryptoEvent.VerificationRequest, request => {
|
||||
request.verifier!.on("show_sas", (e) => {
|
||||
if (!e.sas.emoji || !e.sas.decimal) {
|
||||
e.cancel();
|
||||
} else if (!aliceSasEvent) {
|
||||
@@ -156,14 +152,14 @@ describe("SAS verification", function() {
|
||||
}
|
||||
}
|
||||
});
|
||||
resolve(request.verifier);
|
||||
resolve(request.verifier!);
|
||||
});
|
||||
});
|
||||
|
||||
aliceVerifier = alice.client.beginKeyVerification(
|
||||
verificationMethods.SAS, bob.client.getUserId(), bob.deviceId,
|
||||
verificationMethods.SAS, bob.client.getUserId()!, bob.deviceId!,
|
||||
);
|
||||
aliceVerifier.on("show_sas", (e) => {
|
||||
aliceVerifier.on(SasEvent.ShowSas, (e) => {
|
||||
if (!e.sas.emoji || !e.sas.decimal) {
|
||||
e.cancel();
|
||||
} else if (!bobSasEvent) {
|
||||
@@ -195,9 +191,9 @@ describe("SAS verification", function() {
|
||||
const origSendToDevice = bob.client.sendToDevice.bind(bob.client);
|
||||
bob.client.sendToDevice = function(type, map) {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.client.getUserId()][alice.client.deviceId]
|
||||
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!]
|
||||
.message_authentication_code;
|
||||
keyAgreement = map[alice.client.getUserId()][alice.client.deviceId]
|
||||
keyAgreement = map[alice.client.getUserId()!][alice.client.deviceId!]
|
||||
.key_agreement_protocol;
|
||||
}
|
||||
return origSendToDevice(type, map);
|
||||
@@ -219,8 +215,8 @@ describe("SAS verification", function() {
|
||||
await Promise.all([
|
||||
aliceVerifier.verify(),
|
||||
bobPromise.then((verifier) => verifier.verify()),
|
||||
alice.httpBackend.flush(),
|
||||
bob.httpBackend.flush(),
|
||||
alice.httpBackend.flush(undefined),
|
||||
bob.httpBackend.flush(undefined),
|
||||
]);
|
||||
|
||||
// make sure that it uses the preferred method
|
||||
@@ -230,10 +226,10 @@ describe("SAS verification", function() {
|
||||
// make sure Alice and Bob verified each other
|
||||
const bobDevice
|
||||
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
|
||||
expect(bobDevice.isVerified()).toBeTruthy();
|
||||
expect(bobDevice?.isVerified()).toBeTruthy();
|
||||
const aliceDevice
|
||||
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
|
||||
expect(aliceDevice.isVerified()).toBeTruthy();
|
||||
expect(aliceDevice?.isVerified()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should be able to verify using the old base64", async () => {
|
||||
@@ -248,7 +244,7 @@ describe("SAS verification", function() {
|
||||
// has, since it is the same object. If this does not
|
||||
// happen, the verification will fail due to a hash
|
||||
// commitment mismatch.
|
||||
map[bob.client.getUserId()][bob.client.deviceId]
|
||||
map[bob.client.getUserId()!][bob.client.deviceId!]
|
||||
.message_authentication_codes = ['hkdf-hmac-sha256'];
|
||||
}
|
||||
return aliceOrigSendToDevice(type, map);
|
||||
@@ -256,7 +252,7 @@ describe("SAS verification", function() {
|
||||
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
|
||||
bob.client.sendToDevice = (type, map) => {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.client.getUserId()][alice.client.deviceId]
|
||||
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!]
|
||||
.message_authentication_code;
|
||||
}
|
||||
return bobOrigSendToDevice(type, map);
|
||||
@@ -278,18 +274,18 @@ describe("SAS verification", function() {
|
||||
await Promise.all([
|
||||
aliceVerifier.verify(),
|
||||
bobPromise.then((verifier) => verifier.verify()),
|
||||
alice.httpBackend.flush(),
|
||||
bob.httpBackend.flush(),
|
||||
alice.httpBackend.flush(undefined),
|
||||
bob.httpBackend.flush(undefined),
|
||||
]);
|
||||
|
||||
expect(macMethod).toBe("hkdf-hmac-sha256");
|
||||
|
||||
const bobDevice
|
||||
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
|
||||
expect(bobDevice.isVerified()).toBeTruthy();
|
||||
expect(bobDevice!.isVerified()).toBeTruthy();
|
||||
const aliceDevice
|
||||
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
|
||||
expect(aliceDevice.isVerified()).toBeTruthy();
|
||||
expect(aliceDevice!.isVerified()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should be able to verify using the old MAC", async () => {
|
||||
@@ -304,7 +300,7 @@ describe("SAS verification", function() {
|
||||
// has, since it is the same object. If this does not
|
||||
// happen, the verification will fail due to a hash
|
||||
// commitment mismatch.
|
||||
map[bob.client.getUserId()][bob.client.deviceId]
|
||||
map[bob.client.getUserId()!][bob.client.deviceId!]
|
||||
.message_authentication_codes = ['hmac-sha256'];
|
||||
}
|
||||
return aliceOrigSendToDevice(type, map);
|
||||
@@ -312,7 +308,7 @@ describe("SAS verification", function() {
|
||||
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
|
||||
bob.client.sendToDevice = (type, map) => {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.client.getUserId()][alice.client.deviceId]
|
||||
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!]
|
||||
.message_authentication_code;
|
||||
}
|
||||
return bobOrigSendToDevice(type, map);
|
||||
@@ -334,18 +330,18 @@ describe("SAS verification", function() {
|
||||
await Promise.all([
|
||||
aliceVerifier.verify(),
|
||||
bobPromise.then((verifier) => verifier.verify()),
|
||||
alice.httpBackend.flush(),
|
||||
bob.httpBackend.flush(),
|
||||
alice.httpBackend.flush(undefined),
|
||||
bob.httpBackend.flush(undefined),
|
||||
]);
|
||||
|
||||
expect(macMethod).toBe("hmac-sha256");
|
||||
|
||||
const bobDevice
|
||||
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
|
||||
expect(bobDevice.isVerified()).toBeTruthy();
|
||||
expect(bobDevice?.isVerified()).toBeTruthy();
|
||||
const aliceDevice
|
||||
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
|
||||
expect(aliceDevice.isVerified()).toBeTruthy();
|
||||
expect(aliceDevice?.isVerified()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should verify a cross-signing key", async () => {
|
||||
@@ -361,9 +357,11 @@ describe("SAS verification", function() {
|
||||
|
||||
await resetCrossSigningKeys(bob.client);
|
||||
|
||||
bob.client.crypto.deviceList.storeCrossSigningForUser(
|
||||
bob.client.crypto!.deviceList.storeCrossSigningForUser(
|
||||
"@alice:example.com", {
|
||||
keys: alice.client.crypto.crossSigningInfo.keys,
|
||||
keys: alice.client.crypto!.crossSigningInfo.keys,
|
||||
crossSigningVerifiedBefore: false,
|
||||
firstUse: true,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -415,10 +413,10 @@ describe("SAS verification", function() {
|
||||
|
||||
const bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => {
|
||||
bob.client.on(CryptoEvent.VerificationRequest, request => {
|
||||
request.verifier.on("show_sas", (e) => {
|
||||
request.verifier!.on("show_sas", (e) => {
|
||||
e.mismatch();
|
||||
});
|
||||
resolve(request.verifier);
|
||||
resolve(request.verifier!);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -464,7 +462,7 @@ describe("SAS verification", function() {
|
||||
},
|
||||
);
|
||||
|
||||
alice.client.crypto.setDeviceVerification = jest.fn();
|
||||
alice.client.crypto!.setDeviceVerification = jest.fn();
|
||||
alice.client.getDeviceEd25519Key = () => {
|
||||
return "alice+base64+ed25519+key";
|
||||
};
|
||||
@@ -482,7 +480,7 @@ describe("SAS verification", function() {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
bob.client.crypto.setDeviceVerification = jest.fn();
|
||||
bob.client.crypto!.setDeviceVerification = jest.fn();
|
||||
bob.client.getStoredDevice = () => {
|
||||
return DeviceInfo.fromStorage(
|
||||
{
|
||||
@@ -565,7 +563,7 @@ describe("SAS verification", function() {
|
||||
]);
|
||||
|
||||
// make sure Alice and Bob verified each other
|
||||
expect(alice.client.crypto.setDeviceVerification)
|
||||
expect(alice.client.crypto!.setDeviceVerification)
|
||||
.toHaveBeenCalledWith(
|
||||
bob.client.getUserId(),
|
||||
bob.client.deviceId,
|
||||
@@ -574,7 +572,7 @@ describe("SAS verification", function() {
|
||||
null,
|
||||
{ "ed25519:Dynabook": "bob+base64+ed25519+key" },
|
||||
);
|
||||
expect(bob.client.crypto.setDeviceVerification)
|
||||
expect(bob.client.crypto!.setDeviceVerification)
|
||||
.toHaveBeenCalledWith(
|
||||
alice.client.getUserId(),
|
||||
alice.client.deviceId,
|
||||
|
||||
@@ -14,13 +14,14 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { CrossSigningInfo } from '../../../../src/crypto/CrossSigning';
|
||||
import '../../../olm-loader';
|
||||
import { MatrixClient, MatrixEvent } from '../../../../src/matrix';
|
||||
import { encodeBase64 } from "../../../../src/crypto/olmlib";
|
||||
import { setupWebcrypto, teardownWebcrypto } from './util';
|
||||
import { VerificationBase } from '../../../../src/crypto/verification/Base';
|
||||
import { MatrixClient, MatrixEvent } from '../../../../src';
|
||||
import "../../../../src/crypto"; // import this to cycle-break
|
||||
import { CrossSigningInfo } from '../../../../src/crypto/CrossSigning';
|
||||
import { VerificationRequest } from '../../../../src/crypto/verification/request/VerificationRequest';
|
||||
import { IVerificationChannel } from '../../../../src/crypto/verification/request/Channel';
|
||||
import { VerificationBase } from '../../../../src/crypto/verification/Base';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
@@ -35,14 +36,9 @@ const testKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
|
||||
|
||||
describe("self-verifications", () => {
|
||||
beforeAll(function() {
|
||||
setupWebcrypto();
|
||||
return global.Olm.init();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
teardownWebcrypto();
|
||||
});
|
||||
|
||||
it("triggers a request for key sharing upon completion", async () => {
|
||||
const userId = "@test:localhost";
|
||||
|
||||
|
||||
@@ -15,8 +15,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import nodeCrypto from "crypto";
|
||||
|
||||
import { TestClient } from '../../../TestClient';
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import { IRoomTimelineData } from "../../../../src/models/event-timeline-set";
|
||||
@@ -41,7 +39,7 @@ export async function makeTestClients(userInfos, options): Promise<[TestClient[]
|
||||
});
|
||||
const client = clientMap[userId][deviceId];
|
||||
const decryptionPromise = event.isEncrypted() ?
|
||||
event.attemptDecryption(client.crypto) :
|
||||
event.attemptDecryption(client.crypto!) :
|
||||
Promise.resolve();
|
||||
|
||||
decryptionPromise.then(
|
||||
@@ -118,16 +116,3 @@ export async function makeTestClients(userInfos, options): Promise<[TestClient[]
|
||||
|
||||
return [clients, destroy];
|
||||
}
|
||||
|
||||
export function setupWebcrypto() {
|
||||
global.crypto = {
|
||||
getRandomValues: (buf) => {
|
||||
return nodeCrypto.randomFillSync(buf as any);
|
||||
},
|
||||
} as unknown as Crypto;
|
||||
}
|
||||
|
||||
export function teardownWebcrypto() {
|
||||
// @ts-ignore undefined != Crypto
|
||||
global.crypto = undefined;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import { ToDeviceChannel } from
|
||||
"../../../../src/crypto/verification/request/ToDeviceChannel";
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import { MatrixClient } from "../../../../src/client";
|
||||
import { setupWebcrypto, teardownWebcrypto } from "./util";
|
||||
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
|
||||
import { VerificationBase } from "../../../../src/crypto/verification/Base";
|
||||
|
||||
@@ -131,7 +130,11 @@ function makeRemoteEcho(event) {
|
||||
}));
|
||||
}
|
||||
|
||||
async function distributeEvent(ownRequest, theirRequest, event) {
|
||||
async function distributeEvent(
|
||||
ownRequest: VerificationRequest,
|
||||
theirRequest: VerificationRequest,
|
||||
event: MatrixEvent,
|
||||
): Promise<void> {
|
||||
await ownRequest.channel.handleEvent(
|
||||
makeRemoteEcho(event),
|
||||
ownRequest,
|
||||
@@ -143,14 +146,6 @@ async function distributeEvent(ownRequest, theirRequest, event) {
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("verification request unit tests", function() {
|
||||
beforeAll(function() {
|
||||
setupWebcrypto();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
teardownWebcrypto();
|
||||
});
|
||||
|
||||
it("transition from UNSENT to DONE through happy path", async function() {
|
||||
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
||||
const bob = makeMockClient("@bob:matrix.tld", "device1");
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
/*
|
||||
Copyright 2022 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.
|
||||
*/
|
||||
|
||||
// We have to use EventEmitter here to mock part of the matrix-widget-api
|
||||
// project, which doesn't know about our TypeEventEmitter implementation at all
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EventEmitter } from "events";
|
||||
import { MockedObject } from "jest-mock";
|
||||
import {
|
||||
WidgetApi,
|
||||
WidgetApiToWidgetAction,
|
||||
MatrixCapabilities,
|
||||
ITurnServer,
|
||||
IRoomEvent,
|
||||
} from "matrix-widget-api";
|
||||
|
||||
import { createRoomWidgetClient, MsgType } from "../../src/matrix";
|
||||
import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "../../src/client";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { ICapabilities } from "../../src/embedded";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { ToDeviceBatch } from "../../src/models/ToDeviceMessage";
|
||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||
|
||||
class MockWidgetApi extends EventEmitter {
|
||||
public start = jest.fn();
|
||||
public requestCapability = jest.fn();
|
||||
public requestCapabilities = jest.fn();
|
||||
public requestCapabilityForRoomTimeline = jest.fn();
|
||||
public requestCapabilityToSendEvent = jest.fn();
|
||||
public requestCapabilityToReceiveEvent = jest.fn();
|
||||
public requestCapabilityToSendMessage = jest.fn();
|
||||
public requestCapabilityToReceiveMessage = jest.fn();
|
||||
public requestCapabilityToSendState = jest.fn();
|
||||
public requestCapabilityToReceiveState = jest.fn();
|
||||
public requestCapabilityToSendToDevice = jest.fn();
|
||||
public requestCapabilityToReceiveToDevice = jest.fn();
|
||||
public sendRoomEvent = jest.fn(() => ({ event_id: `$${Math.random()}` }));
|
||||
public sendStateEvent = jest.fn();
|
||||
public sendToDevice = jest.fn();
|
||||
public readStateEvents = jest.fn(() => []);
|
||||
public getTurnServers = jest.fn(() => []);
|
||||
|
||||
public transport = { reply: jest.fn() };
|
||||
}
|
||||
|
||||
describe("RoomWidgetClient", () => {
|
||||
let widgetApi: MockedObject<WidgetApi>;
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
widgetApi = new MockWidgetApi() as unknown as MockedObject<WidgetApi>;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
const makeClient = async (capabilities: ICapabilities): Promise<void> => {
|
||||
const baseUrl = "https://example.org";
|
||||
client = createRoomWidgetClient(widgetApi, capabilities, "!1:example.org", { baseUrl });
|
||||
expect(widgetApi.start).toHaveBeenCalled(); // needs to have been called early in order to not miss messages
|
||||
widgetApi.emit("ready");
|
||||
await client.startClient();
|
||||
};
|
||||
|
||||
describe("events", () => {
|
||||
it("sends", async () => {
|
||||
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToSendEvent).toHaveBeenCalledWith("org.matrix.rageshake_request");
|
||||
await client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 });
|
||||
expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith(
|
||||
"org.matrix.rageshake_request", { request_id: 123 }, "!1:example.org",
|
||||
);
|
||||
});
|
||||
|
||||
it("receives", async () => {
|
||||
const event = new MatrixEvent({
|
||||
type: "org.matrix.rageshake_request",
|
||||
event_id: "$pduhfiidph",
|
||||
room_id: "!1:example.org",
|
||||
sender: "@alice:example.org",
|
||||
content: { request_id: 123 },
|
||||
}).getEffectiveEvent();
|
||||
|
||||
await makeClient({ receiveEvent: ["org.matrix.rageshake_request"] });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToReceiveEvent).toHaveBeenCalledWith("org.matrix.rageshake_request");
|
||||
|
||||
const emittedEvent = new Promise<MatrixEvent>(resolve => client.once(ClientEvent.Event, resolve));
|
||||
const emittedSync = new Promise<SyncState>(resolve => client.once(ClientEvent.Sync, resolve));
|
||||
widgetApi.emit(
|
||||
`action:${WidgetApiToWidgetAction.SendEvent}`,
|
||||
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
|
||||
);
|
||||
|
||||
// The client should've emitted about the received event
|
||||
expect((await emittedEvent).getEffectiveEvent()).toEqual(event);
|
||||
expect(await emittedSync).toEqual(SyncState.Syncing);
|
||||
// It should've also inserted the event into the room object
|
||||
const room = client.getRoom("!1:example.org");
|
||||
expect(room).not.toBeNull();
|
||||
expect(room!.getLiveTimeline().getEvents().map(e => e.getEffectiveEvent())).toEqual([event]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("messages", () => {
|
||||
it("requests permissions for specific message types", async () => {
|
||||
await makeClient({ sendMessage: [MsgType.Text], receiveMessage: [MsgType.Text] });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToSendMessage).toHaveBeenCalledWith(MsgType.Text);
|
||||
expect(widgetApi.requestCapabilityToReceiveMessage).toHaveBeenCalledWith(MsgType.Text);
|
||||
});
|
||||
|
||||
it("requests permissions for all message types", async () => {
|
||||
await makeClient({ sendMessage: true, receiveMessage: true });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToSendMessage).toHaveBeenCalledWith();
|
||||
expect(widgetApi.requestCapabilityToReceiveMessage).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
// No point in testing sending and receiving since it's done exactly the
|
||||
// same way as non-message events
|
||||
});
|
||||
|
||||
describe("state events", () => {
|
||||
const event = new MatrixEvent({
|
||||
type: "org.example.foo",
|
||||
event_id: "$sfkjfsksdkfsd",
|
||||
room_id: "!1:example.org",
|
||||
sender: "@alice:example.org",
|
||||
state_key: "bar",
|
||||
content: { hello: "world" },
|
||||
}).getEffectiveEvent();
|
||||
|
||||
it("sends", async () => {
|
||||
await makeClient({ sendState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToSendState).toHaveBeenCalledWith("org.example.foo", "bar");
|
||||
await client.sendStateEvent("!1:example.org", "org.example.foo", { hello: "world" }, "bar");
|
||||
expect(widgetApi.sendStateEvent).toHaveBeenCalledWith(
|
||||
"org.example.foo", "bar", { hello: "world" }, "!1:example.org",
|
||||
);
|
||||
});
|
||||
|
||||
it("receives", async () => {
|
||||
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
|
||||
|
||||
const emittedEvent = new Promise<MatrixEvent>(resolve => client.once(ClientEvent.Event, resolve));
|
||||
const emittedSync = new Promise<SyncState>(resolve => client.once(ClientEvent.Sync, resolve));
|
||||
widgetApi.emit(
|
||||
`action:${WidgetApiToWidgetAction.SendEvent}`,
|
||||
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
|
||||
);
|
||||
|
||||
// The client should've emitted about the received event
|
||||
expect((await emittedEvent).getEffectiveEvent()).toEqual(event);
|
||||
expect(await emittedSync).toEqual(SyncState.Syncing);
|
||||
// It should've also inserted the event into the room object
|
||||
const room = client.getRoom("!1:example.org");
|
||||
expect(room).not.toBeNull();
|
||||
expect(room!.currentState.getStateEvents("org.example.foo", "bar").getEffectiveEvent()).toEqual(event);
|
||||
});
|
||||
|
||||
it("backfills", async () => {
|
||||
widgetApi.readStateEvents.mockImplementation(async (eventType, limit, stateKey) =>
|
||||
eventType === "org.example.foo" && (limit ?? Infinity) > 0 && stateKey === "bar"
|
||||
? [event as IRoomEvent]
|
||||
: [],
|
||||
);
|
||||
|
||||
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
|
||||
|
||||
const room = client.getRoom("!1:example.org");
|
||||
expect(room).not.toBeNull();
|
||||
expect(room!.currentState.getStateEvents("org.example.foo", "bar").getEffectiveEvent()).toEqual(event);
|
||||
});
|
||||
});
|
||||
|
||||
describe("to-device messages", () => {
|
||||
const unencryptedContentMap = {
|
||||
"@alice:example.org": { "*": { hello: "alice!" } },
|
||||
"@bob:example.org": { bobDesktop: { hello: "bob!" } },
|
||||
};
|
||||
|
||||
it("sends unencrypted (sendToDevice)", async () => {
|
||||
await makeClient({ sendToDevice: ["org.example.foo"] });
|
||||
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
|
||||
|
||||
await client.sendToDevice("org.example.foo", unencryptedContentMap);
|
||||
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, unencryptedContentMap);
|
||||
});
|
||||
|
||||
it("sends unencrypted (queueToDevice)", async () => {
|
||||
await makeClient({ sendToDevice: ["org.example.foo"] });
|
||||
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
|
||||
|
||||
const batch: ToDeviceBatch = {
|
||||
eventType: "org.example.foo",
|
||||
batch: [
|
||||
{ userId: "@alice:example.org", deviceId: "*", payload: { hello: "alice!" } },
|
||||
{ userId: "@bob:example.org", deviceId: "bobDesktop", payload: { hello: "bob!" } },
|
||||
],
|
||||
};
|
||||
await client.queueToDevice(batch);
|
||||
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, unencryptedContentMap);
|
||||
});
|
||||
|
||||
it("sends encrypted (encryptAndSendToDevices)", async () => {
|
||||
await makeClient({ sendToDevice: ["org.example.foo"] });
|
||||
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
|
||||
|
||||
const payload = { type: "org.example.foo", hello: "world" };
|
||||
await client.encryptAndSendToDevices(
|
||||
[
|
||||
{ userId: "@alice:example.org", deviceInfo: new DeviceInfo("aliceWeb") },
|
||||
{ userId: "@bob:example.org", deviceInfo: new DeviceInfo("bobDesktop") },
|
||||
],
|
||||
payload,
|
||||
);
|
||||
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", true, {
|
||||
"@alice:example.org": { aliceWeb: payload },
|
||||
"@bob:example.org": { bobDesktop: payload },
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ encrypted: false, title: "unencrypted" },
|
||||
{ encrypted: true, title: "encrypted" },
|
||||
])("receives $title", async ({ encrypted }) => {
|
||||
await makeClient({ receiveToDevice: ["org.example.foo"] });
|
||||
expect(widgetApi.requestCapabilityToReceiveToDevice).toHaveBeenCalledWith("org.example.foo");
|
||||
|
||||
const event = {
|
||||
type: "org.example.foo",
|
||||
sender: "@alice:example.org",
|
||||
encrypted,
|
||||
content: { hello: "world" },
|
||||
};
|
||||
|
||||
const emittedEvent = new Promise<MatrixEvent>(resolve => client.once(ClientEvent.ToDeviceEvent, resolve));
|
||||
const emittedSync = new Promise<SyncState>(resolve => client.once(ClientEvent.Sync, resolve));
|
||||
widgetApi.emit(
|
||||
`action:${WidgetApiToWidgetAction.SendToDevice}`,
|
||||
new CustomEvent(`action:${WidgetApiToWidgetAction.SendToDevice}`, { detail: { data: event } }),
|
||||
);
|
||||
|
||||
expect((await emittedEvent).getEffectiveEvent()).toEqual({
|
||||
type: event.type,
|
||||
sender: event.sender,
|
||||
content: event.content,
|
||||
});
|
||||
expect((await emittedEvent).isEncrypted()).toEqual(encrypted);
|
||||
expect(await emittedSync).toEqual(SyncState.Syncing);
|
||||
});
|
||||
});
|
||||
|
||||
it("gets TURN servers", async () => {
|
||||
const server1: ITurnServer = {
|
||||
uris: [
|
||||
"turn:turn.example.com:3478?transport=udp",
|
||||
"turn:10.20.30.40:3478?transport=tcp",
|
||||
"turns:10.20.30.40:443?transport=tcp",
|
||||
],
|
||||
username: "1443779631:@user:example.com",
|
||||
password: "JlKfBy1QwLrO20385QyAtEyIv0=",
|
||||
};
|
||||
const server2: ITurnServer = {
|
||||
uris: [
|
||||
"turn:turn.example.com:3478?transport=udp",
|
||||
"turn:10.20.30.40:3478?transport=tcp",
|
||||
"turns:10.20.30.40:443?transport=tcp",
|
||||
],
|
||||
username: "1448999322:@user:example.com",
|
||||
password: "hunter2",
|
||||
};
|
||||
const clientServer1: IClientTurnServer = {
|
||||
urls: server1.uris,
|
||||
username: server1.username,
|
||||
credential: server1.password,
|
||||
};
|
||||
const clientServer2: IClientTurnServer = {
|
||||
urls: server2.uris,
|
||||
username: server2.username,
|
||||
credential: server2.password,
|
||||
};
|
||||
|
||||
let emitServer2: () => void;
|
||||
const getServer2 = new Promise<ITurnServer>(resolve => emitServer2 = () => resolve(server2));
|
||||
widgetApi.getTurnServers.mockImplementation(async function* () {
|
||||
yield server1;
|
||||
yield await getServer2;
|
||||
});
|
||||
|
||||
await makeClient({ turnServers: true });
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC3846TurnServers);
|
||||
|
||||
// The first server should've arrived immediately
|
||||
expect(client.getTurnServers()).toEqual([clientServer1]);
|
||||
|
||||
// Subsequent servers arrive asynchronously and should emit an event
|
||||
const emittedServer = new Promise<IClientTurnServer[]>(resolve =>
|
||||
client.once(ClientEvent.TurnServers, resolve),
|
||||
);
|
||||
emitServer2!();
|
||||
expect(await emittedServer).toEqual([clientServer2]);
|
||||
expect(client.getTurnServers()).toEqual([clientServer2]);
|
||||
});
|
||||
});
|
||||
@@ -32,7 +32,7 @@ describe("eventMapperFor", function() {
|
||||
fetchFn: function() {} as any, // NOP
|
||||
store: {
|
||||
getRoom(roomId: string): Room | null {
|
||||
return rooms.find(r => r.roomId === roomId);
|
||||
return rooms.find(r => r.roomId === roomId) ?? null;
|
||||
},
|
||||
} as IStore,
|
||||
scheduler: {
|
||||
|
||||
@@ -45,16 +45,33 @@ describe('EventTimelineSet', () => {
|
||||
it('should return the related events', () => {
|
||||
eventTimelineSet.relations.aggregateChildEvent(messageEvent);
|
||||
const relations = eventTimelineSet.relations.getChildEventsForEvent(
|
||||
messageEvent.getId(),
|
||||
messageEvent.getId()!,
|
||||
"m.in_reply_to",
|
||||
EventType.RoomMessage,
|
||||
);
|
||||
expect(relations).toBeDefined();
|
||||
expect(relations.getRelations().length).toBe(1);
|
||||
expect(relations.getRelations()[0].getId()).toBe(replyEvent.getId());
|
||||
expect(relations!.getRelations().length).toBe(1);
|
||||
expect(relations!.getRelations()[0].getId()).toBe(replyEvent.getId());
|
||||
});
|
||||
};
|
||||
|
||||
const mkThreadResponse = (root: MatrixEvent) => utils.mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
user: userA,
|
||||
room: roomId,
|
||||
content: {
|
||||
"body": "Thread response :: " + Math.random(),
|
||||
"m.relates_to": {
|
||||
"event_id": root.getId(),
|
||||
"m.in_reply_to": {
|
||||
"event_id": root.getId(),
|
||||
},
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
},
|
||||
}, room.client);
|
||||
|
||||
beforeEach(() => {
|
||||
client = utils.mock(MatrixClient, 'MatrixClient');
|
||||
client.reEmitter = utils.mock(ReEmitter, 'ReEmitter');
|
||||
@@ -117,6 +134,13 @@ describe('EventTimelineSet', () => {
|
||||
});
|
||||
|
||||
describe('addEventToTimeline', () => {
|
||||
let thread: Thread;
|
||||
|
||||
beforeEach(() => {
|
||||
(client.supportsExperimentalThreads as jest.Mock).mockReturnValue(true);
|
||||
thread = new Thread("!thread_id:server", messageEvent, { room, client });
|
||||
});
|
||||
|
||||
it("Adds event to timeline", () => {
|
||||
const liveTimeline = eventTimelineSet.getLiveTimeline();
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
@@ -144,6 +168,58 @@ describe('EventTimelineSet', () => {
|
||||
);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("should not add an event to a timeline that does not belong to the timelineSet", () => {
|
||||
const eventTimelineSet2 = new EventTimelineSet(room);
|
||||
const liveTimeline2 = eventTimelineSet2.getLiveTimeline();
|
||||
expect(liveTimeline2.getEvents().length).toStrictEqual(0);
|
||||
|
||||
expect(() => {
|
||||
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline2, {
|
||||
toStartOfTimeline: true,
|
||||
});
|
||||
}).toThrowError();
|
||||
});
|
||||
|
||||
it("should not add a threaded reply to the main room timeline", () => {
|
||||
const liveTimeline = eventTimelineSet.getLiveTimeline();
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
|
||||
const threadedReplyEvent = mkThreadResponse(messageEvent);
|
||||
|
||||
eventTimelineSet.addEventToTimeline(threadedReplyEvent, liveTimeline, {
|
||||
toStartOfTimeline: true,
|
||||
});
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
});
|
||||
|
||||
it("should not add a normal message to the timelineSet representing a thread", () => {
|
||||
const eventTimelineSetForThread = new EventTimelineSet(room, {}, client, thread);
|
||||
const liveTimeline = eventTimelineSetForThread.getLiveTimeline();
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
|
||||
eventTimelineSetForThread.addEventToTimeline(messageEvent, liveTimeline, {
|
||||
toStartOfTimeline: true,
|
||||
});
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
});
|
||||
|
||||
describe('non-room timeline', () => {
|
||||
it('Adds event to timeline', () => {
|
||||
const nonRoomEventTimelineSet = new EventTimelineSet(
|
||||
// This is what we're specifically testing against, a timeline
|
||||
// without a `room` defined
|
||||
undefined,
|
||||
);
|
||||
const nonRoomEventTimeline = new EventTimeline(nonRoomEventTimelineSet);
|
||||
|
||||
expect(nonRoomEventTimeline.getEvents().length).toStrictEqual(0);
|
||||
nonRoomEventTimelineSet.addEventToTimeline(messageEvent, nonRoomEventTimeline, {
|
||||
toStartOfTimeline: true,
|
||||
});
|
||||
expect(nonRoomEventTimeline.getEvents().length).toStrictEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('aggregateRelations', () => {
|
||||
@@ -193,7 +269,7 @@ describe('EventTimelineSet', () => {
|
||||
it('should not return the related events', () => {
|
||||
eventTimelineSet.relations.aggregateChildEvent(messageEvent);
|
||||
const relations = eventTimelineSet.relations.getChildEventsForEvent(
|
||||
messageEvent.getId(),
|
||||
messageEvent.getId()!,
|
||||
"m.in_reply_to",
|
||||
EventType.RoomMessage,
|
||||
);
|
||||
@@ -236,7 +312,7 @@ describe('EventTimelineSet', () => {
|
||||
"m.relates_to": {
|
||||
"event_id": root.getId(),
|
||||
"m.in_reply_to": {
|
||||
"event_id": root.getId(),
|
||||
"event_id": root.getId()!,
|
||||
},
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
@@ -278,14 +354,14 @@ describe('EventTimelineSet', () => {
|
||||
});
|
||||
|
||||
it("should return true if the timeline set is for a thread and the event is its thread root", () => {
|
||||
const thread = new Thread(messageEvent.getId(), messageEvent, { room, client });
|
||||
const thread = new Thread(messageEvent.getId()!, messageEvent, { room, client });
|
||||
const eventTimelineSet = new EventTimelineSet(room, {}, client, thread);
|
||||
messageEvent.setThread(thread);
|
||||
expect(eventTimelineSet.canContain(messageEvent)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return true if the timeline set is for a thread and the event is a response to it", () => {
|
||||
const thread = new Thread(messageEvent.getId(), messageEvent, { room, client });
|
||||
const thread = new Thread(messageEvent.getId()!, messageEvent, { room, client });
|
||||
const eventTimelineSet = new EventTimelineSet(room, {}, client, thread);
|
||||
messageEvent.setThread(thread);
|
||||
const event = mkThreadResponse(messageEvent);
|
||||
@@ -310,7 +386,7 @@ describe('EventTimelineSet', () => {
|
||||
content: { body: "test" },
|
||||
event_id: "!test1:server",
|
||||
});
|
||||
eventTimelineSet.handleRemoteEcho(roomMessageEvent, "~!local-event-id:server", roomMessageEvent.getId());
|
||||
eventTimelineSet.handleRemoteEcho(roomMessageEvent, "~!local-event-id:server", roomMessageEvent.getId()!);
|
||||
expect(eventTimelineSet.getLiveTimeline().getEvents()).toContain(roomMessageEvent);
|
||||
|
||||
const roomFilteredEvent = new MatrixEvent({
|
||||
@@ -318,7 +394,7 @@ describe('EventTimelineSet', () => {
|
||||
content: { body: "test" },
|
||||
event_id: "!test2:server",
|
||||
});
|
||||
eventTimelineSet.handleRemoteEcho(roomFilteredEvent, "~!local-event-id:server", roomFilteredEvent.getId());
|
||||
eventTimelineSet.handleRemoteEcho(roomFilteredEvent, "~!local-event-id:server", roomFilteredEvent.getId()!);
|
||||
expect(eventTimelineSet.getLiveTimeline().getEvents()).not.toContain(roomFilteredEvent);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { mocked } from 'jest-mock';
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { EventTimeline } from "../../src/models/event-timeline";
|
||||
import { Direction, EventTimeline } from "../../src/models/event-timeline";
|
||||
import { RoomState } from "../../src/models/room-state";
|
||||
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";
|
||||
|
||||
jest.mock("../../src/models/room-state");
|
||||
|
||||
describe("EventTimeline", function() {
|
||||
const roomId = "!foo:bar";
|
||||
const userA = "@alice:bar";
|
||||
@@ -21,9 +19,16 @@ describe("EventTimeline", function() {
|
||||
const getTimeline = (): EventTimeline => {
|
||||
const room = new Room(roomId, mockClient, userA);
|
||||
const timelineSet = new EventTimelineSet(room);
|
||||
jest.spyOn(timelineSet.room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet);
|
||||
jest.spyOn(room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet);
|
||||
|
||||
return new EventTimeline(timelineSet);
|
||||
const timeline = new EventTimeline(timelineSet);
|
||||
// We manually stub the methods we'll be mocking out later instead of mocking the whole module
|
||||
// otherwise the default member property values (e.g. paginationToken) will be incorrect
|
||||
timeline.getState(Direction.Backward)!.setStateEvents = jest.fn();
|
||||
timeline.getState(Direction.Backward)!.getSentinelMember = jest.fn();
|
||||
timeline.getState(Direction.Forward)!.setStateEvents = jest.fn();
|
||||
timeline.getState(Direction.Forward)!.getSentinelMember = jest.fn();
|
||||
return timeline;
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
@@ -55,13 +60,13 @@ describe("EventTimeline", function() {
|
||||
];
|
||||
timeline.initialiseState(events);
|
||||
// @ts-ignore private prop
|
||||
const timelineStartState = timeline.startState;
|
||||
const timelineStartState = timeline.startState!;
|
||||
expect(mocked(timelineStartState).setStateEvents).toHaveBeenCalledWith(
|
||||
events,
|
||||
{ timelineWasEmpty: undefined },
|
||||
);
|
||||
// @ts-ignore private prop
|
||||
const timelineEndState = timeline.endState;
|
||||
const timelineEndState = timeline.endState!;
|
||||
expect(mocked(timelineEndState).setStateEvents).toHaveBeenCalledWith(
|
||||
events,
|
||||
{ timelineWasEmpty: undefined },
|
||||
@@ -98,7 +103,17 @@ describe("EventTimeline", function() {
|
||||
expect(timeline.getPaginationToken(EventTimeline.FORWARDS)).toBe(null);
|
||||
});
|
||||
|
||||
it("setPaginationToken should set token", function() {
|
||||
it("setPaginationToken should set token", function() {
|
||||
timeline.setPaginationToken("back", EventTimeline.BACKWARDS);
|
||||
timeline.setPaginationToken("fwd", EventTimeline.FORWARDS);
|
||||
expect(timeline.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("back");
|
||||
expect(timeline.getPaginationToken(EventTimeline.FORWARDS)).toEqual("fwd");
|
||||
});
|
||||
|
||||
it("should be able to store pagination tokens for mixed room timelines", () => {
|
||||
const timelineSet = new EventTimelineSet(undefined);
|
||||
const timeline = new EventTimeline(timelineSet);
|
||||
|
||||
timeline.setPaginationToken("back", EventTimeline.BACKWARDS);
|
||||
timeline.setPaginationToken("fwd", EventTimeline.FORWARDS);
|
||||
expect(timeline.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("back");
|
||||
@@ -185,14 +200,14 @@ describe("EventTimeline", function() {
|
||||
sentinel.name = "Old Alice";
|
||||
sentinel.membership = "join";
|
||||
|
||||
mocked(timeline.getState(EventTimeline.FORWARDS)).getSentinelMember
|
||||
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
mocked(timeline.getState(EventTimeline.BACKWARDS)).getSentinelMember
|
||||
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
@@ -225,14 +240,14 @@ describe("EventTimeline", function() {
|
||||
sentinel.name = "Old Alice";
|
||||
sentinel.membership = "join";
|
||||
|
||||
mocked(timeline.getState(EventTimeline.FORWARDS)).getSentinelMember
|
||||
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
mocked(timeline.getState(EventTimeline.BACKWARDS)).getSentinelMember
|
||||
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
@@ -269,15 +284,15 @@ describe("EventTimeline", function() {
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: false });
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: false });
|
||||
|
||||
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
|
||||
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).
|
||||
toHaveBeenCalledWith([events[0]], { timelineWasEmpty: undefined });
|
||||
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
|
||||
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).
|
||||
toHaveBeenCalledWith([events[1]], { timelineWasEmpty: undefined });
|
||||
|
||||
expect(events[0].forwardLooking).toBe(true);
|
||||
expect(events[1].forwardLooking).toBe(true);
|
||||
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).
|
||||
not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -298,15 +313,15 @@ describe("EventTimeline", function() {
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: true });
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: true });
|
||||
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).
|
||||
toHaveBeenCalledWith([events[0]], { timelineWasEmpty: undefined });
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).
|
||||
toHaveBeenCalledWith([events[1]], { timelineWasEmpty: undefined });
|
||||
|
||||
expect(events[0].forwardLooking).toBe(false);
|
||||
expect(events[1].forwardLooking).toBe(false);
|
||||
|
||||
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
|
||||
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).
|
||||
not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -341,11 +356,11 @@ describe("EventTimeline", function() {
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: false });
|
||||
expect(timeline.getEvents().length).toEqual(2);
|
||||
|
||||
let ev = timeline.removeEvent(events[0].getId());
|
||||
let ev = timeline.removeEvent(events[0].getId()!);
|
||||
expect(ev).toBe(events[0]);
|
||||
expect(timeline.getEvents().length).toEqual(1);
|
||||
|
||||
ev = timeline.removeEvent(events[1].getId());
|
||||
ev = timeline.removeEvent(events[1].getId()!);
|
||||
expect(ev).toBe(events[1]);
|
||||
expect(timeline.getEvents().length).toEqual(0);
|
||||
});
|
||||
@@ -357,11 +372,11 @@ describe("EventTimeline", function() {
|
||||
expect(timeline.getEvents().length).toEqual(3);
|
||||
expect(timeline.getBaseIndex()).toEqual(1);
|
||||
|
||||
timeline.removeEvent(events[2].getId());
|
||||
timeline.removeEvent(events[2].getId()!);
|
||||
expect(timeline.getEvents().length).toEqual(2);
|
||||
expect(timeline.getBaseIndex()).toEqual(1);
|
||||
|
||||
timeline.removeEvent(events[1].getId());
|
||||
timeline.removeEvent(events[1].getId()!);
|
||||
expect(timeline.getEvents().length).toEqual(1);
|
||||
expect(timeline.getBaseIndex()).toEqual(0);
|
||||
});
|
||||
@@ -372,7 +387,7 @@ describe("EventTimeline", function() {
|
||||
it("should not make baseIndex assplode when removing the last event",
|
||||
function() {
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: true });
|
||||
timeline.removeEvent(events[0].getId());
|
||||
timeline.removeEvent(events[0].getId()!);
|
||||
const initialIndex = timeline.getBaseIndex();
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: false });
|
||||
timeline.addEvent(events[2], { toStartOfTimeline: false });
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2019, 2022 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 { MatrixEvent } from "../../src/models/event";
|
||||
|
||||
describe("MatrixEvent", () => {
|
||||
describe(".attemptDecryption", () => {
|
||||
let encryptedEvent;
|
||||
const eventId = 'test_encrypted_event';
|
||||
|
||||
beforeEach(() => {
|
||||
encryptedEvent = new MatrixEvent({
|
||||
event_id: eventId,
|
||||
type: 'm.room.encrypted',
|
||||
content: {
|
||||
ciphertext: 'secrets',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should retry decryption if a retry is queued', async () => {
|
||||
const eventAttemptDecryptionSpy = jest.spyOn(encryptedEvent, 'attemptDecryption');
|
||||
|
||||
const crypto = {
|
||||
decryptEvent: jest.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
// schedule a second decryption attempt while
|
||||
// the first one is still running.
|
||||
encryptedEvent.attemptDecryption(crypto);
|
||||
|
||||
const error = new Error("nope");
|
||||
error.name = 'DecryptionError';
|
||||
return Promise.reject(error);
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
clearEvent: {
|
||||
type: 'm.room.message',
|
||||
},
|
||||
});
|
||||
}),
|
||||
};
|
||||
|
||||
await encryptedEvent.attemptDecryption(crypto);
|
||||
|
||||
expect(eventAttemptDecryptionSpy).toHaveBeenCalledTimes(2);
|
||||
expect(crypto.decryptEvent).toHaveBeenCalledTimes(2);
|
||||
expect(encryptedEvent.getType()).toEqual('m.room.message');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
Copyright 2022 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 { UNREAD_THREAD_NOTIFICATIONS } from "../../src/@types/sync";
|
||||
import { Filter, IFilterDefinition } from "../../src/filter";
|
||||
import { mkEvent } from "../test-utils/test-utils";
|
||||
|
||||
@@ -28,7 +28,7 @@ describe("MatrixHttpApi", () => {
|
||||
const baseUrl = "http://baseUrl";
|
||||
const prefix = ClientPrefix.V3;
|
||||
|
||||
let xhr: Partial<Writeable<XMLHttpRequest>>;
|
||||
let xhr: Writeable<XMLHttpRequest>;
|
||||
let upload: Promise<UploadResponse>;
|
||||
|
||||
const DONE = 0;
|
||||
@@ -44,7 +44,7 @@ describe("MatrixHttpApi", () => {
|
||||
setRequestHeader: jest.fn(),
|
||||
onreadystatechange: undefined,
|
||||
getResponseHeader: jest.fn(),
|
||||
};
|
||||
} as unknown as XMLHttpRequest;
|
||||
// We stub out XHR here as it is not available in JSDOM
|
||||
// @ts-ignore
|
||||
global.XMLHttpRequest = jest.fn().mockReturnValue(xhr);
|
||||
@@ -62,7 +62,7 @@ describe("MatrixHttpApi", () => {
|
||||
});
|
||||
|
||||
it("should fall back to `fetch` where xhr is unavailable", () => {
|
||||
global.XMLHttpRequest = undefined;
|
||||
global.XMLHttpRequest = undefined!;
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({}) });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
|
||||
upload = api.uploadContent({} as File);
|
||||
|
||||
@@ -28,6 +28,8 @@ import {
|
||||
import { sleep } from "../../../src/utils";
|
||||
|
||||
jest.mock("../../../src/utils");
|
||||
// setupTests mocks `timeoutSignal` due to hanging timers
|
||||
jest.unmock("../../../src/http-api/utils");
|
||||
|
||||
describe("timeoutSignal", () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
+235
-202
@@ -36,7 +36,7 @@ import { ReceiptType } from "../../src/@types/read_receipts";
|
||||
import * as testUtils from "../test-utils/test-utils";
|
||||
import { makeBeaconInfoContent } from "../../src/content-helpers";
|
||||
import { M_BEACON_INFO } from "../../src/@types/beacon";
|
||||
import { ContentHelpers, EventTimeline, Room } from "../../src";
|
||||
import { ContentHelpers, EventTimeline, MatrixError, Room } from "../../src";
|
||||
import { supportsMatrixCall } from "../../src/webrtc/call";
|
||||
import { makeBeaconEvent } from "../test-utils/beacon";
|
||||
import {
|
||||
@@ -88,21 +88,22 @@ describe("MatrixClient", function() {
|
||||
data: SYNC_DATA,
|
||||
};
|
||||
|
||||
let httpLookups = [
|
||||
// items are objects which look like:
|
||||
// {
|
||||
// method: "GET",
|
||||
// path: "/initialSync",
|
||||
// data: {},
|
||||
// error: { errcode: M_FORBIDDEN } // if present will reject promise,
|
||||
// expectBody: {} // additional expects on the body
|
||||
// expectQueryParams: {} // additional expects on query params
|
||||
// thenCall: function(){} // function to call *AFTER* returning response.
|
||||
// }
|
||||
// items are popped off when processed and block if no items left.
|
||||
];
|
||||
// items are popped off when processed and block if no items left.
|
||||
let httpLookups: {
|
||||
method: string;
|
||||
path: string;
|
||||
data?: object;
|
||||
error?: object;
|
||||
expectBody?: object;
|
||||
expectQueryParams?: object;
|
||||
thenCall?: Function;
|
||||
}[] = [];
|
||||
let acceptKeepalives: boolean;
|
||||
let pendingLookup = null;
|
||||
let pendingLookup: {
|
||||
promise: Promise<any>;
|
||||
method: string;
|
||||
path: string;
|
||||
} | null = null;
|
||||
function httpReq(method, path, qp, data, prefix) {
|
||||
if (path === KEEP_ALIVE_PATH && acceptKeepalives) {
|
||||
return Promise.resolve({
|
||||
@@ -144,7 +145,7 @@ describe("MatrixClient", function() {
|
||||
}
|
||||
if (next.expectQueryParams) {
|
||||
Object.keys(next.expectQueryParams).forEach(function(k) {
|
||||
expect(qp[k]).toEqual(next.expectQueryParams[k]);
|
||||
expect(qp[k]).toEqual(next.expectQueryParams![k]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -155,9 +156,9 @@ describe("MatrixClient", function() {
|
||||
if (next.error) {
|
||||
// eslint-disable-next-line
|
||||
return Promise.reject({
|
||||
errcode: next.error.errcode,
|
||||
httpStatus: next.error.httpStatus,
|
||||
name: next.error.errcode,
|
||||
errcode: (<MatrixError>next.error).errcode,
|
||||
httpStatus: (<MatrixError>next.error).httpStatus,
|
||||
name: (<MatrixError>next.error).errcode,
|
||||
message: "Expected testing error",
|
||||
data: next.error,
|
||||
});
|
||||
@@ -230,6 +231,130 @@ describe("MatrixClient", function() {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
describe("sendEvent", () => {
|
||||
const roomId = "!room:example.org";
|
||||
const body = "This is the body";
|
||||
const content = { body };
|
||||
|
||||
it("overload without threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: content,
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
|
||||
it("overload with null threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: content,
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, null, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
|
||||
it("overload with threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
const threadId = "$threadId:server";
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: {
|
||||
...content,
|
||||
"m.relates_to": {
|
||||
"event_id": threadId,
|
||||
"is_falling_back": true,
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
},
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
|
||||
it("should add thread relation if threadId is passed and the relation is missing", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const threadId = "$threadId:server";
|
||||
const txnId = client.makeTxnId();
|
||||
|
||||
const room = new Room(roomId, client, userId);
|
||||
store.getRoom.mockReturnValue(room);
|
||||
|
||||
const rootEvent = new MatrixEvent({ event_id: threadId });
|
||||
room.createThread(threadId, rootEvent, [rootEvent], false);
|
||||
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: {
|
||||
...content,
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: threadId,
|
||||
},
|
||||
"event_id": threadId,
|
||||
"is_falling_back": true,
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
},
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
|
||||
it("should add thread relation if threadId is passed and the relation is missing with reply", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const threadId = "$threadId:server";
|
||||
const txnId = client.makeTxnId();
|
||||
|
||||
const content = {
|
||||
body,
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: "$other:event",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const room = new Room(roomId, client, userId);
|
||||
store.getRoom.mockReturnValue(room);
|
||||
|
||||
const rootEvent = new MatrixEvent({ event_id: threadId });
|
||||
room.createThread(threadId, rootEvent, [rootEvent], false);
|
||||
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: {
|
||||
...content,
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: "$other:event",
|
||||
},
|
||||
"event_id": threadId,
|
||||
"is_falling_back": false,
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
},
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
});
|
||||
|
||||
it("should create (unstable) file trees", async () => {
|
||||
const userId = "@test:example.org";
|
||||
const roomId = "!room:example.org";
|
||||
@@ -254,7 +379,7 @@ describe("MatrixClient", function() {
|
||||
type: UNSTABLE_MSC3088_PURPOSE.unstable,
|
||||
state_key: UNSTABLE_MSC3089_TREE_SUBTYPE.unstable,
|
||||
content: {
|
||||
[UNSTABLE_MSC3088_ENABLED.unstable]: true,
|
||||
[UNSTABLE_MSC3088_ENABLED.unstable!]: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -299,7 +424,7 @@ describe("MatrixClient", function() {
|
||||
expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable);
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[UNSTABLE_MSC3088_ENABLED.unstable]: true,
|
||||
[UNSTABLE_MSC3088_ENABLED.unstable!]: true,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -359,7 +484,7 @@ describe("MatrixClient", function() {
|
||||
expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable);
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[UNSTABLE_MSC3088_ENABLED.unstable]: true,
|
||||
[UNSTABLE_MSC3088_ENABLED.unstable!]: true,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -393,7 +518,7 @@ describe("MatrixClient", function() {
|
||||
expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable);
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[UNSTABLE_MSC3088_ENABLED.unstable]: false,
|
||||
[UNSTABLE_MSC3088_ENABLED.unstable!]: false,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -599,14 +724,14 @@ describe("MatrixClient", function() {
|
||||
}
|
||||
|
||||
it("should transition null -> PREPARED after the first /sync", function(done) {
|
||||
const expectedStates = [];
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition null -> ERROR after a failed /filter", function(done) {
|
||||
const expectedStates = [];
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push({
|
||||
@@ -620,36 +745,35 @@ describe("MatrixClient", function() {
|
||||
// Disabled because now `startClient` makes a legit call to `/versions`
|
||||
// And those tests are really unhappy about it... Not possible to figure
|
||||
// out what a good resolution would look like
|
||||
xit("should transition ERROR -> CATCHUP after /sync if prev failed",
|
||||
function(done) {
|
||||
const expectedStates = [];
|
||||
acceptKeepalives = false;
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH,
|
||||
error: { errcode: "KEEPALIVE_FAIL" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH, data: {},
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", data: SYNC_DATA,
|
||||
});
|
||||
|
||||
expectedStates.push(["RECONNECTING", null]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
expectedStates.push(["CATCHUP", "ERROR"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
xit("should transition ERROR -> CATCHUP after /sync if prev failed", function(done) {
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
acceptKeepalives = false;
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH,
|
||||
error: { errcode: "KEEPALIVE_FAIL" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH, data: {},
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", data: SYNC_DATA,
|
||||
});
|
||||
|
||||
expectedStates.push(["RECONNECTING", null]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
expectedStates.push(["CATCHUP", "ERROR"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition PREPARED -> SYNCING after /sync", function(done) {
|
||||
const expectedStates = [];
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
@@ -658,7 +782,7 @@ describe("MatrixClient", function() {
|
||||
|
||||
xit("should transition SYNCING -> ERROR after a failed /sync", function(done) {
|
||||
acceptKeepalives = false;
|
||||
const expectedStates = [];
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
|
||||
});
|
||||
@@ -675,37 +799,35 @@ describe("MatrixClient", function() {
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
xit("should transition ERROR -> SYNCING after /sync if prev failed",
|
||||
function(done) {
|
||||
const expectedStates = [];
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
|
||||
});
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["ERROR", "SYNCING"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
xit("should transition ERROR -> SYNCING after /sync if prev failed", function(done) {
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
|
||||
});
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
|
||||
it("should transition SYNCING -> SYNCING on subsequent /sync successes",
|
||||
function(done) {
|
||||
const expectedStates = [];
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["ERROR", "SYNCING"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["SYNCING", "SYNCING"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
it("should transition SYNCING -> SYNCING on subsequent /sync successes", function(done) {
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["SYNCING", "SYNCING"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
xit("should transition ERROR -> ERROR if keepalive keeps failing", function(done) {
|
||||
acceptKeepalives = false;
|
||||
const expectedStates = [];
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
|
||||
});
|
||||
@@ -779,130 +901,6 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendEvent", () => {
|
||||
const roomId = "!room:example.org";
|
||||
const body = "This is the body";
|
||||
const content = { body };
|
||||
|
||||
it("overload without threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: content,
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
|
||||
it("overload with null threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: content,
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, null, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
|
||||
it("overload with threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
const threadId = "$threadId:server";
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: {
|
||||
...content,
|
||||
"m.relates_to": {
|
||||
"event_id": threadId,
|
||||
"is_falling_back": true,
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
},
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
|
||||
it("should add thread relation if threadId is passed and the relation is missing", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const threadId = "$threadId:server";
|
||||
const txnId = client.makeTxnId();
|
||||
|
||||
const room = new Room(roomId, client, userId);
|
||||
store.getRoom.mockReturnValue(room);
|
||||
|
||||
const rootEvent = new MatrixEvent({ event_id: threadId });
|
||||
room.createThread(threadId, rootEvent, [rootEvent], false);
|
||||
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: {
|
||||
...content,
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: threadId,
|
||||
},
|
||||
"event_id": threadId,
|
||||
"is_falling_back": true,
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
},
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
|
||||
it("should add thread relation if threadId is passed and the relation is missing with reply", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const threadId = "$threadId:server";
|
||||
const txnId = client.makeTxnId();
|
||||
|
||||
const content = {
|
||||
body,
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: "$other:event",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const room = new Room(roomId, client, userId);
|
||||
store.getRoom.mockReturnValue(room);
|
||||
|
||||
const rootEvent = new MatrixEvent({ event_id: threadId });
|
||||
room.createThread(threadId, rootEvent, [rootEvent], false);
|
||||
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: {
|
||||
...content,
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: "$other:event",
|
||||
},
|
||||
"event_id": threadId,
|
||||
"is_falling_back": false,
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
},
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("redactEvent", () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
@@ -1705,4 +1703,39 @@ describe("MatrixClient", function() {
|
||||
expect(newSourceRoom._state.get(PolicyScope.User)?.[eventId]).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("using E2EE in group calls", () => {
|
||||
const opts = {
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: identityServerUrl,
|
||||
accessToken: "my.access.token",
|
||||
store: store,
|
||||
scheduler: scheduler,
|
||||
userId: userId,
|
||||
};
|
||||
|
||||
it("enables E2EE by default", () => {
|
||||
const client = new MatrixClient(opts);
|
||||
|
||||
expect(client.getUseE2eForGroupCall()).toBe(true);
|
||||
});
|
||||
|
||||
it("enables E2EE when enabled explicitly", () => {
|
||||
const client = new MatrixClient({
|
||||
useE2eForGroupCall: true,
|
||||
...opts,
|
||||
});
|
||||
|
||||
expect(client.getUseE2eForGroupCall()).toBe(true);
|
||||
});
|
||||
|
||||
it("disables E2EE if disabled explicitly", () => {
|
||||
const client = new MatrixClient({
|
||||
useE2eForGroupCall: false,
|
||||
...opts,
|
||||
});
|
||||
|
||||
expect(client.getUseE2eForGroupCall()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -312,7 +312,7 @@ describe("MSC3089Branch", () => {
|
||||
} as MatrixEvent);
|
||||
|
||||
const events = [await branch.getFileEvent(), await branch2.getFileEvent(), {
|
||||
replacingEventId: (): string => null,
|
||||
replacingEventId: (): string | undefined => undefined,
|
||||
getId: () => "$unknown",
|
||||
}];
|
||||
staticRoom.getLiveTimeline = () => ({ getEvents: () => events }) as EventTimeline;
|
||||
|
||||
@@ -135,7 +135,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Failed to fail");
|
||||
} catch (e) {
|
||||
expect(e.errcode).toEqual("M_FORBIDDEN");
|
||||
expect((<MatrixError>e).errcode).toEqual("M_FORBIDDEN");
|
||||
}
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
@@ -513,7 +513,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
function expectOrder(childRoomId: string, order: number) {
|
||||
const child = childTrees.find(c => c.roomId === childRoomId);
|
||||
expect(child).toBeDefined();
|
||||
expect(child.getOrder()).toEqual(order);
|
||||
expect(child!.getOrder()).toEqual(order);
|
||||
}
|
||||
|
||||
function makeMockChildRoom(roomId: string): Room {
|
||||
@@ -608,7 +608,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Failed to fail");
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual("Cannot set order of top level spaces currently");
|
||||
expect((<Error>e).message).toEqual("Cannot set order of top level spaces currently");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -706,7 +706,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
|
||||
const treeA = childTrees.find(c => c.roomId === a);
|
||||
expect(treeA).toBeDefined();
|
||||
await treeA.setOrder(1);
|
||||
await treeA!.setOrder(1);
|
||||
|
||||
expect(clientSendStateFn).toHaveBeenCalledTimes(3);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
@@ -743,7 +743,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
|
||||
const treeA = childTrees.find(c => c.roomId === a);
|
||||
expect(treeA).toBeDefined();
|
||||
await treeA.setOrder(1);
|
||||
await treeA!.setOrder(1);
|
||||
|
||||
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
@@ -771,7 +771,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
|
||||
const treeA = childTrees.find(c => c.roomId === a);
|
||||
expect(treeA).toBeDefined();
|
||||
await treeA.setOrder(2);
|
||||
await treeA!.setOrder(2);
|
||||
|
||||
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
@@ -800,7 +800,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
|
||||
const treeB = childTrees.find(c => c.roomId === b);
|
||||
expect(treeB).toBeDefined();
|
||||
await treeB.setOrder(2);
|
||||
await treeB!.setOrder(2);
|
||||
|
||||
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
@@ -829,7 +829,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
|
||||
const treeC = childTrees.find(ch => ch.roomId === c);
|
||||
expect(treeC).toBeDefined();
|
||||
await treeC.setOrder(1);
|
||||
await treeC!.setOrder(1);
|
||||
|
||||
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
@@ -858,7 +858,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
|
||||
const treeB = childTrees.find(ch => ch.roomId === b);
|
||||
expect(treeB).toBeDefined();
|
||||
await treeB.setOrder(2);
|
||||
await treeB!.setOrder(2);
|
||||
|
||||
expect(clientSendStateFn).toHaveBeenCalledTimes(2);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
@@ -903,7 +903,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
url: mxc,
|
||||
file: fileInfo,
|
||||
metadata: true, // additional content from test
|
||||
[UNSTABLE_MSC3089_LEAF.unstable]: {}, // test to ensure we're definitely using unstable
|
||||
[UNSTABLE_MSC3089_LEAF.unstable!]: {}, // test to ensure we're definitely using unstable
|
||||
});
|
||||
|
||||
return Promise.resolve({ event_id: fileEventId }); // eslint-disable-line camelcase
|
||||
@@ -965,7 +965,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
expect(contents).toMatchObject({
|
||||
...content,
|
||||
"m.new_content": content,
|
||||
[UNSTABLE_MSC3089_LEAF.unstable]: {}, // test to ensure we're definitely using unstable
|
||||
[UNSTABLE_MSC3089_LEAF.unstable!]: {}, // test to ensure we're definitely using unstable
|
||||
});
|
||||
|
||||
return Promise.resolve({ event_id: fileEventId }); // eslint-disable-line camelcase
|
||||
@@ -1010,7 +1010,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
|
||||
const file = tree.getFile(fileEventId);
|
||||
expect(file).toBeDefined();
|
||||
expect(file.indexEvent).toBe(fileEvent);
|
||||
expect(file!.indexEvent).toBe(fileEvent);
|
||||
});
|
||||
|
||||
it('should return falsy for unknown files', () => {
|
||||
|
||||
@@ -263,7 +263,7 @@ describe('Beacon', () => {
|
||||
roomId,
|
||||
);
|
||||
// less than the original event
|
||||
oldUpdateEvent.event.origin_server_ts = liveBeaconEvent.event.origin_server_ts - 1000;
|
||||
oldUpdateEvent.event.origin_server_ts = liveBeaconEvent.event.origin_server_ts! - 1000;
|
||||
|
||||
beacon.update(oldUpdateEvent);
|
||||
// didnt update
|
||||
|
||||
@@ -115,8 +115,53 @@ describe('MatrixEvent', () => {
|
||||
});
|
||||
|
||||
const prom = emitPromise(ev, MatrixEventEvent.VisibilityChange);
|
||||
ev.applyVisibilityEvent({ visible: false, eventId: ev.getId(), reason: null });
|
||||
ev.applyVisibilityEvent({ visible: false, eventId: ev.getId()!, reason: null });
|
||||
await prom;
|
||||
});
|
||||
});
|
||||
|
||||
describe(".attemptDecryption", () => {
|
||||
let encryptedEvent;
|
||||
const eventId = 'test_encrypted_event';
|
||||
|
||||
beforeEach(() => {
|
||||
encryptedEvent = new MatrixEvent({
|
||||
event_id: eventId,
|
||||
type: 'm.room.encrypted',
|
||||
content: {
|
||||
ciphertext: 'secrets',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should retry decryption if a retry is queued', async () => {
|
||||
const eventAttemptDecryptionSpy = jest.spyOn(encryptedEvent, 'attemptDecryption');
|
||||
|
||||
const crypto = {
|
||||
decryptEvent: jest.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
// schedule a second decryption attempt while
|
||||
// the first one is still running.
|
||||
encryptedEvent.attemptDecryption(crypto);
|
||||
|
||||
const error = new Error("nope");
|
||||
error.name = 'DecryptionError';
|
||||
return Promise.reject(error);
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
clearEvent: {
|
||||
type: 'm.room.message',
|
||||
},
|
||||
});
|
||||
}),
|
||||
};
|
||||
|
||||
await encryptedEvent.attemptDecryption(crypto);
|
||||
|
||||
expect(eventAttemptDecryptionSpy).toHaveBeenCalledTimes(2);
|
||||
expect(crypto.decryptEvent).toHaveBeenCalledTimes(2);
|
||||
expect(encryptedEvent.getType()).toEqual('m.room.message');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,11 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClient } from "../../../src/client";
|
||||
import { Room } from "../../../src/models/room";
|
||||
import { Thread } from "../../../src/models/thread";
|
||||
import { mkThread } from "../../test-utils/thread";
|
||||
import { TestClient } from "../../TestClient";
|
||||
|
||||
describe('Thread', () => {
|
||||
describe("constructor", () => {
|
||||
@@ -25,4 +29,52 @@ describe('Thread', () => {
|
||||
}).toThrow("element-web#22141: A thread requires a room in order to function");
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasUserReadEvent", () => {
|
||||
const myUserId = "@bob:example.org";
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
const testClient = new TestClient(
|
||||
myUserId,
|
||||
"DEVICE",
|
||||
"ACCESS_TOKEN",
|
||||
undefined,
|
||||
{ timelineSupport: false },
|
||||
);
|
||||
client = testClient.client;
|
||||
room = new Room("123", client, myUserId);
|
||||
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("considers own events with no RR as read", () => {
|
||||
const { thread, events } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: myUserId,
|
||||
participantUserIds: [myUserId],
|
||||
length: 2,
|
||||
});
|
||||
|
||||
expect(thread.hasUserReadEvent(myUserId, events.at(-1)!.getId() ?? "")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("considers other events with no RR as unread", () => {
|
||||
const { thread, events } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: myUserId,
|
||||
participantUserIds: ["@alice:example.org"],
|
||||
length: 2,
|
||||
});
|
||||
|
||||
expect(thread.hasUserReadEvent("@alice:example.org", events.at(-1)!.getId() ?? "")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -163,6 +163,22 @@ describe('NotificationService', function() {
|
||||
"enabled": true,
|
||||
"rule_id": ".m.rule.room_one_to_one",
|
||||
},
|
||||
{
|
||||
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" }],
|
||||
},
|
||||
],
|
||||
"room": [],
|
||||
"sender": [],
|
||||
@@ -209,32 +225,32 @@ describe('NotificationService', function() {
|
||||
msgtype: "m.text",
|
||||
},
|
||||
});
|
||||
matrixClient.pushRules = PushProcessor.rewriteDefaultRules(matrixClient.pushRules);
|
||||
matrixClient.pushRules = PushProcessor.rewriteDefaultRules(matrixClient.pushRules!);
|
||||
pushProcessor = new PushProcessor(matrixClient);
|
||||
});
|
||||
|
||||
// User IDs
|
||||
|
||||
it('should bing on a user ID.', function() {
|
||||
testEvent.event.content.body = "Hello @ali:matrix.org, how are you?";
|
||||
testEvent.event.content!.body = "Hello @ali:matrix.org, how are you?";
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on a partial user ID with an @.', function() {
|
||||
testEvent.event.content.body = "Hello @ali, how are you?";
|
||||
testEvent.event.content!.body = "Hello @ali, how are you?";
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on a partial user ID without @.', function() {
|
||||
testEvent.event.content.body = "Hello ali, how are you?";
|
||||
testEvent.event.content!.body = "Hello ali, how are you?";
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on a case-insensitive user ID.', function() {
|
||||
testEvent.event.content.body = "Hello @AlI:matrix.org, how are you?";
|
||||
testEvent.event.content!.body = "Hello @AlI:matrix.org, how are you?";
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
@@ -242,13 +258,13 @@ describe('NotificationService', function() {
|
||||
// Display names
|
||||
|
||||
it('should bing on a display name.', function() {
|
||||
testEvent.event.content.body = "Hello Alice M, how are you?";
|
||||
testEvent.event.content!.body = "Hello Alice M, how are you?";
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on a case-insensitive display name.', function() {
|
||||
testEvent.event.content.body = "Hello ALICE M, how are you?";
|
||||
testEvent.event.content!.body = "Hello ALICE M, how are you?";
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
@@ -256,43 +272,43 @@ describe('NotificationService', function() {
|
||||
// Bing words
|
||||
|
||||
it('should bing on a bing word.', function() {
|
||||
testEvent.event.content.body = "I really like coffee";
|
||||
testEvent.event.content!.body = "I really like coffee";
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on case-insensitive bing words.', function() {
|
||||
testEvent.event.content.body = "Coffee is great";
|
||||
testEvent.event.content!.body = "Coffee is great";
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on wildcard (.*) bing words.', function() {
|
||||
testEvent.event.content.body = "It was foomahbar I think.";
|
||||
testEvent.event.content!.body = "It was foomahbar I think.";
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on character group ([abc]) bing words.', function() {
|
||||
testEvent.event.content.body = "Ping!";
|
||||
testEvent.event.content!.body = "Ping!";
|
||||
let actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
testEvent.event.content.body = "Pong!";
|
||||
testEvent.event.content!.body = "Pong!";
|
||||
actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on character range ([a-z]) bing words.', function() {
|
||||
testEvent.event.content.body = "I ate 6 pies";
|
||||
testEvent.event.content!.body = "I ate 6 pies";
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on character negation ([!a]) bing words.', function() {
|
||||
testEvent.event.content.body = "boke";
|
||||
testEvent.event.content!.body = "boke";
|
||||
let actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
testEvent.event.content.body = "bake";
|
||||
testEvent.event.content!.body = "bake";
|
||||
actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(false);
|
||||
});
|
||||
@@ -316,7 +332,7 @@ describe('NotificationService', function() {
|
||||
// invalid
|
||||
|
||||
it('should gracefully handle bad input.', function() {
|
||||
testEvent.event.content.body = { "foo": "bar" };
|
||||
testEvent.event.content!.body = { "foo": "bar" };
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(false);
|
||||
});
|
||||
@@ -337,7 +353,11 @@ describe('NotificationService', function() {
|
||||
}, testEvent)).toBe(true);
|
||||
});
|
||||
|
||||
describe("performCustomEventHandling()", () => {
|
||||
describe("group call started push rule", () => {
|
||||
beforeEach(() => {
|
||||
matrixClient.pushRules!.global!.underride!.find(r => r.rule_id === ".m.rule.fallback")!.enabled = false;
|
||||
});
|
||||
|
||||
const getActionsForEvent = (prevContent: IContent, content: IContent): IActionsObject => {
|
||||
testEvent = utils.mkEvent({
|
||||
type: "org.matrix.msc3401.call",
|
||||
@@ -353,15 +373,15 @@ describe('NotificationService', function() {
|
||||
};
|
||||
|
||||
const assertDoesNotify = (actions: IActionsObject): void => {
|
||||
expect(actions.notify).toBeTruthy();
|
||||
expect(actions.tweaks.sound).toBeTruthy();
|
||||
expect(actions.tweaks.highlight).toBeFalsy();
|
||||
expect(actions?.notify).toBeTruthy();
|
||||
expect(actions?.tweaks?.sound).toBeTruthy();
|
||||
expect(actions?.tweaks?.highlight).toBeFalsy();
|
||||
};
|
||||
|
||||
const assertDoesNotNotify = (actions: IActionsObject): void => {
|
||||
expect(actions.notify).toBeFalsy();
|
||||
expect(actions.tweaks.sound).toBeFalsy();
|
||||
expect(actions.tweaks.highlight).toBeFalsy();
|
||||
expect(actions?.notify).toBeFalsy();
|
||||
expect(actions?.tweaks?.sound).toBeFalsy();
|
||||
expect(actions?.tweaks?.highlight).toBeFalsy();
|
||||
};
|
||||
|
||||
it.each(
|
||||
|
||||
@@ -22,6 +22,8 @@ import { MatrixClient } from "../../src/client";
|
||||
import { ToDeviceBatch } from '../../src/models/ToDeviceMessage';
|
||||
import { logger } from '../../src/logger';
|
||||
import { IStore } from '../../src/store';
|
||||
import { flushPromises } from '../test-utils/flushPromises';
|
||||
import { removeElement } from "../../src/utils";
|
||||
|
||||
const FAKE_USER = "@alice:example.org";
|
||||
const FAKE_DEVICE_ID = "AAAAAAAA";
|
||||
@@ -47,19 +49,6 @@ enum StoreType {
|
||||
IndexedDB = 'IndexedDB',
|
||||
}
|
||||
|
||||
// Jest now uses @sinonjs/fake-timers which exposes tickAsync() and a number of
|
||||
// other async methods which break the event loop, letting scheduled promise
|
||||
// callbacks run. Unfortunately, Jest doesn't expose these, so we have to do
|
||||
// it manually (this is what sinon does under the hood). We do both in a loop
|
||||
// until the thing we expect happens: hopefully this is the least flakey way
|
||||
// and avoids assuming anything about the app's behaviour.
|
||||
const realSetTimeout = setTimeout;
|
||||
function flushPromises() {
|
||||
return new Promise(r => {
|
||||
realSetTimeout(r, 1);
|
||||
});
|
||||
}
|
||||
|
||||
async function flushAndRunTimersUntil(cond: () => boolean) {
|
||||
while (!cond()) {
|
||||
await flushPromises();
|
||||
@@ -75,6 +64,8 @@ describe.each([
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(async function() {
|
||||
jest.runOnlyPendingTimers();
|
||||
jest.useRealTimers();
|
||||
httpBackend = new MockHttpBackend();
|
||||
|
||||
let store: IStore;
|
||||
@@ -140,11 +131,11 @@ describe.each([
|
||||
],
|
||||
});
|
||||
await flushAndRunTimersUntil(() => httpBackend.requests.length > 0);
|
||||
expect(httpBackend.flushSync(null, 1)).toEqual(1);
|
||||
expect(httpBackend.flushSync(undefined, 1)).toEqual(1);
|
||||
|
||||
await flushAndRunTimersUntil(() => httpBackend.requests.length > 0);
|
||||
|
||||
expect(httpBackend.flushSync(null, 1)).toEqual(1);
|
||||
expect(httpBackend.flushSync(undefined, 1)).toEqual(1);
|
||||
|
||||
// flush, as per comment in first test
|
||||
await flushPromises();
|
||||
@@ -164,7 +155,7 @@ describe.each([
|
||||
],
|
||||
});
|
||||
await flushAndRunTimersUntil(() => httpBackend.requests.length > 0);
|
||||
expect(httpBackend.flushSync(null, 1)).toEqual(1);
|
||||
expect(httpBackend.flushSync(undefined, 1)).toEqual(1);
|
||||
|
||||
// Asserting that another request is never made is obviously
|
||||
// a bit tricky - we just flush the queue what should hopefully
|
||||
@@ -200,7 +191,7 @@ describe.each([
|
||||
],
|
||||
});
|
||||
await flushAndRunTimersUntil(() => httpBackend.requests.length > 0);
|
||||
expect(httpBackend.flushSync(null, 1)).toEqual(1);
|
||||
expect(httpBackend.flushSync(undefined, 1)).toEqual(1);
|
||||
await flushPromises();
|
||||
|
||||
logger.info("Advancing clock to just before expected retry time...");
|
||||
@@ -215,7 +206,7 @@ describe.each([
|
||||
jest.advanceTimersByTime(2000);
|
||||
await flushPromises();
|
||||
|
||||
expect(httpBackend.flushSync(null, 1)).toEqual(1);
|
||||
expect(httpBackend.flushSync(undefined, 1)).toEqual(1);
|
||||
});
|
||||
|
||||
it("retries on retryImmediately()", async function() {
|
||||
@@ -223,7 +214,7 @@ describe.each([
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
|
||||
await Promise.all([client.startClient(), httpBackend.flush(null, 1, 20)]);
|
||||
await Promise.all([client.startClient(), httpBackend.flush(undefined, 1, 20)]);
|
||||
|
||||
httpBackend.when(
|
||||
"PUT", "/sendToDevice/org.example.foo/",
|
||||
@@ -239,13 +230,13 @@ describe.each([
|
||||
FAKE_MSG,
|
||||
],
|
||||
});
|
||||
expect(await httpBackend.flush(null, 1, 1)).toEqual(1);
|
||||
expect(await httpBackend.flush(undefined, 1, 1)).toEqual(1);
|
||||
await flushPromises();
|
||||
|
||||
client.retryImmediately();
|
||||
|
||||
// longer timeout here to try & avoid flakiness
|
||||
expect(await httpBackend.flush(null, 1, 3000)).toEqual(1);
|
||||
expect(await httpBackend.flush(undefined, 1, 3000)).toEqual(1);
|
||||
});
|
||||
|
||||
it("retries on when client is started", async function() {
|
||||
@@ -269,13 +260,13 @@ describe.each([
|
||||
FAKE_MSG,
|
||||
],
|
||||
});
|
||||
expect(await httpBackend.flush(null, 1, 1)).toEqual(1);
|
||||
expect(await httpBackend.flush(undefined, 1, 1)).toEqual(1);
|
||||
await flushPromises();
|
||||
|
||||
client.stopClient();
|
||||
await Promise.all([client.startClient(), httpBackend.flush("/_matrix/client/versions", 1, 20)]);
|
||||
|
||||
expect(await httpBackend.flush(null, 1, 20)).toEqual(1);
|
||||
expect(await httpBackend.flush(undefined, 1, 20)).toEqual(1);
|
||||
});
|
||||
|
||||
it("retries when a message is retried", async function() {
|
||||
@@ -283,7 +274,7 @@ describe.each([
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
|
||||
await Promise.all([client.startClient(), httpBackend.flush(null, 1, 20)]);
|
||||
await Promise.all([client.startClient(), httpBackend.flush(undefined, 1, 20)]);
|
||||
|
||||
httpBackend.when(
|
||||
"PUT", "/sendToDevice/org.example.foo/",
|
||||
@@ -300,7 +291,7 @@ describe.each([
|
||||
],
|
||||
});
|
||||
|
||||
expect(await httpBackend.flush(null, 1, 1)).toEqual(1);
|
||||
expect(await httpBackend.flush(undefined, 1, 20)).toEqual(1);
|
||||
await flushPromises();
|
||||
|
||||
const dummyEvent = new MatrixEvent({
|
||||
@@ -311,7 +302,7 @@ describe.each([
|
||||
} as unknown as Room;
|
||||
client.resendEvent(dummyEvent, mockRoom);
|
||||
|
||||
expect(await httpBackend.flush(null, 1, 20)).toEqual(1);
|
||||
expect(await httpBackend.flush(undefined, 1, 20)).toEqual(1);
|
||||
});
|
||||
|
||||
it("splits many messages into multiple HTTP requests", async function() {
|
||||
@@ -328,12 +319,12 @@ describe.each([
|
||||
});
|
||||
}
|
||||
|
||||
const expectedCounts = [20, 1];
|
||||
httpBackend.when(
|
||||
"PUT", "/sendToDevice/org.example.foo/",
|
||||
).check((request) => {
|
||||
expect(Object.keys(request.data.messages).length).toEqual(20);
|
||||
expect(removeElement(expectedCounts, c => c === Object.keys(request.data.messages).length)).toBeTruthy();
|
||||
}).respond(200, {});
|
||||
|
||||
httpBackend.when(
|
||||
"PUT", "/sendToDevice/org.example.foo/",
|
||||
).check((request) => {
|
||||
|
||||
@@ -18,6 +18,7 @@ import MockHttpBackend from 'matrix-mock-request';
|
||||
|
||||
import { ReceiptType } from '../../src/@types/read_receipts';
|
||||
import { MatrixClient } from "../../src/client";
|
||||
import { Feature, ServerSupport } from '../../src/feature';
|
||||
import { EventType } from '../../src/matrix';
|
||||
import { MAIN_ROOM_TIMELINE } from '../../src/models/read-receipt';
|
||||
import { encodeUri } from '../../src/utils';
|
||||
@@ -69,15 +70,8 @@ const roomEvent = utils.mkEvent({
|
||||
},
|
||||
});
|
||||
|
||||
function mockServerSideSupport(client, hasServerSideSupport) {
|
||||
const doesServerSupportUnstableFeature = client.doesServerSupportUnstableFeature;
|
||||
client.doesServerSupportUnstableFeature = (unstableFeature) => {
|
||||
if (unstableFeature === "org.matrix.msc3771") {
|
||||
return Promise.resolve(hasServerSideSupport);
|
||||
} else {
|
||||
return doesServerSupportUnstableFeature(unstableFeature);
|
||||
}
|
||||
};
|
||||
function mockServerSideSupport(client, serverSideSupport: ServerSupport) {
|
||||
client.canSupport.set(Feature.ThreadUnreadNotifications, serverSideSupport);
|
||||
}
|
||||
|
||||
describe("Read receipt", () => {
|
||||
@@ -97,31 +91,49 @@ describe("Read receipt", () => {
|
||||
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
|
||||
$roomId: ROOM_ID,
|
||||
$receiptType: ReceiptType.Read,
|
||||
$eventId: threadEvent.getId(),
|
||||
$eventId: threadEvent.getId()!,
|
||||
}),
|
||||
).check((request) => {
|
||||
expect(request.data.thread_id).toEqual(THREAD_ID);
|
||||
}).respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, true);
|
||||
mockServerSideSupport(client, ServerSupport.Stable);
|
||||
client.sendReceipt(threadEvent, ReceiptType.Read, {});
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("sends an unthreaded receipt", async () => {
|
||||
httpBackend.when(
|
||||
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
|
||||
$roomId: ROOM_ID,
|
||||
$receiptType: ReceiptType.Read,
|
||||
$eventId: threadEvent.getId()!,
|
||||
}),
|
||||
).check((request) => {
|
||||
expect(request.data.thread_id).toBeUndefined();
|
||||
}).respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, ServerSupport.Stable);
|
||||
client.sendReadReceipt(threadEvent, ReceiptType.Read, true);
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("sends a room read receipt", async () => {
|
||||
httpBackend.when(
|
||||
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
|
||||
$roomId: ROOM_ID,
|
||||
$receiptType: ReceiptType.Read,
|
||||
$eventId: roomEvent.getId(),
|
||||
$eventId: roomEvent.getId()!,
|
||||
}),
|
||||
).check((request) => {
|
||||
expect(request.data.thread_id).toEqual(MAIN_ROOM_TIMELINE);
|
||||
}).respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, true);
|
||||
mockServerSideSupport(client, ServerSupport.Stable);
|
||||
client.sendReceipt(roomEvent, ReceiptType.Read, {});
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
@@ -133,13 +145,13 @@ describe("Read receipt", () => {
|
||||
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
|
||||
$roomId: ROOM_ID,
|
||||
$receiptType: ReceiptType.Read,
|
||||
$eventId: threadEvent.getId(),
|
||||
$eventId: threadEvent.getId()!,
|
||||
}),
|
||||
).check((request) => {
|
||||
expect(request.data.thread_id).toBeUndefined();
|
||||
}).respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, false);
|
||||
mockServerSideSupport(client, ServerSupport.Unsupported);
|
||||
client.sendReceipt(threadEvent, ReceiptType.Read, {});
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
@@ -151,13 +163,13 @@ describe("Read receipt", () => {
|
||||
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
|
||||
$roomId: ROOM_ID,
|
||||
$receiptType: ReceiptType.Read,
|
||||
$eventId: threadEvent.getId(),
|
||||
$eventId: threadEvent.getId()!,
|
||||
}),
|
||||
).check((request) => {
|
||||
expect(request.data).toEqual({});
|
||||
}).respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, false);
|
||||
mockServerSideSupport(client, ServerSupport.Unsupported);
|
||||
client.sendReceipt(threadEvent, ReceiptType.Read, undefined);
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
Copyright 2022 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 * as callbacks from "../../src/realtime-callbacks";
|
||||
|
||||
let wallTime = 1234567890;
|
||||
@@ -37,7 +53,7 @@ describe("realtime-callbacks", function() {
|
||||
|
||||
it("should set 'this' to the global object", function() {
|
||||
let passed = false;
|
||||
const callback = function() {
|
||||
const callback = function(this: typeof global) {
|
||||
expect(this).toBe(global); // eslint-disable-line @typescript-eslint/no-invalid-this
|
||||
expect(this.console).toBeTruthy(); // eslint-disable-line @typescript-eslint/no-invalid-this
|
||||
passed = true;
|
||||
|
||||
@@ -18,10 +18,11 @@ import { EventTimelineSet } from "../../src/models/event-timeline-set";
|
||||
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { Relations } from "../../src/models/relations";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("Relations", function() {
|
||||
it("should deduplicate annotations", function() {
|
||||
const room = new Room("room123", null, null);
|
||||
const room = new Room("room123", null!, null!);
|
||||
const relations = new Relations("m.annotation", "m.reaction", room);
|
||||
|
||||
// Create an instance of an annotation
|
||||
@@ -43,7 +44,7 @@ describe("Relations", function() {
|
||||
// Add the event once and check results
|
||||
{
|
||||
relations.addEvent(eventA);
|
||||
const annotationsByKey = relations.getSortedAnnotationsByKey();
|
||||
const annotationsByKey = relations.getSortedAnnotationsByKey()!;
|
||||
expect(annotationsByKey.length).toEqual(1);
|
||||
const [key, events] = annotationsByKey[0];
|
||||
expect(key).toEqual("👍️");
|
||||
@@ -53,7 +54,7 @@ describe("Relations", function() {
|
||||
// Add the event again and expect the same
|
||||
{
|
||||
relations.addEvent(eventA);
|
||||
const annotationsByKey = relations.getSortedAnnotationsByKey();
|
||||
const annotationsByKey = relations.getSortedAnnotationsByKey()!;
|
||||
expect(annotationsByKey.length).toEqual(1);
|
||||
const [key, events] = annotationsByKey[0];
|
||||
expect(key).toEqual("👍️");
|
||||
@@ -66,7 +67,7 @@ describe("Relations", function() {
|
||||
// Add the event again and expect the same
|
||||
{
|
||||
relations.addEvent(eventB);
|
||||
const annotationsByKey = relations.getSortedAnnotationsByKey();
|
||||
const annotationsByKey = relations.getSortedAnnotationsByKey()!;
|
||||
expect(annotationsByKey.length).toEqual(1);
|
||||
const [key, events] = annotationsByKey[0];
|
||||
expect(key).toEqual("👍️");
|
||||
@@ -98,7 +99,7 @@ describe("Relations", function() {
|
||||
|
||||
// Add the target event first, then the relation event
|
||||
{
|
||||
const room = new Room("room123", null, null);
|
||||
const room = new Room("room123", null!, null!);
|
||||
const relationsCreated = new Promise(resolve => {
|
||||
targetEvent.once(MatrixEventEvent.RelationsCreated, resolve);
|
||||
});
|
||||
@@ -112,7 +113,7 @@ describe("Relations", function() {
|
||||
|
||||
// Add the relation event first, then the target event
|
||||
{
|
||||
const room = new Room("room123", null, null);
|
||||
const room = new Room("room123", null!, null!);
|
||||
const relationsCreated = new Promise(resolve => {
|
||||
targetEvent.once(MatrixEventEvent.RelationsCreated, resolve);
|
||||
});
|
||||
@@ -126,7 +127,7 @@ describe("Relations", function() {
|
||||
});
|
||||
|
||||
it("should re-use Relations between all timeline sets in a room", async () => {
|
||||
const room = new Room("room123", null, null);
|
||||
const room = new Room("room123", null!, null!);
|
||||
const timelineSet1 = new EventTimelineSet(room);
|
||||
const timelineSet2 = new EventTimelineSet(room);
|
||||
expect(room.relations).toBe(timelineSet1.relations);
|
||||
@@ -135,7 +136,7 @@ describe("Relations", function() {
|
||||
|
||||
it("should ignore m.replace for state events", async () => {
|
||||
const userId = "@bob:example.com";
|
||||
const room = new Room("room123", null, userId);
|
||||
const room = new Room("room123", null!, userId);
|
||||
const relations = new Relations("m.replace", "m.room.topic", room);
|
||||
|
||||
// Create an instance of a state event with rel_type m.replace
|
||||
@@ -179,4 +180,28 @@ describe("Relations", function() {
|
||||
expect(badlyEditedTopic.replacingEvent()).toBe(null);
|
||||
expect(badlyEditedTopic.getContent().topic).toBe("topic");
|
||||
});
|
||||
|
||||
it("getSortedAnnotationsByKey should return null for non-annotation relations", async () => {
|
||||
const userId = "@user:server";
|
||||
const room = new Room("room123", new TestClient(userId).client, userId);
|
||||
const relations = new Relations("m.replace", "m.room.message", room);
|
||||
|
||||
// Create an instance of an annotation
|
||||
const eventData = {
|
||||
"sender": "@bob:example.com",
|
||||
"type": "m.room.message",
|
||||
"event_id": "$cZ1biX33ENJqIm00ks0W_hgiO_6CHrsAc3ZQrnLeNTw",
|
||||
"room_id": "!pzVjCQSoQPpXQeHpmK:example.com",
|
||||
"content": {
|
||||
"m.relates_to": {
|
||||
"event_id": "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o",
|
||||
"rel_type": "m.replace",
|
||||
},
|
||||
},
|
||||
};
|
||||
const eventA = new MatrixEvent(eventData);
|
||||
|
||||
relations.addEvent(eventA);
|
||||
expect(relations.getSortedAnnotationsByKey()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
Copyright 2022 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 { logger } from "../../../src/logger";
|
||||
import {
|
||||
RendezvousFailureListener,
|
||||
RendezvousFailureReason,
|
||||
RendezvousTransport,
|
||||
RendezvousTransportDetails,
|
||||
} from "../../../src/rendezvous";
|
||||
import { sleep } from '../../../src/utils';
|
||||
|
||||
export class DummyTransport<D extends RendezvousTransportDetails, T> implements RendezvousTransport<T> {
|
||||
otherParty?: DummyTransport<D, T>;
|
||||
etag?: string;
|
||||
lastEtagReceived?: string;
|
||||
data: T | undefined;
|
||||
|
||||
ready = false;
|
||||
cancelled = false;
|
||||
|
||||
constructor(private name: string, private mockDetails: D) {}
|
||||
onCancelled?: RendezvousFailureListener;
|
||||
|
||||
details(): Promise<RendezvousTransportDetails> {
|
||||
return Promise.resolve(this.mockDetails);
|
||||
}
|
||||
|
||||
async send(data: T): Promise<void> {
|
||||
logger.info(
|
||||
`[${this.name}] => [${this.otherParty?.name}] Attempting to send data: ${
|
||||
JSON.stringify(data)} where etag matches ${this.etag}`,
|
||||
);
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (!this.cancelled) {
|
||||
if (!this.etag || (this.otherParty?.etag && this.otherParty?.etag === this.etag)) {
|
||||
this.data = data;
|
||||
this.etag = Math.random().toString();
|
||||
this.lastEtagReceived = this.etag;
|
||||
this.otherParty!.etag = this.etag;
|
||||
this.otherParty!.data = data;
|
||||
logger.info(`[${this.name}] => [${this.otherParty?.name}] Sent with etag ${this.etag}`);
|
||||
return;
|
||||
}
|
||||
logger.info(`[${this.name}] Sleeping to retry send after etag ${this.etag}`);
|
||||
await sleep(250);
|
||||
}
|
||||
}
|
||||
|
||||
async receive(): Promise<T | undefined> {
|
||||
logger.info(`[${this.name}] Attempting to receive where etag is after ${this.lastEtagReceived}`);
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (!this.cancelled) {
|
||||
if (!this.lastEtagReceived || this.lastEtagReceived !== this.etag) {
|
||||
this.lastEtagReceived = this.etag;
|
||||
logger.info(
|
||||
`[${this.otherParty?.name}] => [${this.name}] Received data: ` +
|
||||
`${JSON.stringify(this.data)} with etag ${this.etag}`,
|
||||
);
|
||||
return this.data;
|
||||
}
|
||||
logger.info(`[${this.name}] Sleeping to retry receive after etag ${
|
||||
this.lastEtagReceived} as remote is ${this.etag}`);
|
||||
await sleep(250);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
cancel(reason: RendezvousFailureReason): Promise<void> {
|
||||
this.cancelled = true;
|
||||
this.onCancelled?.(reason);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.cancelled = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
Copyright 2022 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 '../../olm-loader';
|
||||
import { RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous";
|
||||
import { MSC3903ECDHPayload, MSC3903ECDHv1RendezvousChannel } from '../../../src/rendezvous/channels';
|
||||
import { decodeBase64 } from '../../../src/crypto/olmlib';
|
||||
import { DummyTransport } from './DummyTransport';
|
||||
|
||||
function makeTransport(name: string) {
|
||||
return new DummyTransport<any, MSC3903ECDHPayload>(name, { type: 'dummy' });
|
||||
}
|
||||
|
||||
describe('ECDHv1', function() {
|
||||
beforeAll(async function() {
|
||||
await global.Olm.init();
|
||||
});
|
||||
|
||||
describe('with crypto', () => {
|
||||
it("initiator wants to sign in", async function() {
|
||||
const aliceTransport = makeTransport('Alice');
|
||||
const bobTransport = makeTransport('Bob');
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
|
||||
expect(aliceChecksum).toEqual(bobChecksum);
|
||||
|
||||
const message = { key: "xxx" };
|
||||
await alice.send(message);
|
||||
const bobReceive = await bob.receive();
|
||||
expect(bobReceive).toEqual(message);
|
||||
|
||||
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||
await bob.cancel(RendezvousFailureReason.Unknown);
|
||||
});
|
||||
|
||||
it("initiator wants to reciprocate", async function() {
|
||||
const aliceTransport = makeTransport('Alice');
|
||||
const bobTransport = makeTransport('Bob');
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
|
||||
expect(aliceChecksum).toEqual(bobChecksum);
|
||||
|
||||
const message = { key: "xxx" };
|
||||
await bob.send(message);
|
||||
const aliceReceive = await alice.receive();
|
||||
expect(aliceReceive).toEqual(message);
|
||||
|
||||
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||
await bob.cancel(RendezvousFailureReason.Unknown);
|
||||
});
|
||||
|
||||
it("double connect", async function() {
|
||||
const aliceTransport = makeTransport('Alice');
|
||||
const bobTransport = makeTransport('Bob');
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
|
||||
expect(aliceChecksum).toEqual(bobChecksum);
|
||||
|
||||
expect(alice.connect()).rejects.toThrow();
|
||||
|
||||
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||
await bob.cancel(RendezvousFailureReason.Unknown);
|
||||
});
|
||||
|
||||
it("closed", async function() {
|
||||
const aliceTransport = makeTransport('Alice');
|
||||
const bobTransport = makeTransport('Bob');
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
|
||||
expect(aliceChecksum).toEqual(bobChecksum);
|
||||
|
||||
alice.close();
|
||||
|
||||
expect(alice.connect()).rejects.toThrow();
|
||||
expect(alice.send({})).rejects.toThrow();
|
||||
expect(alice.receive()).rejects.toThrow();
|
||||
|
||||
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||
await bob.cancel(RendezvousFailureReason.Unknown);
|
||||
});
|
||||
|
||||
it("require ciphertext", async function() {
|
||||
const aliceTransport = makeTransport('Alice');
|
||||
const bobTransport = makeTransport('Bob');
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
|
||||
expect(aliceChecksum).toEqual(bobChecksum);
|
||||
|
||||
// send a message without encryption
|
||||
await aliceTransport.send({ iv: "dummy", ciphertext: "dummy" });
|
||||
expect(bob.receive()).rejects.toThrowError();
|
||||
|
||||
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||
await bob.cancel(RendezvousFailureReason.Unknown);
|
||||
});
|
||||
|
||||
it("ciphertext before set up", async function() {
|
||||
const aliceTransport = makeTransport('Alice');
|
||||
const bobTransport = makeTransport('Bob');
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||
await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
|
||||
await bobTransport.send({ iv: "dummy", ciphertext: "dummy" });
|
||||
|
||||
expect(alice.receive()).rejects.toThrowError();
|
||||
|
||||
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,602 @@
|
||||
/*
|
||||
Copyright 2022 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 MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import '../../olm-loader';
|
||||
import {
|
||||
MSC3906Rendezvous,
|
||||
RendezvousCode,
|
||||
RendezvousFailureReason,
|
||||
RendezvousIntent,
|
||||
} from "../../../src/rendezvous";
|
||||
import {
|
||||
ECDHv1RendezvousCode,
|
||||
MSC3903ECDHPayload,
|
||||
MSC3903ECDHv1RendezvousChannel,
|
||||
} from "../../../src/rendezvous/channels";
|
||||
import { MatrixClient } from "../../../src";
|
||||
import {
|
||||
MSC3886SimpleHttpRendezvousTransport,
|
||||
MSC3886SimpleHttpRendezvousTransportDetails,
|
||||
} from "../../../src/rendezvous/transports";
|
||||
import { DummyTransport } from "./DummyTransport";
|
||||
import { decodeBase64 } from "../../../src/crypto/olmlib";
|
||||
import { logger } from "../../../src/logger";
|
||||
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
|
||||
|
||||
function makeMockClient(opts: {
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
deviceKey?: string;
|
||||
msc3882Enabled: boolean;
|
||||
msc3886Enabled: boolean;
|
||||
devices?: Record<string, Partial<DeviceInfo>>;
|
||||
verificationFunction?: (
|
||||
userId: string, deviceId: string, verified: boolean, blocked: boolean, known: boolean,
|
||||
) => void;
|
||||
crossSigningIds?: Record<string, string>;
|
||||
}): MatrixClient {
|
||||
return {
|
||||
getVersions() {
|
||||
return {
|
||||
unstable_features: {
|
||||
"org.matrix.msc3882": opts.msc3882Enabled,
|
||||
"org.matrix.msc3886": opts.msc3886Enabled,
|
||||
},
|
||||
};
|
||||
},
|
||||
getUserId() { return opts.userId; },
|
||||
getDeviceId() { return opts.deviceId; },
|
||||
getDeviceEd25519Key() { return opts.deviceKey; },
|
||||
baseUrl: "https://example.com",
|
||||
crypto: {
|
||||
getStoredDevice(userId: string, deviceId: string) {
|
||||
return opts.devices?.[deviceId] ?? null;
|
||||
},
|
||||
setDeviceVerification: opts.verificationFunction,
|
||||
crossSigningInfo: {
|
||||
getId(key: string) {
|
||||
return opts.crossSigningIds?.[key];
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as MatrixClient;
|
||||
}
|
||||
|
||||
function makeTransport(name: string, uri = 'https://test.rz/123456') {
|
||||
return new DummyTransport<any, MSC3903ECDHPayload>(name, { type: 'http.v1', uri });
|
||||
}
|
||||
|
||||
describe("Rendezvous", function() {
|
||||
beforeAll(async function() {
|
||||
await global.Olm.init();
|
||||
});
|
||||
|
||||
let httpBackend: MockHttpBackend;
|
||||
let fetchFn: typeof global.fetchFn;
|
||||
let transports: DummyTransport<any, MSC3903ECDHPayload>[];
|
||||
|
||||
beforeEach(function() {
|
||||
httpBackend = new MockHttpBackend();
|
||||
fetchFn = httpBackend.fetchFn as typeof global.fetch;
|
||||
transports = [];
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
transports.forEach(x => x.cleanup());
|
||||
});
|
||||
|
||||
it("generate and cancel", async function() {
|
||||
const alice = makeMockClient({
|
||||
userId: "@alice:example.com",
|
||||
deviceId: "DEVICEID",
|
||||
msc3886Enabled: false,
|
||||
msc3882Enabled: true,
|
||||
});
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: "https://fallbackserver/rz/123",
|
||||
},
|
||||
},
|
||||
};
|
||||
const aliceTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client: alice,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
|
||||
expect(aliceRz.code).toBeUndefined();
|
||||
|
||||
const codePromise = aliceRz.generateCode();
|
||||
await httpBackend.flush('');
|
||||
|
||||
await aliceRz.generateCode();
|
||||
|
||||
expect(typeof aliceRz.code).toBe('string');
|
||||
|
||||
await codePromise;
|
||||
|
||||
const code = JSON.parse(aliceRz.code!) as RendezvousCode;
|
||||
|
||||
expect(code.intent).toEqual(RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE);
|
||||
expect(code.rendezvous?.algorithm).toEqual("org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256");
|
||||
expect(code.rendezvous?.transport.type).toEqual("org.matrix.msc3886.http.v1");
|
||||
expect((code.rendezvous?.transport as MSC3886SimpleHttpRendezvousTransportDetails).uri)
|
||||
.toEqual("https://fallbackserver/rz/123");
|
||||
|
||||
httpBackend.when("DELETE", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 204,
|
||||
headers: {},
|
||||
},
|
||||
};
|
||||
|
||||
const cancelPromise = aliceRz.cancel(RendezvousFailureReason.UserDeclined);
|
||||
await httpBackend.flush('');
|
||||
expect(cancelPromise).resolves.toBeUndefined();
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
|
||||
await aliceRz.close();
|
||||
});
|
||||
|
||||
it("no protocols", async function() {
|
||||
const aliceTransport = makeTransport('Alice');
|
||||
const bobTransport = makeTransport('Bob', 'https://test.rz/999999');
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const alice = makeMockClient({
|
||||
userId: "alice",
|
||||
deviceId: "ALICE",
|
||||
msc3882Enabled: false,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
const aliceStartProm = aliceRz.startAfterShowingCode();
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
);
|
||||
|
||||
const bobStartPromise = (async () => {
|
||||
const bobChecksum = await bobEcdh.connect();
|
||||
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
|
||||
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
|
||||
|
||||
// wait for protocols
|
||||
logger.info('Bob waiting for protocols');
|
||||
const protocols = await bobEcdh.receive();
|
||||
|
||||
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
|
||||
|
||||
expect(protocols).toEqual({
|
||||
type: 'm.login.finish',
|
||||
outcome: 'unsupported',
|
||||
});
|
||||
})();
|
||||
|
||||
await aliceStartProm;
|
||||
await bobStartPromise;
|
||||
});
|
||||
|
||||
it("new device declines protocol", async function() {
|
||||
const aliceTransport = makeTransport('Alice', 'https://test.rz/123456');
|
||||
const bobTransport = makeTransport('Bob', 'https://test.rz/999999');
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const alice = makeMockClient({
|
||||
userId: "alice",
|
||||
deviceId: "ALICE",
|
||||
msc3882Enabled: true,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
const aliceStartProm = aliceRz.startAfterShowingCode();
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
);
|
||||
|
||||
const bobStartPromise = (async () => {
|
||||
const bobChecksum = await bobEcdh.connect();
|
||||
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
|
||||
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
|
||||
|
||||
// wait for protocols
|
||||
logger.info('Bob waiting for protocols');
|
||||
const protocols = await bobEcdh.receive();
|
||||
|
||||
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
|
||||
|
||||
expect(protocols).toEqual({
|
||||
type: 'm.login.progress',
|
||||
protocols: ['org.matrix.msc3906.login_token'],
|
||||
});
|
||||
|
||||
await bobEcdh.send({ type: 'm.login.finish', outcome: 'unsupported' });
|
||||
})();
|
||||
|
||||
await aliceStartProm;
|
||||
await bobStartPromise;
|
||||
|
||||
expect(aliceOnFailure).toHaveBeenCalledWith(RendezvousFailureReason.UnsupportedAlgorithm);
|
||||
});
|
||||
|
||||
it("new device declines protocol", async function() {
|
||||
const aliceTransport = makeTransport('Alice', 'https://test.rz/123456');
|
||||
const bobTransport = makeTransport('Bob', 'https://test.rz/999999');
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const alice = makeMockClient({
|
||||
userId: "alice",
|
||||
deviceId: "ALICE",
|
||||
msc3882Enabled: true,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
const aliceStartProm = aliceRz.startAfterShowingCode();
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
);
|
||||
|
||||
const bobStartPromise = (async () => {
|
||||
const bobChecksum = await bobEcdh.connect();
|
||||
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
|
||||
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
|
||||
|
||||
// wait for protocols
|
||||
logger.info('Bob waiting for protocols');
|
||||
const protocols = await bobEcdh.receive();
|
||||
|
||||
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
|
||||
|
||||
expect(protocols).toEqual({
|
||||
type: 'm.login.progress',
|
||||
protocols: ['org.matrix.msc3906.login_token'],
|
||||
});
|
||||
|
||||
await bobEcdh.send({ type: 'm.login.progress', protocol: 'bad protocol' });
|
||||
})();
|
||||
|
||||
await aliceStartProm;
|
||||
await bobStartPromise;
|
||||
|
||||
expect(aliceOnFailure).toHaveBeenCalledWith(RendezvousFailureReason.UnsupportedAlgorithm);
|
||||
});
|
||||
|
||||
it("decline on existing device", async function() {
|
||||
const aliceTransport = makeTransport('Alice', 'https://test.rz/123456');
|
||||
const bobTransport = makeTransport('Bob', 'https://test.rz/999999');
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const alice = makeMockClient({
|
||||
userId: "alice",
|
||||
deviceId: "ALICE",
|
||||
msc3882Enabled: true,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
const aliceStartProm = aliceRz.startAfterShowingCode();
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
);
|
||||
|
||||
const bobStartPromise = (async () => {
|
||||
const bobChecksum = await bobEcdh.connect();
|
||||
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
|
||||
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
|
||||
|
||||
// wait for protocols
|
||||
logger.info('Bob waiting for protocols');
|
||||
const protocols = await bobEcdh.receive();
|
||||
|
||||
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
|
||||
|
||||
expect(protocols).toEqual({
|
||||
type: 'm.login.progress',
|
||||
protocols: ['org.matrix.msc3906.login_token'],
|
||||
});
|
||||
|
||||
await bobEcdh.send({ type: 'm.login.progress', protocol: 'org.matrix.msc3906.login_token' });
|
||||
})();
|
||||
|
||||
await aliceStartProm;
|
||||
await bobStartPromise;
|
||||
|
||||
await aliceRz.declineLoginOnExistingDevice();
|
||||
const loginToken = await bobEcdh.receive();
|
||||
expect(loginToken).toEqual({ type: 'm.login.finish', outcome: 'declined' });
|
||||
});
|
||||
|
||||
it("approve on existing device + no verification", async function() {
|
||||
const aliceTransport = makeTransport('Alice', 'https://test.rz/123456');
|
||||
const bobTransport = makeTransport('Bob', 'https://test.rz/999999');
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const alice = makeMockClient({
|
||||
userId: "alice",
|
||||
deviceId: "ALICE",
|
||||
msc3882Enabled: true,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
const aliceStartProm = aliceRz.startAfterShowingCode();
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
);
|
||||
|
||||
const bobStartPromise = (async () => {
|
||||
const bobChecksum = await bobEcdh.connect();
|
||||
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
|
||||
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
|
||||
|
||||
// wait for protocols
|
||||
logger.info('Bob waiting for protocols');
|
||||
const protocols = await bobEcdh.receive();
|
||||
|
||||
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
|
||||
|
||||
expect(protocols).toEqual({
|
||||
type: 'm.login.progress',
|
||||
protocols: ['org.matrix.msc3906.login_token'],
|
||||
});
|
||||
|
||||
await bobEcdh.send({ type: 'm.login.progress', protocol: 'org.matrix.msc3906.login_token' });
|
||||
})();
|
||||
|
||||
await aliceStartProm;
|
||||
await bobStartPromise;
|
||||
|
||||
const confirmProm = aliceRz.approveLoginOnExistingDevice("token");
|
||||
|
||||
const bobCompleteProm = (async () => {
|
||||
const loginToken = await bobEcdh.receive();
|
||||
expect(loginToken).toEqual({ type: 'm.login.progress', login_token: 'token', homeserver: alice.baseUrl });
|
||||
await bobEcdh.send({ type: 'm.login.finish', outcome: 'success' });
|
||||
})();
|
||||
|
||||
await confirmProm;
|
||||
await bobCompleteProm;
|
||||
});
|
||||
|
||||
async function completeLogin(devices: Record<string, Partial<DeviceInfo>>) {
|
||||
const aliceTransport = makeTransport('Alice', 'https://test.rz/123456');
|
||||
const bobTransport = makeTransport('Bob', 'https://test.rz/999999');
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const aliceVerification = jest.fn();
|
||||
const alice = makeMockClient({
|
||||
userId: "alice",
|
||||
deviceId: "ALICE",
|
||||
msc3882Enabled: true,
|
||||
msc3886Enabled: false,
|
||||
devices,
|
||||
deviceKey: 'aaaa',
|
||||
verificationFunction: aliceVerification,
|
||||
crossSigningIds: {
|
||||
master: 'mmmmm',
|
||||
},
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
const aliceStartProm = aliceRz.startAfterShowingCode();
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
);
|
||||
|
||||
const bobStartPromise = (async () => {
|
||||
const bobChecksum = await bobEcdh.connect();
|
||||
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
|
||||
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
|
||||
|
||||
// wait for protocols
|
||||
logger.info('Bob waiting for protocols');
|
||||
const protocols = await bobEcdh.receive();
|
||||
|
||||
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
|
||||
|
||||
expect(protocols).toEqual({
|
||||
type: 'm.login.progress',
|
||||
protocols: ['org.matrix.msc3906.login_token'],
|
||||
});
|
||||
|
||||
await bobEcdh.send({ type: 'm.login.progress', protocol: 'org.matrix.msc3906.login_token' });
|
||||
})();
|
||||
|
||||
await aliceStartProm;
|
||||
await bobStartPromise;
|
||||
|
||||
const confirmProm = aliceRz.approveLoginOnExistingDevice("token");
|
||||
|
||||
const bobLoginProm = (async () => {
|
||||
const loginToken = await bobEcdh.receive();
|
||||
expect(loginToken).toEqual({ type: 'm.login.progress', login_token: 'token', homeserver: alice.baseUrl });
|
||||
await bobEcdh.send({ type: 'm.login.finish', outcome: 'success', device_id: 'BOB', device_key: 'bbbb' });
|
||||
})();
|
||||
|
||||
expect(await confirmProm).toEqual('BOB');
|
||||
await bobLoginProm;
|
||||
|
||||
return {
|
||||
aliceTransport,
|
||||
aliceEcdh,
|
||||
aliceRz,
|
||||
bobTransport,
|
||||
bobEcdh,
|
||||
};
|
||||
}
|
||||
|
||||
it("approve on existing device + verification", async function() {
|
||||
const { bobEcdh, aliceRz } = await completeLogin({
|
||||
BOB: {
|
||||
getFingerprint: () => "bbbb",
|
||||
},
|
||||
});
|
||||
const verifyProm = aliceRz.verifyNewDeviceOnExistingDevice();
|
||||
|
||||
const bobVerifyProm = (async () => {
|
||||
const verified = await bobEcdh.receive();
|
||||
expect(verified).toEqual({
|
||||
type: 'm.login.finish',
|
||||
outcome: 'verified',
|
||||
verifying_device_id: 'ALICE',
|
||||
verifying_device_key: 'aaaa',
|
||||
master_key: 'mmmmm',
|
||||
});
|
||||
})();
|
||||
|
||||
await verifyProm;
|
||||
await bobVerifyProm;
|
||||
});
|
||||
|
||||
it("device not online within timeout", async function() {
|
||||
const { aliceRz } = await completeLogin({});
|
||||
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrowError();
|
||||
});
|
||||
|
||||
it("device appears online within timeout", async function() {
|
||||
const devices: Record<string, Partial<DeviceInfo>> = {};
|
||||
const { aliceRz } = await completeLogin(devices);
|
||||
// device appears after 1 second
|
||||
setTimeout(() => {
|
||||
devices.BOB = {
|
||||
getFingerprint: () => "bbbb",
|
||||
};
|
||||
}, 1000);
|
||||
await aliceRz.verifyNewDeviceOnExistingDevice(2000);
|
||||
});
|
||||
|
||||
it("device appears online after timeout", async function() {
|
||||
const devices: Record<string, Partial<DeviceInfo>> = {};
|
||||
const { aliceRz } = await completeLogin(devices);
|
||||
// device appears after 1 second
|
||||
setTimeout(() => {
|
||||
devices.BOB = {
|
||||
getFingerprint: () => "bbbb",
|
||||
};
|
||||
}, 1500);
|
||||
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrowError();
|
||||
});
|
||||
|
||||
it("mismatched device key", async function() {
|
||||
const { aliceRz } = await completeLogin({
|
||||
BOB: {
|
||||
getFingerprint: () => "XXXX",
|
||||
},
|
||||
});
|
||||
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrowError(/different key/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,451 @@
|
||||
/*
|
||||
Copyright 2022 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 MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import type { MatrixClient } from "../../../src";
|
||||
import { RendezvousFailureReason } from "../../../src/rendezvous";
|
||||
import { MSC3886SimpleHttpRendezvousTransport } from "../../../src/rendezvous/transports";
|
||||
|
||||
function makeMockClient(opts: { userId: string, deviceId: string, msc3886Enabled: boolean}): MatrixClient {
|
||||
return {
|
||||
doesServerSupportUnstableFeature(feature: string) {
|
||||
return Promise.resolve(opts.msc3886Enabled && feature === "org.matrix.msc3886");
|
||||
},
|
||||
getUserId() { return opts.userId; },
|
||||
getDeviceId() { return opts.deviceId; },
|
||||
requestLoginToken() {
|
||||
return Promise.resolve({ login_token: "token" });
|
||||
},
|
||||
baseUrl: "https://example.com",
|
||||
} as unknown as MatrixClient;
|
||||
}
|
||||
|
||||
describe("SimpleHttpRendezvousTransport", function() {
|
||||
let httpBackend: MockHttpBackend;
|
||||
let fetchFn: typeof global.fetch;
|
||||
|
||||
beforeEach(function() {
|
||||
httpBackend = new MockHttpBackend();
|
||||
fetchFn = httpBackend.fetchFn as typeof global.fetch;
|
||||
});
|
||||
|
||||
async function postAndCheckLocation(
|
||||
msc3886Enabled: boolean,
|
||||
fallbackRzServer: string,
|
||||
locationResponse: string,
|
||||
expectedFinalLocation: string,
|
||||
) {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ client, fallbackRzServer, fetchFn });
|
||||
{ // initial POST
|
||||
const expectedPostLocation = msc3886Enabled ?
|
||||
`${client.baseUrl}/_matrix/client/unstable/org.matrix.msc3886/rendezvous` :
|
||||
fallbackRzServer;
|
||||
|
||||
const prom = simpleHttpTransport.send({});
|
||||
httpBackend.when("POST", expectedPostLocation).response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: locationResponse,
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('');
|
||||
await prom;
|
||||
}
|
||||
const details = await simpleHttpTransport.details();
|
||||
expect(details.uri).toBe(expectedFinalLocation);
|
||||
|
||||
{ // first GET without etag
|
||||
const prom = simpleHttpTransport.receive();
|
||||
httpBackend.when("GET", expectedFinalLocation).response = {
|
||||
body: {},
|
||||
response: {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('');
|
||||
expect(await prom).toEqual({});
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
}
|
||||
}
|
||||
it("should throw an error when no server available", function() {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ client, fetchFn });
|
||||
expect(simpleHttpTransport.send({})).rejects.toThrowError("Invalid rendezvous URI");
|
||||
});
|
||||
|
||||
it("POST to fallback server", async function() {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
const prom = simpleHttpTransport.send({});
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: "https://fallbackserver/rz/123",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('');
|
||||
expect(await prom).toStrictEqual(undefined);
|
||||
});
|
||||
|
||||
it("POST with no location", async function() {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
const prom = simpleHttpTransport.send({});
|
||||
expect(prom).rejects.toThrowError();
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('');
|
||||
});
|
||||
|
||||
it("POST with absolute path response", async function() {
|
||||
await postAndCheckLocation(
|
||||
false,
|
||||
"https://fallbackserver/rz",
|
||||
"/123",
|
||||
"https://fallbackserver/123",
|
||||
);
|
||||
});
|
||||
|
||||
it("POST to built-in MSC3886 implementation", async function() {
|
||||
await postAndCheckLocation(
|
||||
true,
|
||||
"https://fallbackserver/rz",
|
||||
"123",
|
||||
"https://example.com/_matrix/client/unstable/org.matrix.msc3886/rendezvous/123",
|
||||
);
|
||||
});
|
||||
|
||||
it("POST with relative path response including parent", async function() {
|
||||
await postAndCheckLocation(
|
||||
false,
|
||||
"https://fallbackserver/rz/abc",
|
||||
"../xyz/123",
|
||||
"https://fallbackserver/rz/xyz/123",
|
||||
);
|
||||
});
|
||||
|
||||
it("POST with relative path response including parent", async function() {
|
||||
await postAndCheckLocation(
|
||||
false,
|
||||
"https://fallbackserver/rz/abc",
|
||||
"../xyz/123",
|
||||
"https://fallbackserver/rz/xyz/123",
|
||||
);
|
||||
});
|
||||
|
||||
it("POST to follow 307 to other server", async function() {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
const prom = simpleHttpTransport.send({});
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 307,
|
||||
headers: {
|
||||
location: "https://redirected.fallbackserver/rz",
|
||||
},
|
||||
},
|
||||
};
|
||||
httpBackend.when("POST", "https://redirected.fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: "https://redirected.fallbackserver/rz/123",
|
||||
etag: "aaa",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('');
|
||||
expect(await prom).toStrictEqual(undefined);
|
||||
});
|
||||
|
||||
it("POST and GET", async function() {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
{ // initial POST
|
||||
const prom = simpleHttpTransport.send({ foo: "baa" });
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => {
|
||||
expect(headers["content-type"]).toEqual("application/json");
|
||||
expect(data).toEqual({ foo: "baa" });
|
||||
}).response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: "https://fallbackserver/rz/123",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('');
|
||||
expect(await prom).toStrictEqual(undefined);
|
||||
}
|
||||
{ // first GET without etag
|
||||
const prom = simpleHttpTransport.receive();
|
||||
httpBackend.when("GET", "https://fallbackserver/rz/123").response = {
|
||||
body: { foo: "baa" },
|
||||
response: {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"etag": "aaa",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('');
|
||||
expect(await prom).toEqual({ foo: "baa" });
|
||||
}
|
||||
{ // subsequent GET which should have etag from previous request
|
||||
const prom = simpleHttpTransport.receive();
|
||||
httpBackend.when("GET", "https://fallbackserver/rz/123").check(({ headers }) => {
|
||||
expect(headers["if-none-match"]).toEqual("aaa");
|
||||
}).response = {
|
||||
body: { foo: "baa" },
|
||||
response: {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"etag": "bbb",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('');
|
||||
expect(await prom).toEqual({ foo: "baa" });
|
||||
}
|
||||
});
|
||||
|
||||
it("POST and PUTs", async function() {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
{ // initial POST
|
||||
const prom = simpleHttpTransport.send({ foo: "baa" });
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => {
|
||||
expect(headers["content-type"]).toEqual("application/json");
|
||||
expect(data).toEqual({ foo: "baa" });
|
||||
}).response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: "https://fallbackserver/rz/123",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('', 1);
|
||||
await prom;
|
||||
}
|
||||
{ // first PUT without etag
|
||||
const prom = simpleHttpTransport.send({ a: "b" });
|
||||
httpBackend.when("PUT", "https://fallbackserver/rz/123").check(({ headers, data }) => {
|
||||
expect(headers["if-match"]).toBeUndefined();
|
||||
expect(data).toEqual({ a: "b" });
|
||||
}).response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 202,
|
||||
headers: {
|
||||
"etag": "aaa",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('', 1);
|
||||
await prom;
|
||||
}
|
||||
{ // subsequent PUT which should have etag from previous request
|
||||
const prom = simpleHttpTransport.send({ c: "d" });
|
||||
httpBackend.when("PUT", "https://fallbackserver/rz/123").check(({ headers }) => {
|
||||
expect(headers["if-match"]).toEqual("aaa");
|
||||
}).response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 202,
|
||||
headers: {
|
||||
"etag": "bbb",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('', 1);
|
||||
await prom;
|
||||
}
|
||||
});
|
||||
|
||||
it("POST and DELETE", async function() {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
{ // Create
|
||||
const prom = simpleHttpTransport.send({ foo: "baa" });
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => {
|
||||
expect(headers["content-type"]).toEqual("application/json");
|
||||
expect(data).toEqual({ foo: "baa" });
|
||||
}).response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: "https://fallbackserver/rz/123",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('');
|
||||
expect(await prom).toStrictEqual(undefined);
|
||||
}
|
||||
{ // Cancel
|
||||
const prom = simpleHttpTransport.cancel(RendezvousFailureReason.UserDeclined);
|
||||
httpBackend.when("DELETE", "https://fallbackserver/rz/123").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 204,
|
||||
headers: {},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('');
|
||||
await prom;
|
||||
}
|
||||
});
|
||||
|
||||
it("details before ready", async function() {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
expect(simpleHttpTransport.details()).rejects.toThrowError();
|
||||
});
|
||||
|
||||
it("send after cancelled", async function() {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
await simpleHttpTransport.cancel(RendezvousFailureReason.UserDeclined);
|
||||
expect(simpleHttpTransport.send({})).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("receive before ready", async function() {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
expect(simpleHttpTransport.receive()).rejects.toThrowError();
|
||||
});
|
||||
|
||||
it("404 failure callback", async function() {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const onFailure = jest.fn();
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
onFailure,
|
||||
});
|
||||
|
||||
expect(simpleHttpTransport.send({ foo: "baa" })).resolves.toBeUndefined();
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 404,
|
||||
headers: {},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('', 1);
|
||||
expect(onFailure).toBeCalledWith(RendezvousFailureReason.Unknown);
|
||||
});
|
||||
|
||||
it("404 failure callback mapped to expired", async function() {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const onFailure = jest.fn();
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
onFailure,
|
||||
});
|
||||
|
||||
{ // initial POST
|
||||
const prom = simpleHttpTransport.send({ foo: "baa" });
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: "https://fallbackserver/rz/123",
|
||||
expires: "Thu, 01 Jan 1970 00:00:00 GMT",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('');
|
||||
await prom;
|
||||
}
|
||||
{ // GET with 404 to simulate expiry
|
||||
expect(simpleHttpTransport.receive()).resolves.toBeUndefined();
|
||||
httpBackend.when("GET", "https://fallbackserver/rz/123").response = {
|
||||
body: { foo: "baa" },
|
||||
response: {
|
||||
statusCode: 404,
|
||||
headers: {},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush('');
|
||||
expect(onFailure).toBeCalledWith(RendezvousFailureReason.Expired);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -172,7 +172,7 @@ describe("RoomState", function() {
|
||||
state.on(RoomStateEvent.Members, function(ev, st, mem) {
|
||||
expect(ev).toEqual(memberEvents[emitCount]);
|
||||
expect(st).toEqual(state);
|
||||
expect(mem).toEqual(state.getMember(ev.getSender()));
|
||||
expect(mem).toEqual(state.getMember(ev.getSender()!));
|
||||
emitCount += 1;
|
||||
});
|
||||
state.setStateEvents(memberEvents);
|
||||
|
||||
+281
-92
@@ -24,7 +24,7 @@ import {
|
||||
DuplicateStrategy,
|
||||
EventStatus,
|
||||
EventTimelineSet,
|
||||
EventType,
|
||||
EventType, IStateEventWithRoomId,
|
||||
JoinRule,
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
@@ -39,7 +39,7 @@ import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
import { ReceiptType } from "../../src/@types/read_receipts";
|
||||
import { FeatureSupport, Thread, ThreadEvent } from "../../src/models/thread";
|
||||
import { FeatureSupport, Thread, ThreadEvent, THREAD_RELATION_TYPE } from "../../src/models/thread";
|
||||
import { WrappedReceipt } from "../../src/models/read-receipt";
|
||||
import { Crypto } from "../../src/crypto";
|
||||
|
||||
@@ -66,7 +66,7 @@ describe("Room", function() {
|
||||
"body": "Reply :: " + Math.random(),
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
"event_id": target.getId(),
|
||||
"event_id": target.getId()!,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -84,7 +84,7 @@ describe("Room", function() {
|
||||
},
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Replace,
|
||||
event_id: target.getId(),
|
||||
event_id: target.getId()!,
|
||||
},
|
||||
},
|
||||
}, room.client);
|
||||
@@ -97,9 +97,9 @@ describe("Room", function() {
|
||||
content: {
|
||||
"body": "Thread response :: " + Math.random(),
|
||||
"m.relates_to": {
|
||||
"event_id": root.getId(),
|
||||
"event_id": root.getId()!,
|
||||
"m.in_reply_to": {
|
||||
"event_id": root.getId(),
|
||||
"event_id": root.getId()!,
|
||||
},
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
@@ -114,7 +114,7 @@ describe("Room", function() {
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
"rel_type": RelationType.Annotation,
|
||||
"event_id": target.getId(),
|
||||
"event_id": target.getId()!,
|
||||
"key": Math.random().toString(),
|
||||
},
|
||||
},
|
||||
@@ -125,7 +125,7 @@ describe("Room", function() {
|
||||
type: EventType.RoomRedaction,
|
||||
user: userA,
|
||||
room: roomId,
|
||||
redacts: target.getId(),
|
||||
redacts: target.getId()!,
|
||||
content: {},
|
||||
}, room.client);
|
||||
|
||||
@@ -601,7 +601,7 @@ describe("Room", function() {
|
||||
});
|
||||
|
||||
const resetTimelineTests = function(timelineSupport) {
|
||||
let events = null;
|
||||
let events: MatrixEvent[];
|
||||
|
||||
beforeEach(function() {
|
||||
room = new Room(roomId, new TestClient(userA).client, userA, { timelineSupport: timelineSupport });
|
||||
@@ -722,13 +722,13 @@ describe("Room", function() {
|
||||
it("should handle events in the same timeline", function() {
|
||||
room.addLiveEvents(events);
|
||||
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId(),
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!,
|
||||
events[1].getId()))
|
||||
.toBeLessThan(0);
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[2].getId(),
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[2].getId()!,
|
||||
events[1].getId()))
|
||||
.toBeGreaterThan(0);
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId(),
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!,
|
||||
events[1].getId()))
|
||||
.toEqual(0);
|
||||
});
|
||||
@@ -741,10 +741,10 @@ describe("Room", function() {
|
||||
room.addEventsToTimeline([events[0]], false, oldTimeline);
|
||||
room.addLiveEvents([events[1]]);
|
||||
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId(),
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!,
|
||||
events[1].getId()))
|
||||
.toBeLessThan(0);
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId(),
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!,
|
||||
events[0].getId()))
|
||||
.toBeGreaterThan(0);
|
||||
});
|
||||
@@ -755,10 +755,10 @@ describe("Room", function() {
|
||||
room.addEventsToTimeline([events[0]], false, oldTimeline);
|
||||
room.addLiveEvents([events[1]]);
|
||||
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId(),
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!,
|
||||
events[1].getId()))
|
||||
.toBe(null);
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId(),
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!,
|
||||
events[0].getId()))
|
||||
.toBe(null);
|
||||
});
|
||||
@@ -767,13 +767,13 @@ describe("Room", function() {
|
||||
room.addLiveEvents(events);
|
||||
|
||||
expect(room.getUnfilteredTimelineSet()
|
||||
.compareEventOrdering(events[0].getId(), "xxx"))
|
||||
.compareEventOrdering(events[0].getId()!, "xxx"))
|
||||
.toBe(null);
|
||||
expect(room.getUnfilteredTimelineSet()
|
||||
.compareEventOrdering("xxx", events[0].getId()))
|
||||
.toBe(null);
|
||||
expect(room.getUnfilteredTimelineSet()
|
||||
.compareEventOrdering(events[0].getId(), events[0].getId()))
|
||||
.compareEventOrdering(events[0].getId()!, events[0].getId()))
|
||||
.toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -1228,7 +1228,7 @@ describe("Room", function() {
|
||||
it("should store the receipt so it can be obtained via getReceiptsForEvent", function() {
|
||||
const ts = 13787898424;
|
||||
room.addReceipt(mkReceipt(roomId, [
|
||||
mkRecord(eventToAck.getId(), "m.read", userB, ts),
|
||||
mkRecord(eventToAck.getId()!, "m.read", userB, ts),
|
||||
]));
|
||||
expect(room.getReceiptsForEvent(eventToAck)).toEqual([{
|
||||
type: "m.read",
|
||||
@@ -1247,7 +1247,7 @@ describe("Room", function() {
|
||||
const ts = 13787898424;
|
||||
|
||||
const receiptEvent = mkReceipt(roomId, [
|
||||
mkRecord(eventToAck.getId(), "m.read", userB, ts),
|
||||
mkRecord(eventToAck.getId()!, "m.read", userB, ts),
|
||||
]);
|
||||
|
||||
room.addReceipt(receiptEvent);
|
||||
@@ -1261,11 +1261,11 @@ describe("Room", function() {
|
||||
});
|
||||
const ts = 13787898424;
|
||||
room.addReceipt(mkReceipt(roomId, [
|
||||
mkRecord(eventToAck.getId(), "m.read", userB, ts),
|
||||
mkRecord(eventToAck.getId()!, "m.read", userB, ts),
|
||||
]));
|
||||
const ts2 = 13787899999;
|
||||
room.addReceipt(mkReceipt(roomId, [
|
||||
mkRecord(nextEventToAck.getId(), "m.read", userB, ts2),
|
||||
mkRecord(nextEventToAck.getId()!, "m.read", userB, ts2),
|
||||
]));
|
||||
expect(room.getReceiptsForEvent(eventToAck)).toEqual([]);
|
||||
expect(room.getReceiptsForEvent(nextEventToAck)).toEqual([{
|
||||
@@ -1280,9 +1280,9 @@ describe("Room", function() {
|
||||
it("should persist multiple receipts for a single event ID", function() {
|
||||
const ts = 13787898424;
|
||||
room.addReceipt(mkReceipt(roomId, [
|
||||
mkRecord(eventToAck.getId(), "m.read", userB, ts),
|
||||
mkRecord(eventToAck.getId(), "m.read", userC, ts),
|
||||
mkRecord(eventToAck.getId(), "m.read", userD, ts),
|
||||
mkRecord(eventToAck.getId()!, "m.read", userB, ts),
|
||||
mkRecord(eventToAck.getId()!, "m.read", userC, ts),
|
||||
mkRecord(eventToAck.getId()!, "m.read", userD, ts),
|
||||
]));
|
||||
expect(room.getUsersReadUpTo(eventToAck)).toEqual(
|
||||
[userB, userC, userD],
|
||||
@@ -1300,9 +1300,9 @@ describe("Room", function() {
|
||||
});
|
||||
const ts = 13787898424;
|
||||
room.addReceipt(mkReceipt(roomId, [
|
||||
mkRecord(eventToAck.getId(), "m.read", userB, ts),
|
||||
mkRecord(eventTwo.getId(), "m.read", userC, ts),
|
||||
mkRecord(eventThree.getId(), "m.read", userD, ts),
|
||||
mkRecord(eventToAck.getId()!, "m.read", userB, ts),
|
||||
mkRecord(eventTwo.getId()!, "m.read", userC, ts),
|
||||
mkRecord(eventThree.getId()!, "m.read", userD, ts),
|
||||
]));
|
||||
expect(room.getUsersReadUpTo(eventToAck)).toEqual([userB]);
|
||||
expect(room.getUsersReadUpTo(eventTwo)).toEqual([userC]);
|
||||
@@ -1311,9 +1311,9 @@ describe("Room", function() {
|
||||
|
||||
it("should persist multiple receipts for a single user ID", function() {
|
||||
room.addReceipt(mkReceipt(roomId, [
|
||||
mkRecord(eventToAck.getId(), "m.delivered", userB, 13787898424),
|
||||
mkRecord(eventToAck.getId(), "m.read", userB, 22222222),
|
||||
mkRecord(eventToAck.getId(), "m.seen", userB, 33333333),
|
||||
mkRecord(eventToAck.getId()!, "m.delivered", userB, 13787898424),
|
||||
mkRecord(eventToAck.getId()!, "m.read", userB, 22222222),
|
||||
mkRecord(eventToAck.getId()!, "m.seen", userB, 33333333),
|
||||
]));
|
||||
expect(room.getReceiptsForEvent(eventToAck)).toEqual([
|
||||
{
|
||||
@@ -1361,19 +1361,19 @@ describe("Room", function() {
|
||||
|
||||
// check it initialises correctly
|
||||
room.addReceipt(mkReceipt(roomId, [
|
||||
mkRecord(events[0].getId(), "m.read", userB, ts),
|
||||
mkRecord(events[0].getId()!, "m.read", userB, ts),
|
||||
]));
|
||||
expect(room.getEventReadUpTo(userB)).toEqual(events[0].getId());
|
||||
|
||||
// 2>0, so it should move forward
|
||||
room.addReceipt(mkReceipt(roomId, [
|
||||
mkRecord(events[2].getId(), "m.read", userB, ts),
|
||||
mkRecord(events[2].getId()!, "m.read", userB, ts),
|
||||
]));
|
||||
expect(room.getEventReadUpTo(userB)).toEqual(events[2].getId());
|
||||
|
||||
// 1<2, so it should stay put
|
||||
room.addReceipt(mkReceipt(roomId, [
|
||||
mkRecord(events[1].getId(), "m.read", userB, ts),
|
||||
mkRecord(events[1].getId()!, "m.read", userB, ts),
|
||||
]));
|
||||
expect(room.getEventReadUpTo(userB)).toEqual(events[2].getId());
|
||||
});
|
||||
@@ -1399,13 +1399,13 @@ describe("Room", function() {
|
||||
|
||||
// check it initialises correctly
|
||||
room.addReceipt(mkReceipt(roomId, [
|
||||
mkRecord(events[0].getId(), "m.read", userB, ts),
|
||||
mkRecord(events[0].getId()!, "m.read", userB, ts),
|
||||
]));
|
||||
expect(room.getEventReadUpTo(userB)).toEqual(events[0].getId());
|
||||
|
||||
// 2>0, so it should move forward
|
||||
room.addReceipt(mkReceipt(roomId, [
|
||||
mkRecord(events[2].getId(), "m.read", userB, ts),
|
||||
mkRecord(events[2].getId()!, "m.read", userB, ts),
|
||||
]), true);
|
||||
expect(room.getEventReadUpTo(userB)).toEqual(events[2].getId());
|
||||
expect(room.getReceiptsForEvent(events[2])).toEqual([
|
||||
@@ -1414,7 +1414,7 @@ describe("Room", function() {
|
||||
|
||||
// 1<2, so it should stay put
|
||||
room.addReceipt(mkReceipt(roomId, [
|
||||
mkRecord(events[1].getId(), "m.read", userB, ts),
|
||||
mkRecord(events[1].getId()!, "m.read", userB, ts),
|
||||
]));
|
||||
expect(room.getEventReadUpTo(userB)).toEqual(events[2].getId());
|
||||
expect(room.getEventReadUpTo(userB, true)).toEqual(events[1].getId());
|
||||
@@ -1428,7 +1428,7 @@ describe("Room", function() {
|
||||
it("should return user IDs read up to the given event", function() {
|
||||
const ts = 13787898424;
|
||||
room.addReceipt(mkReceipt(roomId, [
|
||||
mkRecord(eventToAck.getId(), "m.read", userB, ts),
|
||||
mkRecord(eventToAck.getId()!, "m.read", userB, ts),
|
||||
]));
|
||||
expect(room.getUsersReadUpTo(eventToAck)).toEqual([userB]);
|
||||
});
|
||||
@@ -1438,9 +1438,9 @@ describe("Room", function() {
|
||||
it("should acknowledge if an event has been read", function() {
|
||||
const ts = 13787898424;
|
||||
room.addReceipt(mkReceipt(roomId, [
|
||||
mkRecord(eventToAck.getId(), "m.read", userB, ts),
|
||||
mkRecord(eventToAck.getId()!, "m.read", userB, ts),
|
||||
]));
|
||||
expect(room.hasUserReadEvent(userB, eventToAck.getId())).toEqual(true);
|
||||
expect(room.hasUserReadEvent(userB, eventToAck.getId()!)).toEqual(true);
|
||||
});
|
||||
it("return false for an unknown event", function() {
|
||||
expect(room.hasUserReadEvent(userB, "unknown_event")).toEqual(false);
|
||||
@@ -1556,7 +1556,7 @@ describe("Room", function() {
|
||||
user: userA,
|
||||
type: EventType.RoomRedaction,
|
||||
content: {},
|
||||
redacts: eventA.getId(),
|
||||
redacts: eventA.getId()!,
|
||||
event: true,
|
||||
});
|
||||
redactA.status = EventStatus.SENDING;
|
||||
@@ -1609,7 +1609,7 @@ describe("Room", function() {
|
||||
});
|
||||
|
||||
it("should remove cancelled events from the timeline", function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
const room = new Room(roomId, null!, userA);
|
||||
const eventA = utils.mkMessage({
|
||||
room: roomId, user: userA, event: true,
|
||||
});
|
||||
@@ -1643,7 +1643,7 @@ describe("Room", function() {
|
||||
});
|
||||
|
||||
describe("loadMembersIfNeeded", function() {
|
||||
function createClientMock(serverResponse, storageResponse = null) {
|
||||
function createClientMock(serverResponse, storageResponse: MatrixEvent[] | Error | null = null) {
|
||||
return {
|
||||
getEventMapper: function() {
|
||||
// events should already be MatrixEvents
|
||||
@@ -1664,7 +1664,7 @@ describe("Room", function() {
|
||||
}),
|
||||
store: {
|
||||
storageResponse,
|
||||
storedMembers: null,
|
||||
storedMembers: [] as IStateEventWithRoomId[] | null,
|
||||
getOutOfBandMembers: function() {
|
||||
if (this.storageResponse instanceof Error) {
|
||||
return Promise.reject(this.storageResponse);
|
||||
@@ -1693,11 +1693,11 @@ describe("Room", function() {
|
||||
|
||||
it("should load members from server on first call", async function() {
|
||||
const client = createClientMock([memberEvent]);
|
||||
const room = new Room(roomId, client as any, null, { lazyLoadMembers: true });
|
||||
const room = new Room(roomId, client as any, null!, { lazyLoadMembers: true });
|
||||
await room.loadMembersIfNeeded();
|
||||
const memberA = room.getMember("@user_a:bar");
|
||||
const memberA = room.getMember("@user_a:bar")!;
|
||||
expect(memberA.name).toEqual("User A");
|
||||
const storedMembers = client.store.storedMembers;
|
||||
const storedMembers = client.store.storedMembers!;
|
||||
expect(storedMembers.length).toEqual(1);
|
||||
expect(storedMembers[0].event_id).toEqual(memberEvent.getId());
|
||||
});
|
||||
@@ -1711,17 +1711,17 @@ describe("Room", function() {
|
||||
name: "Ms A",
|
||||
});
|
||||
const client = createClientMock([memberEvent2], [memberEvent]);
|
||||
const room = new Room(roomId, client as any, null, { lazyLoadMembers: true });
|
||||
const room = new Room(roomId, client as any, null!, { lazyLoadMembers: true });
|
||||
|
||||
await room.loadMembersIfNeeded();
|
||||
|
||||
const memberA = room.getMember("@user_a:bar");
|
||||
const memberA = room.getMember("@user_a:bar")!;
|
||||
expect(memberA.name).toEqual("User A");
|
||||
});
|
||||
|
||||
it("should allow retry on error", async function() {
|
||||
const client = createClientMock(new Error("server says no"));
|
||||
const room = new Room(roomId, client as any, null, { lazyLoadMembers: true });
|
||||
const room = new Room(roomId, client as any, null!, { lazyLoadMembers: true });
|
||||
let hasThrown = false;
|
||||
try {
|
||||
await room.loadMembersIfNeeded();
|
||||
@@ -1732,7 +1732,7 @@ describe("Room", function() {
|
||||
|
||||
client.members.mockReturnValue({ chunk: [memberEvent] });
|
||||
await room.loadMembersIfNeeded();
|
||||
const memberA = room.getMember("@user_a:bar");
|
||||
const memberA = room.getMember("@user_a:bar")!;
|
||||
expect(memberA.name).toEqual("User A");
|
||||
});
|
||||
});
|
||||
@@ -1740,27 +1740,68 @@ describe("Room", function() {
|
||||
describe("getMyMembership", function() {
|
||||
it("should return synced membership if membership isn't available yet",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
const room = new Room(roomId, null!, userA);
|
||||
room.updateMyMembership(JoinRule.Invite);
|
||||
expect(room.getMyMembership()).toEqual(JoinRule.Invite);
|
||||
});
|
||||
it("should emit a Room.myMembership event on a change",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
const events = [];
|
||||
room.on(RoomEvent.MyMembership, (_room, membership, oldMembership) => {
|
||||
events.push({ membership, oldMembership });
|
||||
});
|
||||
room.updateMyMembership(JoinRule.Invite);
|
||||
expect(room.getMyMembership()).toEqual(JoinRule.Invite);
|
||||
expect(events[0]).toEqual({ membership: "invite", oldMembership: undefined });
|
||||
events.splice(0); //clear
|
||||
room.updateMyMembership(JoinRule.Invite);
|
||||
expect(events.length).toEqual(0);
|
||||
room.updateMyMembership("join");
|
||||
expect(room.getMyMembership()).toEqual("join");
|
||||
expect(events[0]).toEqual({ membership: "join", oldMembership: "invite" });
|
||||
it("should emit a Room.myMembership event on a change", function() {
|
||||
const room = new Room(roomId, null!, userA);
|
||||
const events: {
|
||||
membership: string;
|
||||
oldMembership?: string;
|
||||
}[] = [];
|
||||
room.on(RoomEvent.MyMembership, (_room, membership, oldMembership) => {
|
||||
events.push({ membership, oldMembership });
|
||||
});
|
||||
room.updateMyMembership(JoinRule.Invite);
|
||||
expect(room.getMyMembership()).toEqual(JoinRule.Invite);
|
||||
expect(events[0]).toEqual({ membership: "invite", oldMembership: undefined });
|
||||
events.splice(0); //clear
|
||||
room.updateMyMembership(JoinRule.Invite);
|
||||
expect(events.length).toEqual(0);
|
||||
room.updateMyMembership("join");
|
||||
expect(room.getMyMembership()).toEqual("join");
|
||||
expect(events[0]).toEqual({ membership: "join", oldMembership: "invite" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDMInviter", () => {
|
||||
it("should delegate to RoomMember::getDMInviter if available", () => {
|
||||
const room = new Room(roomId, null!, userA);
|
||||
room.currentState.markOutOfBandMembersStarted();
|
||||
room.currentState.setOutOfBandMembers([
|
||||
new MatrixEvent({
|
||||
type: EventType.RoomMember,
|
||||
state_key: userA,
|
||||
sender: userB,
|
||||
content: {
|
||||
membership: "invite",
|
||||
is_direct: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(room.getDMInviter()).toBe(userB);
|
||||
});
|
||||
|
||||
it("should fall back to summary heroes and return the first one", () => {
|
||||
const room = new Room(roomId, null!, userA);
|
||||
room.updateMyMembership("invite");
|
||||
room.setSummary({
|
||||
"m.heroes": [userA, userC],
|
||||
"m.joined_member_count": 1,
|
||||
"m.invited_member_count": 1,
|
||||
});
|
||||
|
||||
expect(room.getDMInviter()).toBe(userC);
|
||||
});
|
||||
|
||||
it("should return undefined if we're not joined or invited to the room", () => {
|
||||
const room = new Room(roomId, null!, userA);
|
||||
expect(room.getDMInviter()).toBeUndefined();
|
||||
room.updateMyMembership("leave");
|
||||
expect(room.getDMInviter()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("guessDMUserId", function() {
|
||||
@@ -1789,6 +1830,36 @@ describe("Room", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAvatarFallbackMember", () => {
|
||||
it("should should return undefined if the room isn't a 1:1", () => {
|
||||
const room = new Room(roomId, null!, userA);
|
||||
room.currentState.setJoinedMemberCount(2);
|
||||
room.currentState.setInvitedMemberCount(1);
|
||||
expect(room.getAvatarFallbackMember()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should use summary heroes member if 1:1", () => {
|
||||
const room = new Room(roomId, null!, userA);
|
||||
room.currentState.markOutOfBandMembersStarted();
|
||||
room.currentState.setOutOfBandMembers([
|
||||
new MatrixEvent({
|
||||
type: EventType.RoomMember,
|
||||
state_key: userD,
|
||||
sender: userD,
|
||||
content: {
|
||||
membership: "join",
|
||||
},
|
||||
}),
|
||||
]);
|
||||
room.setSummary({
|
||||
"m.heroes": [userA, userD],
|
||||
"m.joined_member_count": 1,
|
||||
"m.invited_member_count": 1,
|
||||
});
|
||||
expect(room.getAvatarFallbackMember()?.userId).toBe(userD);
|
||||
});
|
||||
});
|
||||
|
||||
describe("maySendMessage", function() {
|
||||
it("should return false if synced membership not join", function() {
|
||||
const room = new Room(roomId, { isRoomEncrypted: () => false } as any, userA);
|
||||
@@ -2118,7 +2189,7 @@ describe("Room", function() {
|
||||
},
|
||||
});
|
||||
|
||||
expect(() => room.createThread(rootEvent.getId(), rootEvent, [])).not.toThrow();
|
||||
expect(() => room.createThread(rootEvent.getId()!, rootEvent, [])).not.toThrow();
|
||||
});
|
||||
|
||||
it("creating thread from edited event should not conflate old versions of the event", () => {
|
||||
@@ -2132,6 +2203,7 @@ describe("Room", function() {
|
||||
|
||||
it("Edits update the lastReply event", async () => {
|
||||
room.client.supportsExperimentalThreads = () => true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
|
||||
const randomMessage = mkMessage();
|
||||
const threadRoot = mkMessage();
|
||||
@@ -2145,7 +2217,7 @@ describe("Room", function() {
|
||||
unsigned: {
|
||||
"age": 123,
|
||||
"m.relations": {
|
||||
"m.thread": {
|
||||
[THREAD_RELATION_TYPE.name]: {
|
||||
latest_event: threadResponse.event,
|
||||
count: 2,
|
||||
current_user_participated: true,
|
||||
@@ -2157,11 +2229,29 @@ describe("Room", function() {
|
||||
let prom = emitPromise(room, ThreadEvent.New);
|
||||
room.addLiveEvents([randomMessage, threadRoot, threadResponse]);
|
||||
const thread = await prom;
|
||||
await emitPromise(room, ThreadEvent.Update);
|
||||
|
||||
expect(thread.replyToEvent).toBe(threadResponse);
|
||||
expect(thread.replyToEvent.event).toEqual(threadResponse.event);
|
||||
expect(thread.replyToEvent.getContent().body).toBe(threadResponse.getContent().body);
|
||||
|
||||
prom = emitPromise(thread, ThreadEvent.Update);
|
||||
room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({
|
||||
...threadRoot.event,
|
||||
unsigned: {
|
||||
"age": 123,
|
||||
"m.relations": {
|
||||
[THREAD_RELATION_TYPE.name]: {
|
||||
latest_event: {
|
||||
...threadResponse.event,
|
||||
content: threadResponseEdit.event.content,
|
||||
},
|
||||
count: 2,
|
||||
current_user_participated: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
prom = emitPromise(room, ThreadEvent.Update);
|
||||
room.addLiveEvents([threadResponseEdit]);
|
||||
await prom;
|
||||
expect(thread.replyToEvent.getContent().body).toBe(threadResponseEdit.getContent()["m.new_content"].body);
|
||||
@@ -2169,6 +2259,7 @@ describe("Room", function() {
|
||||
|
||||
it("Redactions to thread responses decrement the length", async () => {
|
||||
room.client.supportsExperimentalThreads = () => true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
|
||||
const threadRoot = mkMessage();
|
||||
const threadResponse1 = mkThreadResponse(threadRoot);
|
||||
@@ -2181,7 +2272,7 @@ describe("Room", function() {
|
||||
unsigned: {
|
||||
"age": 123,
|
||||
"m.relations": {
|
||||
"m.thread": {
|
||||
[THREAD_RELATION_TYPE.name]: {
|
||||
latest_event: threadResponse2.event,
|
||||
count: 2,
|
||||
current_user_participated: true,
|
||||
@@ -2193,10 +2284,36 @@ describe("Room", function() {
|
||||
let prom = emitPromise(room, ThreadEvent.New);
|
||||
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]);
|
||||
const thread = await prom;
|
||||
await emitPromise(room, ThreadEvent.Update);
|
||||
|
||||
expect(thread).toHaveLength(2);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
||||
|
||||
thread.timelineSet.addEventToTimeline(
|
||||
threadResponse1,
|
||||
thread.liveTimeline,
|
||||
{ toStartOfTimeline: true, fromCache: false, roomState: thread.roomState },
|
||||
);
|
||||
thread.timelineSet.addEventToTimeline(
|
||||
threadResponse2,
|
||||
thread.liveTimeline,
|
||||
{ toStartOfTimeline: true, fromCache: false, roomState: thread.roomState },
|
||||
);
|
||||
|
||||
room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({
|
||||
...threadRoot.event,
|
||||
unsigned: {
|
||||
"age": 123,
|
||||
"m.relations": {
|
||||
[THREAD_RELATION_TYPE.name]: {
|
||||
latest_event: threadResponse2.event,
|
||||
count: 1,
|
||||
current_user_participated: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
prom = emitPromise(thread, ThreadEvent.Update);
|
||||
const threadResponse1Redaction = mkRedaction(threadResponse1);
|
||||
room.addLiveEvents([threadResponse1Redaction]);
|
||||
@@ -2207,6 +2324,7 @@ describe("Room", function() {
|
||||
|
||||
it("Redactions to reactions in threads do not decrement the length", async () => {
|
||||
room.client.supportsExperimentalThreads = () => true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
|
||||
const threadRoot = mkMessage();
|
||||
const threadResponse1 = mkThreadResponse(threadRoot);
|
||||
@@ -2220,7 +2338,7 @@ describe("Room", function() {
|
||||
unsigned: {
|
||||
"age": 123,
|
||||
"m.relations": {
|
||||
"m.thread": {
|
||||
[THREAD_RELATION_TYPE.name]: {
|
||||
latest_event: threadResponse2.event,
|
||||
count: 2,
|
||||
current_user_participated: true,
|
||||
@@ -2232,6 +2350,7 @@ describe("Room", function() {
|
||||
const prom = emitPromise(room, ThreadEvent.New);
|
||||
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]);
|
||||
const thread = await prom;
|
||||
await emitPromise(room, ThreadEvent.Update);
|
||||
|
||||
expect(thread).toHaveLength(2);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
||||
@@ -2244,6 +2363,7 @@ describe("Room", function() {
|
||||
|
||||
it("should not decrement the length when the thread root is redacted", async () => {
|
||||
room.client.supportsExperimentalThreads = () => true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
|
||||
const threadRoot = mkMessage();
|
||||
const threadResponse1 = mkThreadResponse(threadRoot);
|
||||
@@ -2257,7 +2377,7 @@ describe("Room", function() {
|
||||
unsigned: {
|
||||
"age": 123,
|
||||
"m.relations": {
|
||||
"m.thread": {
|
||||
[THREAD_RELATION_TYPE.name]: {
|
||||
latest_event: threadResponse2.event,
|
||||
count: 2,
|
||||
current_user_participated: true,
|
||||
@@ -2269,6 +2389,7 @@ describe("Room", function() {
|
||||
let prom = emitPromise(room, ThreadEvent.New);
|
||||
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]);
|
||||
const thread = await prom;
|
||||
await emitPromise(room, ThreadEvent.Update);
|
||||
|
||||
expect(thread).toHaveLength(2);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
||||
@@ -2282,6 +2403,18 @@ describe("Room", function() {
|
||||
|
||||
it("Redacting the lastEvent finds a new lastEvent", async () => {
|
||||
room.client.supportsExperimentalThreads = () => true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
|
||||
room.client.createThreadListMessagesRequest = () => Promise.resolve({
|
||||
start: null,
|
||||
end: null,
|
||||
chunk: [],
|
||||
state: [],
|
||||
});
|
||||
|
||||
await room.createThreadsTimelineSets();
|
||||
await room.fetchRoomThreads();
|
||||
|
||||
const threadRoot = mkMessage();
|
||||
const threadResponse1 = mkThreadResponse(threadRoot);
|
||||
@@ -2306,21 +2439,53 @@ describe("Room", function() {
|
||||
let prom = emitPromise(room, ThreadEvent.New);
|
||||
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]);
|
||||
const thread = await prom;
|
||||
await emitPromise(room, ThreadEvent.Update);
|
||||
|
||||
expect(thread).toHaveLength(2);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
||||
|
||||
room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({
|
||||
...threadRoot.event,
|
||||
unsigned: {
|
||||
"age": 123,
|
||||
"m.relations": {
|
||||
"m.thread": {
|
||||
latest_event: threadResponse1.event,
|
||||
count: 1,
|
||||
current_user_participated: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
prom = emitPromise(room, ThreadEvent.Update);
|
||||
const threadResponse2Redaction = mkRedaction(threadResponse2);
|
||||
room.addLiveEvents([threadResponse2Redaction]);
|
||||
await prom;
|
||||
await emitPromise(room, ThreadEvent.Update);
|
||||
expect(thread).toHaveLength(1);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadResponse1.getId());
|
||||
|
||||
prom = emitPromise(room, ThreadEvent.Update);
|
||||
room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({
|
||||
...threadRoot.event,
|
||||
unsigned: {
|
||||
"age": 123,
|
||||
"m.relations": {
|
||||
"m.thread": {
|
||||
latest_event: threadRoot.event,
|
||||
count: 0,
|
||||
current_user_participated: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
prom = emitPromise(room, ThreadEvent.Delete);
|
||||
const prom2 = emitPromise(room, RoomEvent.Timeline);
|
||||
const threadResponse1Redaction = mkRedaction(threadResponse1);
|
||||
room.addLiveEvents([threadResponse1Redaction]);
|
||||
await prom;
|
||||
await prom2;
|
||||
expect(thread).toHaveLength(0);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadRoot.getId());
|
||||
});
|
||||
@@ -2329,6 +2494,7 @@ describe("Room", function() {
|
||||
describe("eventShouldLiveIn", () => {
|
||||
const client = new TestClient(userA).client;
|
||||
client.supportsExperimentalThreads = () => true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
const room = new Room(roomId, client, userA);
|
||||
|
||||
it("thread root and its relations&redactions should be in both", () => {
|
||||
@@ -2339,7 +2505,7 @@ describe("Room", function() {
|
||||
const threadReaction2 = mkReaction(threadRoot);
|
||||
const threadReaction2Redaction = mkRedaction(threadReaction2);
|
||||
|
||||
const roots = new Set([threadRoot.getId()]);
|
||||
const roots = new Set([threadRoot.getId()!]);
|
||||
const events = [
|
||||
randomMessage,
|
||||
threadRoot,
|
||||
@@ -2377,7 +2543,7 @@ describe("Room", function() {
|
||||
const threadReaction2 = mkReaction(threadResponse1);
|
||||
const threadReaction2Redaction = mkRedaction(threadReaction2);
|
||||
|
||||
const roots = new Set([threadRoot.getId()]);
|
||||
const roots = new Set([threadRoot.getId()!]);
|
||||
const events = [threadRoot, threadResponse1, threadReaction1, threadReaction2, threadReaction2Redaction];
|
||||
|
||||
expect(room.eventShouldLiveIn(threadReaction1, events, roots).shouldLiveInRoom).toBeFalsy();
|
||||
@@ -2399,7 +2565,7 @@ describe("Room", function() {
|
||||
const reaction2 = mkReaction(reply1);
|
||||
const reaction2Redaction = mkRedaction(reply1);
|
||||
|
||||
const roots = new Set([threadRoot.getId()]);
|
||||
const roots = new Set([threadRoot.getId()!]);
|
||||
const events = [
|
||||
threadRoot,
|
||||
threadResponse1,
|
||||
@@ -2425,7 +2591,7 @@ describe("Room", function() {
|
||||
const reply1 = mkReply(threadRoot);
|
||||
const reply2 = mkReply(reply1);
|
||||
|
||||
const roots = new Set([threadRoot.getId()]);
|
||||
const roots = new Set([threadRoot.getId()!]);
|
||||
const events = [
|
||||
threadRoot,
|
||||
threadResponse1,
|
||||
@@ -2455,28 +2621,28 @@ describe("Room", function() {
|
||||
|
||||
room.addLiveEvents(events);
|
||||
|
||||
const thread = threadRoot.getThread();
|
||||
const thread = threadRoot.getThread()!;
|
||||
expect(thread.rootEvent).toBe(threadRoot);
|
||||
|
||||
const rootRelations = thread.timelineSet.relations.getChildEventsForEvent(
|
||||
threadRoot.getId(),
|
||||
threadRoot.getId()!,
|
||||
RelationType.Annotation,
|
||||
EventType.Reaction,
|
||||
).getSortedAnnotationsByKey();
|
||||
)!.getSortedAnnotationsByKey();
|
||||
expect(rootRelations).toHaveLength(1);
|
||||
expect(rootRelations[0][0]).toEqual(rootReaction.getRelation().key);
|
||||
expect(rootRelations[0][1].size).toEqual(1);
|
||||
expect(rootRelations[0][1].has(rootReaction)).toBeTruthy();
|
||||
expect(rootRelations![0][0]).toEqual(rootReaction.getRelation()!.key);
|
||||
expect(rootRelations![0][1].size).toEqual(1);
|
||||
expect(rootRelations![0][1].has(rootReaction)).toBeTruthy();
|
||||
|
||||
const responseRelations = thread.timelineSet.relations.getChildEventsForEvent(
|
||||
threadResponse.getId(),
|
||||
threadResponse.getId()!,
|
||||
RelationType.Annotation,
|
||||
EventType.Reaction,
|
||||
).getSortedAnnotationsByKey();
|
||||
)!.getSortedAnnotationsByKey();
|
||||
expect(responseRelations).toHaveLength(1);
|
||||
expect(responseRelations[0][0]).toEqual(threadReaction.getRelation().key);
|
||||
expect(responseRelations[0][1].size).toEqual(1);
|
||||
expect(responseRelations[0][1].has(threadReaction)).toBeTruthy();
|
||||
expect(responseRelations![0][0]).toEqual(threadReaction.getRelation()!.key);
|
||||
expect(responseRelations![0][1].size).toEqual(1);
|
||||
expect(responseRelations![0][1].has(threadReaction)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2642,6 +2808,29 @@ describe("Room", function() {
|
||||
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 333);
|
||||
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Highlight);
|
||||
});
|
||||
|
||||
it("partially resets room notifications", () => {
|
||||
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 666);
|
||||
room.setThreadUnreadNotificationCount("456", NotificationCountType.Highlight, 123);
|
||||
|
||||
room.resetThreadUnreadNotificationCount(["123"]);
|
||||
|
||||
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBe(666);
|
||||
expect(room.getThreadUnreadNotificationCount("456", NotificationCountType.Highlight)).toBe(0);
|
||||
});
|
||||
|
||||
it("emits event on notifications reset", () => {
|
||||
const cb = jest.fn();
|
||||
|
||||
room.on(RoomEvent.UnreadNotifications, cb);
|
||||
|
||||
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 666);
|
||||
room.setThreadUnreadNotificationCount("456", NotificationCountType.Highlight, 123);
|
||||
|
||||
room.resetThreadUnreadNotificationCount();
|
||||
|
||||
expect(cb).toHaveBeenLastCalledWith();
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasThreadUnreadNotification", () => {
|
||||
@@ -2734,7 +2923,7 @@ describe("Room", function() {
|
||||
expect(pendingEvents[1].isBeingDecrypted()).toBeFalsy();
|
||||
expect(pendingEvents[1].isEncrypted()).toBeTruthy();
|
||||
for (const ev of pendingEvents) {
|
||||
expect(room.getPendingEvent(ev.getId())).toBe(ev);
|
||||
expect(room.getPendingEvent(ev.getId()!)).toBe(ev);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -160,10 +160,10 @@ describe("MatrixScheduler", function() {
|
||||
const eventD = utils.mkMessage({ user: "@b:bar", room: roomId, event: true });
|
||||
|
||||
const buckets = {};
|
||||
buckets[eventA.getId()] = "queue_A";
|
||||
buckets[eventD.getId()] = "queue_A";
|
||||
buckets[eventB.getId()] = "queue_B";
|
||||
buckets[eventC.getId()] = "queue_B";
|
||||
buckets[eventA.getId()!] = "queue_A";
|
||||
buckets[eventD.getId()!] = "queue_A";
|
||||
buckets[eventB.getId()!] = "queue_B";
|
||||
buckets[eventC.getId()!] = "queue_B";
|
||||
|
||||
retryFn = function() {
|
||||
return 0;
|
||||
|
||||
@@ -37,7 +37,7 @@ const mockClient = {
|
||||
function createTimeline(numEvents = 3, baseIndex = 1): EventTimeline {
|
||||
const room = new Room(ROOM_ID, mockClient, USER_ID);
|
||||
const timelineSet = new EventTimelineSet(room);
|
||||
jest.spyOn(timelineSet.room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet);
|
||||
jest.spyOn(room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet);
|
||||
|
||||
const timeline = new EventTimeline(timelineSet);
|
||||
|
||||
@@ -170,7 +170,7 @@ describe("TimelineWindow", function() {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockClient.getEventTimeline.mockResolvedValue(undefined);
|
||||
mockClient.paginateEventTimeline.mockReturnValue(undefined);
|
||||
mockClient.paginateEventTimeline.mockResolvedValue(false);
|
||||
});
|
||||
|
||||
describe("load", function() {
|
||||
|
||||
+1040
-339
File diff suppressed because it is too large
Load Diff
@@ -20,22 +20,117 @@ import {
|
||||
EventTimeline,
|
||||
EventTimelineSet,
|
||||
EventType,
|
||||
GroupCallIntent,
|
||||
GroupCallType,
|
||||
IRoomTimelineData,
|
||||
MatrixCall,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
RoomEvent,
|
||||
RoomMember,
|
||||
} from "../../../src";
|
||||
import { MatrixClient } from "../../../src/client";
|
||||
import { CallEventHandler, CallEventHandlerEvent } from "../../../src/webrtc/callEventHandler";
|
||||
import { GroupCallEventHandler } from "../../../src/webrtc/groupCallEventHandler";
|
||||
import { SyncState } from "../../../src/sync";
|
||||
import { installWebRTCMocks, MockRTCPeerConnection } from "../../test-utils/webrtc";
|
||||
import { sleep } from "../../../src/utils";
|
||||
|
||||
describe("callEventHandler", () => {
|
||||
it("should ignore a call if invite & hangup come within a single sync", () => {
|
||||
const testClient = new TestClient();
|
||||
const client = testClient.client;
|
||||
const room = new Room("!room:id", client, "@user:id");
|
||||
const timelineData: IRoomTimelineData = { timeline: new EventTimeline(new EventTimelineSet(room, {})) };
|
||||
describe("CallEventHandler", () => {
|
||||
let client: MatrixClient;
|
||||
beforeEach(() => {
|
||||
installWebRTCMocks();
|
||||
|
||||
client = new TestClient("@alice:foo", "somedevice", "token", undefined, {}).client;
|
||||
client.callEventHandler = new CallEventHandler(client);
|
||||
client.callEventHandler.start();
|
||||
client.groupCallEventHandler = new GroupCallEventHandler(client);
|
||||
client.groupCallEventHandler.start();
|
||||
client.sendStateEvent = jest.fn().mockResolvedValue({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
client.callEventHandler!.stop();
|
||||
client.groupCallEventHandler!.stop();
|
||||
});
|
||||
|
||||
const sync = async () => {
|
||||
client.getSyncState = jest.fn().mockReturnValue(SyncState.Syncing);
|
||||
client.emit(ClientEvent.Sync, SyncState.Syncing, SyncState.Prepared);
|
||||
|
||||
// We can't await the event processing
|
||||
await sleep(10);
|
||||
};
|
||||
|
||||
it("should enforce inbound toDevice message ordering", async () => {
|
||||
const callEventHandler = client.callEventHandler!;
|
||||
const event1 = new MatrixEvent({
|
||||
type: EventType.CallInvite,
|
||||
content: {
|
||||
call_id: "123",
|
||||
seq: 0,
|
||||
},
|
||||
});
|
||||
callEventHandler["onToDeviceEvent"](event1);
|
||||
|
||||
expect(callEventHandler.callEventBuffer.length).toBe(1);
|
||||
expect(callEventHandler.callEventBuffer[0]).toBe(event1);
|
||||
|
||||
const event2 = new MatrixEvent({
|
||||
type: EventType.CallCandidates,
|
||||
content: {
|
||||
call_id: "123",
|
||||
seq: 1,
|
||||
},
|
||||
});
|
||||
callEventHandler["onToDeviceEvent"](event2);
|
||||
|
||||
expect(callEventHandler.callEventBuffer.length).toBe(2);
|
||||
expect(callEventHandler.callEventBuffer[1]).toBe(event2);
|
||||
|
||||
const event3 = new MatrixEvent({
|
||||
type: EventType.CallCandidates,
|
||||
content: {
|
||||
call_id: "123",
|
||||
seq: 3,
|
||||
},
|
||||
});
|
||||
callEventHandler["onToDeviceEvent"](event3);
|
||||
|
||||
expect(callEventHandler.callEventBuffer.length).toBe(2);
|
||||
expect(callEventHandler.nextSeqByCall.get("123")).toBe(2);
|
||||
expect(callEventHandler.toDeviceEventBuffers.get("123")?.length).toBe(1);
|
||||
|
||||
const event4 = new MatrixEvent({
|
||||
type: EventType.CallCandidates,
|
||||
content: {
|
||||
call_id: "123",
|
||||
seq: 4,
|
||||
},
|
||||
});
|
||||
callEventHandler["onToDeviceEvent"](event4);
|
||||
|
||||
expect(callEventHandler.callEventBuffer.length).toBe(2);
|
||||
expect(callEventHandler.nextSeqByCall.get("123")).toBe(2);
|
||||
expect(callEventHandler.toDeviceEventBuffers.get("123")?.length).toBe(2);
|
||||
|
||||
const event5 = new MatrixEvent({
|
||||
type: EventType.CallCandidates,
|
||||
content: {
|
||||
call_id: "123",
|
||||
seq: 2,
|
||||
},
|
||||
});
|
||||
callEventHandler["onToDeviceEvent"](event5);
|
||||
|
||||
expect(callEventHandler.callEventBuffer.length).toBe(5);
|
||||
expect(callEventHandler.nextSeqByCall.get("123")).toBe(5);
|
||||
expect(callEventHandler.toDeviceEventBuffers.get("123")?.length).toBe(0);
|
||||
});
|
||||
|
||||
it("should ignore a call if invite & hangup come within a single sync", () => {
|
||||
const room = new Room("!room:id", client, "@user:id");
|
||||
const timelineData: IRoomTimelineData = { timeline: new EventTimeline(new EventTimelineSet(room, {})) };
|
||||
|
||||
// Fire off call invite then hangup within a single sync
|
||||
const callInvite = new MatrixEvent({
|
||||
@@ -58,8 +153,121 @@ describe("callEventHandler", () => {
|
||||
client.on(CallEventHandlerEvent.Incoming, incomingCallEmitted);
|
||||
|
||||
client.getSyncState = jest.fn().mockReturnValue(SyncState.Syncing);
|
||||
client.emit(ClientEvent.Sync, SyncState.Syncing);
|
||||
client.emit(ClientEvent.Sync, SyncState.Syncing, null);
|
||||
|
||||
expect(incomingCallEmitted).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should ignore non-call events", async () => {
|
||||
// @ts-ignore Mock handleCallEvent is private
|
||||
jest.spyOn(client.callEventHandler, "handleCallEvent");
|
||||
jest.spyOn(client, "checkTurnServers").mockReturnValue(Promise.resolve(true));
|
||||
|
||||
const room = new Room("!room:id", client, "@user:id");
|
||||
const timelineData: IRoomTimelineData = { timeline: new EventTimeline(new EventTimelineSet(room, {})) };
|
||||
|
||||
client.emit(RoomEvent.Timeline, new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
room_id: "!room:id",
|
||||
content: {
|
||||
text: "hello",
|
||||
|
||||
},
|
||||
}), room, false, false, timelineData);
|
||||
await sync();
|
||||
|
||||
// @ts-ignore Mock handleCallEvent is private
|
||||
expect(client.callEventHandler.handleCallEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("handleCallEvent()", () => {
|
||||
const incomingCallListener = jest.fn();
|
||||
let timelineData: IRoomTimelineData;
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
room = new Room("!room:id", client, client.getUserId()!);
|
||||
timelineData = { timeline: new EventTimeline(new EventTimelineSet(room, {})) };
|
||||
|
||||
jest.spyOn(client, "checkTurnServers").mockReturnValue(Promise.resolve(true));
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
jest.spyOn(room, "getMember").mockReturnValue({ user_id: client.getUserId() } as unknown as RoomMember);
|
||||
|
||||
client.on(CallEventHandlerEvent.Incoming, incomingCallListener);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
MockRTCPeerConnection.resetInstances();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should create a call when receiving an invite", async () => {
|
||||
client.emit(RoomEvent.Timeline, new MatrixEvent({
|
||||
type: EventType.CallInvite,
|
||||
room_id: "!room:id",
|
||||
content: {
|
||||
call_id: "123",
|
||||
},
|
||||
}), room, false, false, timelineData);
|
||||
await sync();
|
||||
|
||||
expect(incomingCallListener).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle group call event", async () => {
|
||||
let call: MatrixCall;
|
||||
const groupCall = await client.createGroupCall(
|
||||
room.roomId,
|
||||
GroupCallType.Voice,
|
||||
false,
|
||||
GroupCallIntent.Ring,
|
||||
);
|
||||
const SESSION_ID = "sender_session_id";
|
||||
const GROUP_CALL_ID = "group_call_id";
|
||||
const DEVICE_ID = "device_id";
|
||||
|
||||
incomingCallListener.mockImplementation((c) => call = c);
|
||||
jest.spyOn(client.groupCallEventHandler!, "getGroupCallById").mockReturnValue(groupCall);
|
||||
// @ts-ignore Mock onIncomingCall is private
|
||||
jest.spyOn(groupCall, "onIncomingCall");
|
||||
|
||||
await groupCall.enter();
|
||||
client.emit(RoomEvent.Timeline, new MatrixEvent({
|
||||
type: EventType.CallInvite,
|
||||
room_id: "!room:id",
|
||||
content: {
|
||||
call_id: "123",
|
||||
conf_id: GROUP_CALL_ID,
|
||||
device_id: DEVICE_ID,
|
||||
sender_session_id: SESSION_ID,
|
||||
dest_session_id: client.getSessionId(),
|
||||
},
|
||||
}), room, false, false, timelineData);
|
||||
await sync();
|
||||
|
||||
expect(incomingCallListener).toHaveBeenCalled();
|
||||
expect(call!.groupCallId).toBe(GROUP_CALL_ID);
|
||||
// @ts-ignore Mock opponentDeviceId is private
|
||||
expect(call.opponentDeviceId).toBe(DEVICE_ID);
|
||||
expect(call!.getOpponentSessionId()).toBe(SESSION_ID);
|
||||
// @ts-ignore Mock onIncomingCall is private
|
||||
expect(groupCall.onIncomingCall).toHaveBeenCalledWith(call);
|
||||
|
||||
groupCall.terminate(false);
|
||||
});
|
||||
|
||||
it("ignores a call with a different invitee than us", async () => {
|
||||
client.emit(RoomEvent.Timeline, new MatrixEvent({
|
||||
type: EventType.CallInvite,
|
||||
room_id: "!room:id",
|
||||
content: {
|
||||
call_id: "123",
|
||||
invitee: "@bob:bar",
|
||||
},
|
||||
}), room, false, false, timelineData);
|
||||
await sync();
|
||||
|
||||
expect(incomingCallListener).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,316 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import {
|
||||
ClientEvent,
|
||||
GroupCall,
|
||||
GroupCallIntent,
|
||||
GroupCallState,
|
||||
GroupCallType,
|
||||
IContent,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
RoomState,
|
||||
} from "../../../src";
|
||||
import { SyncState } from "../../../src/sync";
|
||||
import { GroupCallTerminationReason } from "../../../src/webrtc/groupCall";
|
||||
import { GroupCallEventHandler, GroupCallEventHandlerEvent } from "../../../src/webrtc/groupCallEventHandler";
|
||||
import { flushPromises } from "../../test-utils/flushPromises";
|
||||
import {
|
||||
makeMockGroupCallMemberStateEvent,
|
||||
makeMockGroupCallStateEvent,
|
||||
MockCallMatrixClient,
|
||||
} from "../../test-utils/webrtc";
|
||||
|
||||
const FAKE_USER_ID = "@alice:test.dummy";
|
||||
const FAKE_DEVICE_ID = "AAAAAAA";
|
||||
const FAKE_SESSION_ID = "session1";
|
||||
const FAKE_ROOM_ID = "!roomid:test.dummy";
|
||||
const FAKE_GROUP_CALL_ID = "fakegroupcallid";
|
||||
|
||||
describe('Group Call Event Handler', function() {
|
||||
let groupCallEventHandler: GroupCallEventHandler;
|
||||
let mockClient: MockCallMatrixClient;
|
||||
let mockRoom: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = new MockCallMatrixClient(
|
||||
FAKE_USER_ID, FAKE_DEVICE_ID, FAKE_SESSION_ID,
|
||||
);
|
||||
groupCallEventHandler = new GroupCallEventHandler(mockClient.typed());
|
||||
|
||||
mockRoom = {
|
||||
roomId: FAKE_ROOM_ID,
|
||||
currentState: {
|
||||
getStateEvents: jest.fn().mockReturnValue([makeMockGroupCallStateEvent(
|
||||
FAKE_ROOM_ID, FAKE_GROUP_CALL_ID,
|
||||
)]),
|
||||
},
|
||||
} as unknown as Room;
|
||||
|
||||
(mockClient as any).getRoom = jest.fn().mockReturnValue(mockRoom);
|
||||
});
|
||||
|
||||
describe("reacts to state changes", () => {
|
||||
it("terminates call", async () => {
|
||||
await groupCallEventHandler.start();
|
||||
mockClient.emitRoomState(
|
||||
makeMockGroupCallStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID),
|
||||
{ roomId: FAKE_ROOM_ID } as unknown as RoomState,
|
||||
);
|
||||
|
||||
const groupCall = groupCallEventHandler.groupCalls.get(FAKE_ROOM_ID)!;
|
||||
|
||||
expect(groupCall.state).toBe(GroupCallState.LocalCallFeedUninitialized);
|
||||
|
||||
mockClient.emitRoomState(
|
||||
makeMockGroupCallStateEvent(
|
||||
FAKE_ROOM_ID, FAKE_GROUP_CALL_ID, {
|
||||
"m.type": GroupCallType.Video,
|
||||
"m.intent": GroupCallIntent.Prompt,
|
||||
"m.terminated": GroupCallTerminationReason.CallEnded,
|
||||
},
|
||||
),
|
||||
{
|
||||
roomId: FAKE_ROOM_ID,
|
||||
} as unknown as RoomState,
|
||||
);
|
||||
|
||||
expect(groupCall.state).toBe(GroupCallState.Ended);
|
||||
});
|
||||
});
|
||||
|
||||
it("waits until client starts syncing", async () => {
|
||||
mockClient.getSyncState.mockReturnValue(null);
|
||||
let isStarted = false;
|
||||
(async () => {
|
||||
await groupCallEventHandler.start();
|
||||
isStarted = true;
|
||||
})();
|
||||
|
||||
const setSyncState = async (newState: SyncState) => {
|
||||
const oldState = mockClient.getSyncState();
|
||||
mockClient.getSyncState.mockReturnValue(newState);
|
||||
mockClient.emit(ClientEvent.Sync, newState, oldState, undefined);
|
||||
await flushPromises();
|
||||
};
|
||||
|
||||
await flushPromises();
|
||||
expect(isStarted).toEqual(false);
|
||||
|
||||
await setSyncState(SyncState.Prepared);
|
||||
expect(isStarted).toEqual(false);
|
||||
|
||||
await setSyncState(SyncState.Syncing);
|
||||
expect(isStarted).toEqual(true);
|
||||
});
|
||||
|
||||
it("finds existing group calls when started", async () => {
|
||||
const mockClientEmit = mockClient.emit = jest.fn();
|
||||
|
||||
mockClient.getRooms.mockReturnValue([mockRoom]);
|
||||
await groupCallEventHandler.start();
|
||||
|
||||
expect(mockClientEmit).toHaveBeenCalledWith(
|
||||
GroupCallEventHandlerEvent.Incoming,
|
||||
expect.objectContaining({
|
||||
groupCallId: FAKE_GROUP_CALL_ID,
|
||||
}),
|
||||
);
|
||||
|
||||
groupCallEventHandler.stop();
|
||||
});
|
||||
|
||||
it("can wait until a room is ready for group calls", async () => {
|
||||
await groupCallEventHandler.start();
|
||||
|
||||
const prom = groupCallEventHandler.waitUntilRoomReadyForGroupCalls(FAKE_ROOM_ID);
|
||||
let resolved = false;
|
||||
|
||||
(async () => {
|
||||
await prom;
|
||||
resolved = true;
|
||||
})();
|
||||
|
||||
expect(resolved).toEqual(false);
|
||||
mockClient.emit(ClientEvent.Room, mockRoom);
|
||||
|
||||
await prom;
|
||||
expect(resolved).toEqual(true);
|
||||
|
||||
groupCallEventHandler.stop();
|
||||
});
|
||||
|
||||
it("fires events for incoming calls", async () => {
|
||||
const onIncomingGroupCall = jest.fn();
|
||||
mockClient.on(GroupCallEventHandlerEvent.Incoming, onIncomingGroupCall);
|
||||
await groupCallEventHandler.start();
|
||||
|
||||
mockClient.emitRoomState(
|
||||
makeMockGroupCallStateEvent(
|
||||
FAKE_ROOM_ID, FAKE_GROUP_CALL_ID,
|
||||
),
|
||||
{
|
||||
roomId: FAKE_ROOM_ID,
|
||||
} as unknown as RoomState,
|
||||
);
|
||||
|
||||
expect(onIncomingGroupCall).toHaveBeenCalledWith(expect.objectContaining({
|
||||
groupCallId: FAKE_GROUP_CALL_ID,
|
||||
}));
|
||||
|
||||
mockClient.off(GroupCallEventHandlerEvent.Incoming, onIncomingGroupCall);
|
||||
});
|
||||
|
||||
it("handles data channel", async () => {
|
||||
await groupCallEventHandler.start();
|
||||
|
||||
const dataChannelOptions = {
|
||||
"maxPacketLifeTime": "life_time",
|
||||
"maxRetransmits": "retransmits",
|
||||
"ordered": "ordered",
|
||||
"protocol": "protocol",
|
||||
};
|
||||
|
||||
mockClient.emitRoomState(
|
||||
makeMockGroupCallStateEvent(
|
||||
FAKE_ROOM_ID,
|
||||
FAKE_GROUP_CALL_ID,
|
||||
{
|
||||
"m.type": GroupCallType.Video,
|
||||
"m.intent": GroupCallIntent.Prompt,
|
||||
"dataChannelsEnabled": true,
|
||||
dataChannelOptions,
|
||||
},
|
||||
),
|
||||
{
|
||||
roomId: FAKE_ROOM_ID,
|
||||
} as unknown as RoomState,
|
||||
);
|
||||
|
||||
// @ts-ignore Mock dataChannelsEnabled is private
|
||||
expect(groupCallEventHandler.groupCalls.get(FAKE_ROOM_ID)?.dataChannelsEnabled).toBe(true);
|
||||
// @ts-ignore Mock dataChannelOptions is private
|
||||
expect(groupCallEventHandler.groupCalls.get(FAKE_ROOM_ID)?.dataChannelOptions).toStrictEqual(
|
||||
dataChannelOptions,
|
||||
);
|
||||
});
|
||||
|
||||
it("sends member events to group calls", async () => {
|
||||
await groupCallEventHandler.start();
|
||||
|
||||
const mockGroupCall = {
|
||||
onMemberStateChanged: jest.fn(),
|
||||
};
|
||||
|
||||
groupCallEventHandler.groupCalls.set(FAKE_ROOM_ID, mockGroupCall as unknown as GroupCall);
|
||||
|
||||
const mockStateEvent = makeMockGroupCallMemberStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID);
|
||||
|
||||
mockClient.emitRoomState(
|
||||
mockStateEvent,
|
||||
{
|
||||
roomId: FAKE_ROOM_ID,
|
||||
} as unknown as RoomState,
|
||||
);
|
||||
|
||||
expect(mockGroupCall.onMemberStateChanged).toHaveBeenCalledWith(mockStateEvent);
|
||||
});
|
||||
|
||||
describe("ignoring invalid group call state events", () => {
|
||||
let mockClientEmit: jest.Func;
|
||||
|
||||
beforeEach(() => {
|
||||
mockClientEmit = mockClient.emit = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
groupCallEventHandler.stop();
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const setupCallAndStart = async (content?: IContent) => {
|
||||
mocked(mockRoom.currentState.getStateEvents).mockReturnValue([
|
||||
makeMockGroupCallStateEvent(
|
||||
FAKE_ROOM_ID,
|
||||
FAKE_GROUP_CALL_ID,
|
||||
content,
|
||||
),
|
||||
] as unknown as MatrixEvent);
|
||||
mockClient.getRooms.mockReturnValue([mockRoom]);
|
||||
await groupCallEventHandler.start();
|
||||
};
|
||||
|
||||
it("ignores terminated calls", async () => {
|
||||
await setupCallAndStart({
|
||||
"m.type": GroupCallType.Video,
|
||||
"m.intent": GroupCallIntent.Prompt,
|
||||
"m.terminated": GroupCallTerminationReason.CallEnded,
|
||||
});
|
||||
|
||||
expect(mockClientEmit).not.toHaveBeenCalledWith(
|
||||
GroupCallEventHandlerEvent.Incoming,
|
||||
expect.objectContaining({
|
||||
groupCallId: FAKE_GROUP_CALL_ID,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("ignores calls with invalid type", async () => {
|
||||
await setupCallAndStart({
|
||||
"m.type": "fake_type",
|
||||
"m.intent": GroupCallIntent.Prompt,
|
||||
});
|
||||
|
||||
expect(mockClientEmit).not.toHaveBeenCalledWith(
|
||||
GroupCallEventHandlerEvent.Incoming,
|
||||
expect.objectContaining({
|
||||
groupCallId: FAKE_GROUP_CALL_ID,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("ignores calls with invalid intent", async () => {
|
||||
await setupCallAndStart({
|
||||
"m.type": GroupCallType.Video,
|
||||
"m.intent": "fake_intent",
|
||||
});
|
||||
|
||||
expect(mockClientEmit).not.toHaveBeenCalledWith(
|
||||
GroupCallEventHandlerEvent.Incoming,
|
||||
expect.objectContaining({
|
||||
groupCallId: FAKE_GROUP_CALL_ID,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("ignores calls without a room", async () => {
|
||||
mockClient.getRoom.mockReturnValue(undefined);
|
||||
|
||||
await setupCallAndStart();
|
||||
|
||||
expect(mockClientEmit).not.toHaveBeenCalledWith(
|
||||
GroupCallEventHandlerEvent.Incoming,
|
||||
expect.objectContaining({
|
||||
groupCallId: FAKE_GROUP_CALL_ID,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,462 @@
|
||||
/*
|
||||
Copyright 2022 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 { GroupCall, MatrixCall, MatrixClient } from "../../../src";
|
||||
import { MediaHandler, MediaHandlerEvent } from "../../../src/webrtc/mediaHandler";
|
||||
import { MockMediaDeviceInfo, MockMediaDevices, MockMediaStream, MockMediaStreamTrack } from "../../test-utils/webrtc";
|
||||
|
||||
const FAKE_AUDIO_INPUT_ID = "aaaaaaaa";
|
||||
const FAKE_VIDEO_INPUT_ID = "vvvvvvvv";
|
||||
const FAKE_DESKTOP_SOURCE_ID = "ddddddd";
|
||||
|
||||
describe('Media Handler', function() {
|
||||
let mockMediaDevices: MockMediaDevices;
|
||||
let mediaHandler: MediaHandler;
|
||||
let calls: Map<string, MatrixCall>;
|
||||
let groupCalls: Map<string, GroupCall>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockMediaDevices = new MockMediaDevices();
|
||||
|
||||
global.navigator = {
|
||||
mediaDevices: mockMediaDevices.typed(),
|
||||
} as unknown as Navigator;
|
||||
|
||||
calls = new Map();
|
||||
groupCalls = new Map();
|
||||
|
||||
mediaHandler = new MediaHandler({
|
||||
callEventHandler: {
|
||||
calls,
|
||||
},
|
||||
groupCallEventHandler: {
|
||||
groupCalls,
|
||||
},
|
||||
} as unknown as MatrixClient);
|
||||
});
|
||||
|
||||
it("does not trigger update after restore media settings ", () => {
|
||||
mediaHandler.restoreMediaSettings(FAKE_AUDIO_INPUT_ID, FAKE_VIDEO_INPUT_ID);
|
||||
|
||||
expect(mockMediaDevices.getUserMedia).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sets device IDs on restore media settings", async () => {
|
||||
mediaHandler.restoreMediaSettings(FAKE_AUDIO_INPUT_ID, FAKE_VIDEO_INPUT_ID);
|
||||
|
||||
await mediaHandler.getUserMediaStream(true, true);
|
||||
expect(mockMediaDevices.getUserMedia).toHaveBeenCalledWith(expect.objectContaining({
|
||||
audio: expect.objectContaining({
|
||||
deviceId: { ideal: FAKE_AUDIO_INPUT_ID },
|
||||
}),
|
||||
video: expect.objectContaining({
|
||||
deviceId: { ideal: FAKE_VIDEO_INPUT_ID },
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
it("sets audio device ID", async () => {
|
||||
await mediaHandler.setAudioInput(FAKE_AUDIO_INPUT_ID);
|
||||
|
||||
await mediaHandler.getUserMediaStream(true, false);
|
||||
expect(mockMediaDevices.getUserMedia).toHaveBeenCalledWith(expect.objectContaining({
|
||||
audio: expect.objectContaining({
|
||||
deviceId: { ideal: FAKE_AUDIO_INPUT_ID },
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
it("sets audio settings", async () => {
|
||||
await mediaHandler.setAudioSettings({
|
||||
autoGainControl: false,
|
||||
echoCancellation: true,
|
||||
noiseSuppression: false,
|
||||
});
|
||||
|
||||
await mediaHandler.getUserMediaStream(true, false);
|
||||
expect(mockMediaDevices.getUserMedia).toHaveBeenCalledWith(expect.objectContaining({
|
||||
audio: expect.objectContaining({
|
||||
autoGainControl: { ideal: false },
|
||||
echoCancellation: { ideal: true },
|
||||
noiseSuppression: { ideal: false },
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
it("sets video device ID", async () => {
|
||||
await mediaHandler.setVideoInput(FAKE_VIDEO_INPUT_ID);
|
||||
|
||||
await mediaHandler.getUserMediaStream(false, true);
|
||||
expect(mockMediaDevices.getUserMedia).toHaveBeenCalledWith(expect.objectContaining({
|
||||
video: expect.objectContaining({
|
||||
deviceId: { ideal: FAKE_VIDEO_INPUT_ID },
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
it("sets media inputs", async () => {
|
||||
await mediaHandler.setMediaInputs(FAKE_AUDIO_INPUT_ID, FAKE_VIDEO_INPUT_ID);
|
||||
|
||||
await mediaHandler.getUserMediaStream(true, true);
|
||||
expect(mockMediaDevices.getUserMedia).toHaveBeenCalledWith(expect.objectContaining({
|
||||
audio: expect.objectContaining({
|
||||
deviceId: { ideal: FAKE_AUDIO_INPUT_ID },
|
||||
}),
|
||||
video: expect.objectContaining({
|
||||
deviceId: { ideal: FAKE_VIDEO_INPUT_ID },
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
describe("updateLocalUsermediaStreams", () => {
|
||||
let localStreamsChangedHandler: jest.Mock<void, []>;
|
||||
|
||||
beforeEach(() => {
|
||||
localStreamsChangedHandler = jest.fn();
|
||||
mediaHandler.on(MediaHandlerEvent.LocalStreamsChanged, localStreamsChangedHandler);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mediaHandler.off(MediaHandlerEvent.LocalStreamsChanged, localStreamsChangedHandler);
|
||||
});
|
||||
|
||||
it("does nothing if it has no streams", async () => {
|
||||
mediaHandler.updateLocalUsermediaStreams();
|
||||
expect(mockMediaDevices.getUserMedia).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not emit LocalStreamsChanged if it had no streams", async () => {
|
||||
await mediaHandler.updateLocalUsermediaStreams();
|
||||
|
||||
expect(localStreamsChangedHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("with existing streams", () => {
|
||||
let stopTrack: jest.Mock<void, []>;
|
||||
let updateLocalUsermediaStream: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
stopTrack = jest.fn();
|
||||
|
||||
mediaHandler.userMediaStreams = [
|
||||
{
|
||||
getTracks: () => [{
|
||||
stop: stopTrack,
|
||||
} as unknown as MediaStreamTrack],
|
||||
} as unknown as MediaStream,
|
||||
];
|
||||
|
||||
updateLocalUsermediaStream = jest.fn();
|
||||
});
|
||||
|
||||
it("stops existing streams", async () => {
|
||||
mediaHandler.updateLocalUsermediaStreams();
|
||||
expect(stopTrack).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("replaces streams on calls", async () => {
|
||||
calls.set("some_call", {
|
||||
hasLocalUserMediaAudioTrack: true,
|
||||
hasLocalUserMediaVideoTrack: true,
|
||||
callHasEnded: jest.fn().mockReturnValue(false),
|
||||
updateLocalUsermediaStream,
|
||||
} as unknown as MatrixCall);
|
||||
|
||||
await mediaHandler.updateLocalUsermediaStreams();
|
||||
expect(updateLocalUsermediaStream).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("doesn't replace streams on ended calls", async () => {
|
||||
calls.set("some_call", {
|
||||
hasLocalUserMediaAudioTrack: true,
|
||||
hasLocalUserMediaVideoTrack: true,
|
||||
callHasEnded: jest.fn().mockReturnValue(true),
|
||||
updateLocalUsermediaStream,
|
||||
} as unknown as MatrixCall);
|
||||
|
||||
await mediaHandler.updateLocalUsermediaStreams();
|
||||
expect(updateLocalUsermediaStream).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("replaces streams on group calls", async () => {
|
||||
groupCalls.set("some_group_call", {
|
||||
localCallFeed: {},
|
||||
updateLocalUsermediaStream,
|
||||
} as unknown as GroupCall);
|
||||
|
||||
await mediaHandler.updateLocalUsermediaStreams();
|
||||
expect(updateLocalUsermediaStream).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("doesn't replace streams on group calls with no localCallFeed", async () => {
|
||||
groupCalls.set("some_group_call", {
|
||||
localCallFeed: null,
|
||||
updateLocalUsermediaStream,
|
||||
} as unknown as GroupCall);
|
||||
|
||||
await mediaHandler.updateLocalUsermediaStreams();
|
||||
expect(updateLocalUsermediaStream).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("emits LocalStreamsChanged", async () => {
|
||||
await mediaHandler.updateLocalUsermediaStreams();
|
||||
|
||||
expect(localStreamsChangedHandler).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasAudioDevice", () => {
|
||||
it("returns true if the system has audio inputs", async () => {
|
||||
expect(await mediaHandler.hasAudioDevice()).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns false if the system has no audio inputs", async () => {
|
||||
mockMediaDevices.enumerateDevices.mockReturnValue(Promise.resolve([
|
||||
new MockMediaDeviceInfo("videoinput").typed(),
|
||||
]));
|
||||
expect(await mediaHandler.hasAudioDevice()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasVideoDevice", () => {
|
||||
it("returns true if the system has video inputs", async () => {
|
||||
expect(await mediaHandler.hasVideoDevice()).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns false if the system has no video inputs", async () => {
|
||||
mockMediaDevices.enumerateDevices.mockReturnValue(Promise.resolve([
|
||||
new MockMediaDeviceInfo("audioinput").typed(),
|
||||
]));
|
||||
expect(await mediaHandler.hasVideoDevice()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUserMediaStream", () => {
|
||||
beforeEach(() => {
|
||||
// replace this with one that returns a new object each time so we can
|
||||
// tell whether we've ended up with the same stream
|
||||
mockMediaDevices.getUserMedia.mockImplementation((constraints: MediaStreamConstraints) => {
|
||||
const stream = new MockMediaStream("local_stream");
|
||||
if (constraints.audio) {
|
||||
const track = new MockMediaStreamTrack("audio_track", "audio");
|
||||
track.settings = { deviceId: FAKE_AUDIO_INPUT_ID };
|
||||
stream.addTrack(track);
|
||||
}
|
||||
if (constraints.video) {
|
||||
const track = new MockMediaStreamTrack("video_track", "video");
|
||||
track.settings = { deviceId: FAKE_VIDEO_INPUT_ID };
|
||||
stream.addTrack(track);
|
||||
}
|
||||
|
||||
return Promise.resolve(stream.typed());
|
||||
});
|
||||
|
||||
mediaHandler.restoreMediaSettings(FAKE_AUDIO_INPUT_ID, FAKE_VIDEO_INPUT_ID);
|
||||
});
|
||||
|
||||
it("returns the same stream for reusable streams", async () => {
|
||||
const stream1 = await mediaHandler.getUserMediaStream(true, false);
|
||||
const stream2 = await mediaHandler.getUserMediaStream(true, false) as unknown as MockMediaStream;
|
||||
|
||||
expect(stream2.isCloneOf(stream1)).toEqual(true);
|
||||
});
|
||||
|
||||
it("doesn't re-use stream if reusable is false", async () => {
|
||||
const stream1 = await mediaHandler.getUserMediaStream(true, false, false);
|
||||
const stream2 = await mediaHandler.getUserMediaStream(true, false) as unknown as MockMediaStream;
|
||||
|
||||
expect(stream2.isCloneOf(stream1)).toEqual(false);
|
||||
});
|
||||
|
||||
it("doesn't re-use stream if existing stream lacks audio", async () => {
|
||||
const stream1 = await mediaHandler.getUserMediaStream(false, true);
|
||||
const stream2 = await mediaHandler.getUserMediaStream(true, false) as unknown as MockMediaStream;
|
||||
|
||||
expect(stream2.isCloneOf(stream1)).toEqual(false);
|
||||
});
|
||||
|
||||
it("doesn't re-use stream if existing stream lacks video", async () => {
|
||||
const stream1 = await mediaHandler.getUserMediaStream(true, false);
|
||||
const stream2 = await mediaHandler.getUserMediaStream(false, true) as unknown as MockMediaStream;
|
||||
|
||||
expect(stream2.isCloneOf(stream1)).toEqual(false);
|
||||
});
|
||||
|
||||
it("strips unwanted audio tracks from re-used stream", async () => {
|
||||
const stream1 = await mediaHandler.getUserMediaStream(true, true);
|
||||
const stream2 = await mediaHandler.getUserMediaStream(false, true) as unknown as MockMediaStream;
|
||||
|
||||
expect(stream2.isCloneOf(stream1)).toEqual(true);
|
||||
expect(stream2.getAudioTracks().length).toEqual(0);
|
||||
});
|
||||
|
||||
it("strips unwanted video tracks from re-used stream", async () => {
|
||||
const stream1 = await mediaHandler.getUserMediaStream(true, true);
|
||||
const stream2 = await mediaHandler.getUserMediaStream(true, false) as unknown as MockMediaStream;
|
||||
|
||||
expect(stream2.isCloneOf(stream1)).toEqual(true);
|
||||
expect(stream2.getVideoTracks().length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getScreensharingStream", () => {
|
||||
it("gets any screen sharing stream when called with no args", async () => {
|
||||
const stream = await mediaHandler.getScreensharingStream();
|
||||
expect(stream).toBeTruthy();
|
||||
expect(stream.getTracks()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("re-uses streams", async () => {
|
||||
const stream = await mediaHandler.getScreensharingStream(undefined, true);
|
||||
|
||||
expect(mockMediaDevices.getDisplayMedia).toHaveBeenCalled();
|
||||
mockMediaDevices.getDisplayMedia.mockClear();
|
||||
|
||||
const stream2 = await mediaHandler.getScreensharingStream() as unknown as MockMediaStream;
|
||||
|
||||
expect(mockMediaDevices.getDisplayMedia).not.toHaveBeenCalled();
|
||||
|
||||
expect(stream2.isCloneOf(stream)).toEqual(true);
|
||||
});
|
||||
|
||||
it("passes through desktopCapturerSourceId for Electron", async () => {
|
||||
await mediaHandler.getScreensharingStream({
|
||||
desktopCapturerSourceId: FAKE_DESKTOP_SOURCE_ID,
|
||||
});
|
||||
|
||||
expect(mockMediaDevices.getUserMedia).toHaveBeenCalledWith(expect.objectContaining({
|
||||
video: {
|
||||
mandatory: expect.objectContaining({
|
||||
chromeMediaSource: "desktop",
|
||||
chromeMediaSourceId: FAKE_DESKTOP_SOURCE_ID,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
it("emits LocalStreamsChanged", async () => {
|
||||
const onLocalStreamChanged = jest.fn();
|
||||
mediaHandler.on(MediaHandlerEvent.LocalStreamsChanged, onLocalStreamChanged);
|
||||
await mediaHandler.getScreensharingStream();
|
||||
expect(onLocalStreamChanged).toHaveBeenCalled();
|
||||
|
||||
mediaHandler.off(MediaHandlerEvent.LocalStreamsChanged, onLocalStreamChanged);
|
||||
});
|
||||
});
|
||||
|
||||
describe("stopUserMediaStream", () => {
|
||||
let stream: MediaStream;
|
||||
|
||||
beforeEach(async () => {
|
||||
stream = await mediaHandler.getUserMediaStream(true, false);
|
||||
});
|
||||
|
||||
it("stops tracks on streams", async () => {
|
||||
const mockTrack = new MockMediaStreamTrack("audio_track", "audio");
|
||||
stream.addTrack(mockTrack.typed());
|
||||
|
||||
mediaHandler.stopUserMediaStream(stream);
|
||||
expect(mockTrack.stop).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("removes stopped streams", async () => {
|
||||
expect(mediaHandler.userMediaStreams).toContain(stream);
|
||||
mediaHandler.stopUserMediaStream(stream);
|
||||
expect(mediaHandler.userMediaStreams).not.toContain(stream);
|
||||
});
|
||||
|
||||
it("emits LocalStreamsChanged", async () => {
|
||||
const onLocalStreamChanged = jest.fn();
|
||||
mediaHandler.on(MediaHandlerEvent.LocalStreamsChanged, onLocalStreamChanged);
|
||||
mediaHandler.stopUserMediaStream(stream);
|
||||
expect(onLocalStreamChanged).toHaveBeenCalled();
|
||||
|
||||
mediaHandler.off(MediaHandlerEvent.LocalStreamsChanged, onLocalStreamChanged);
|
||||
});
|
||||
});
|
||||
|
||||
describe("stopUserMediaStream", () => {
|
||||
let stream: MediaStream;
|
||||
|
||||
beforeEach(async () => {
|
||||
stream = await mediaHandler.getScreensharingStream();
|
||||
});
|
||||
|
||||
it("stops tracks on streams", async () => {
|
||||
const mockTrack = new MockMediaStreamTrack("audio_track", "audio");
|
||||
stream.addTrack(mockTrack.typed());
|
||||
|
||||
mediaHandler.stopScreensharingStream(stream);
|
||||
expect(mockTrack.stop).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("removes stopped streams", async () => {
|
||||
expect(mediaHandler.screensharingStreams).toContain(stream);
|
||||
mediaHandler.stopScreensharingStream(stream);
|
||||
expect(mediaHandler.screensharingStreams).not.toContain(stream);
|
||||
});
|
||||
|
||||
it("emits LocalStreamsChanged", async () => {
|
||||
const onLocalStreamChanged = jest.fn();
|
||||
mediaHandler.on(MediaHandlerEvent.LocalStreamsChanged, onLocalStreamChanged);
|
||||
mediaHandler.stopScreensharingStream(stream);
|
||||
expect(onLocalStreamChanged).toHaveBeenCalled();
|
||||
|
||||
mediaHandler.off(MediaHandlerEvent.LocalStreamsChanged, onLocalStreamChanged);
|
||||
});
|
||||
});
|
||||
|
||||
describe("stopAllStreams", () => {
|
||||
let userMediaStream: MediaStream;
|
||||
let screenSharingStream: MediaStream;
|
||||
|
||||
beforeEach(async () => {
|
||||
userMediaStream = await mediaHandler.getUserMediaStream(true, false);
|
||||
screenSharingStream = await mediaHandler.getScreensharingStream();
|
||||
});
|
||||
|
||||
it("stops tracks on streams", async () => {
|
||||
const mockUserMediaTrack = new MockMediaStreamTrack("audio_track", "audio");
|
||||
userMediaStream.addTrack(mockUserMediaTrack.typed());
|
||||
|
||||
const mockScreenshareTrack = new MockMediaStreamTrack("audio_track", "audio");
|
||||
screenSharingStream.addTrack(mockScreenshareTrack.typed());
|
||||
|
||||
mediaHandler.stopAllStreams();
|
||||
|
||||
expect(mockUserMediaTrack.stop).toHaveBeenCalled();
|
||||
expect(mockScreenshareTrack.stop).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("removes stopped streams", async () => {
|
||||
expect(mediaHandler.userMediaStreams).toContain(userMediaStream);
|
||||
expect(mediaHandler.screensharingStreams).toContain(screenSharingStream);
|
||||
mediaHandler.stopAllStreams();
|
||||
expect(mediaHandler.userMediaStreams).not.toContain(userMediaStream);
|
||||
expect(mediaHandler.screensharingStreams).not.toContain(screenSharingStream);
|
||||
});
|
||||
|
||||
it("emits LocalStreamsChanged", async () => {
|
||||
const onLocalStreamChanged = jest.fn();
|
||||
mediaHandler.on(MediaHandlerEvent.LocalStreamsChanged, onLocalStreamChanged);
|
||||
mediaHandler.stopAllStreams();
|
||||
expect(onLocalStreamChanged).toHaveBeenCalled();
|
||||
|
||||
mediaHandler.off(MediaHandlerEvent.LocalStreamsChanged, onLocalStreamChanged);
|
||||
});
|
||||
});
|
||||
});
|
||||
+18
-5
@@ -30,7 +30,7 @@ export enum TweakName {
|
||||
|
||||
export type Tweak<N extends TweakName, V> = {
|
||||
set_tweak: N;
|
||||
value: V;
|
||||
value?: V;
|
||||
};
|
||||
|
||||
export type TweakHighlight = Tweak<TweakName.Highlight, boolean>;
|
||||
@@ -65,6 +65,8 @@ export enum ConditionKind {
|
||||
ContainsDisplayName = "contains_display_name",
|
||||
RoomMemberCount = "room_member_count",
|
||||
SenderNotificationPermission = "sender_notification_permission",
|
||||
CallStarted = "call_started",
|
||||
CallStartedPrefix = "org.matrix.msc3914.call_started",
|
||||
}
|
||||
|
||||
export interface IPushRuleCondition<N extends ConditionKind | string> {
|
||||
@@ -74,7 +76,8 @@ export interface IPushRuleCondition<N extends ConditionKind | string> {
|
||||
|
||||
export interface IEventMatchCondition extends IPushRuleCondition<ConditionKind.EventMatch> {
|
||||
key: string;
|
||||
pattern: string;
|
||||
pattern?: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export interface IContainsDisplayNameCondition extends IPushRuleCondition<ConditionKind.ContainsDisplayName> {
|
||||
@@ -90,12 +93,22 @@ export interface ISenderNotificationPermissionCondition
|
||||
key: string;
|
||||
}
|
||||
|
||||
export interface ICallStartedCondition extends IPushRuleCondition<ConditionKind.CallStarted> {
|
||||
// no additional fields
|
||||
}
|
||||
|
||||
export interface ICallStartedPrefixCondition extends IPushRuleCondition<ConditionKind.CallStartedPrefix> {
|
||||
// no additional fields
|
||||
}
|
||||
|
||||
// XXX: custom conditions are possible but always fail, and break the typescript discriminated union so ignore them here
|
||||
// IPushRuleCondition<Exclude<string, ConditionKind>> unfortunately does not resolve this at the time of writing.
|
||||
export type PushRuleCondition = IEventMatchCondition
|
||||
| IContainsDisplayNameCondition
|
||||
| IRoomMemberCountCondition
|
||||
| ISenderNotificationPermissionCondition;
|
||||
| ISenderNotificationPermissionCondition
|
||||
| ICallStartedCondition
|
||||
| ICallStartedPrefixCondition;
|
||||
|
||||
export enum PushRuleKind {
|
||||
Override = "override",
|
||||
@@ -156,8 +169,8 @@ export interface IPusher {
|
||||
lang: string;
|
||||
profile_tag?: string;
|
||||
pushkey: string;
|
||||
enabled?: boolean | null | undefined;
|
||||
"org.matrix.msc3881.enabled"?: boolean | null | undefined;
|
||||
enabled?: boolean | null;
|
||||
"org.matrix.msc3881.enabled"?: boolean | null;
|
||||
device_id?: string | null;
|
||||
"org.matrix.msc3881.device_id"?: string | null;
|
||||
}
|
||||
|
||||
@@ -59,6 +59,10 @@ export enum EventType {
|
||||
KeyVerificationCancel = "m.key.verification.cancel",
|
||||
KeyVerificationMac = "m.key.verification.mac",
|
||||
KeyVerificationDone = "m.key.verification.done",
|
||||
KeyVerificationKey = "m.key.verification.key",
|
||||
KeyVerificationAccept = "m.key.verification.accept",
|
||||
// XXX this event is not yet supported by js-sdk
|
||||
KeyVerificationReady = "m.key.verification.ready",
|
||||
// use of this is discouraged https://matrix.org/docs/spec/client_server/r0.6.1#m-room-message-feedback
|
||||
RoomMessageFeedback = "m.room.message.feedback",
|
||||
Reaction = "m.reaction",
|
||||
@@ -83,6 +87,10 @@ export enum EventType {
|
||||
RoomKeyRequest = "m.room_key_request",
|
||||
ForwardedRoomKey = "m.forwarded_room_key",
|
||||
Dummy = "m.dummy",
|
||||
|
||||
// Group call events
|
||||
GroupCallPrefix = "org.matrix.msc3401.call",
|
||||
GroupCallMemberPrefix = "org.matrix.msc3401.call.member",
|
||||
}
|
||||
|
||||
export enum RelationType {
|
||||
|
||||
@@ -114,5 +114,6 @@ export interface ISearchResults {
|
||||
count?: number;
|
||||
next_batch?: string;
|
||||
pendingRequest?: Promise<ISearchResults>;
|
||||
abortSignal?: AbortSignal;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
+10
-7
@@ -23,7 +23,10 @@ import { Optional } from "matrix-events-sdk/lib/types";
|
||||
export class NamespacedValue<S extends string, U extends string> {
|
||||
// Stable is optional, but one of the two parameters is required, hence the weird-looking types.
|
||||
// Goal is to to have developers explicitly say there is no stable value (if applicable).
|
||||
public constructor(public readonly stable: S | null | undefined, public readonly unstable?: U) {
|
||||
public constructor(stable: S, unstable: U);
|
||||
public constructor(stable: S, unstable?: U);
|
||||
public constructor(stable: null | undefined, unstable: U);
|
||||
public constructor(public readonly stable?: S | null, public readonly unstable?: U) {
|
||||
if (!this.unstable && !this.stable) {
|
||||
throw new Error("One of stable or unstable values must be supplied");
|
||||
}
|
||||
@@ -33,10 +36,10 @@ export class NamespacedValue<S extends string, U extends string> {
|
||||
if (this.stable) {
|
||||
return this.stable;
|
||||
}
|
||||
return this.unstable;
|
||||
return this.unstable!;
|
||||
}
|
||||
|
||||
public get altName(): U | S | null {
|
||||
public get altName(): U | S | null | undefined {
|
||||
if (!this.stable) {
|
||||
return null;
|
||||
}
|
||||
@@ -57,7 +60,7 @@ export class NamespacedValue<S extends string, U extends string> {
|
||||
// this desperately wants https://github.com/microsoft/TypeScript/pull/26349 at the top level of the class
|
||||
// so we can instantiate `NamespacedValue<string, _, _>` as a default type for that namespace.
|
||||
public findIn<T>(obj: any): Optional<T> {
|
||||
let val: T;
|
||||
let val: T | undefined = undefined;
|
||||
if (this.name) {
|
||||
val = obj?.[this.name];
|
||||
}
|
||||
@@ -91,7 +94,7 @@ export class ServerControlledNamespacedValue<S extends string, U extends string>
|
||||
if (this.stable && !this.preferUnstable) {
|
||||
return this.stable;
|
||||
}
|
||||
return this.unstable;
|
||||
return this.unstable!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,10 +112,10 @@ export class UnstableValue<S extends string, U extends string> extends Namespace
|
||||
}
|
||||
|
||||
public get name(): U {
|
||||
return this.unstable;
|
||||
return this.unstable!;
|
||||
}
|
||||
|
||||
public get altName(): S {
|
||||
return this.stable;
|
||||
return this.stable!;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ export class ReEmitter {
|
||||
if (!reEmittersByEvent) return; // We were never re-emitting these events in the first place
|
||||
|
||||
for (const eventName of eventNames) {
|
||||
source.off(eventName, reEmittersByEvent.get(eventName));
|
||||
source.off(eventName, reEmittersByEvent.get(eventName)!);
|
||||
reEmittersByEvent.delete(eventName);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from "./logger";
|
||||
import { MatrixClient } from "./matrix";
|
||||
import { MatrixError, MatrixClient } from "./matrix";
|
||||
import { IndexedToDeviceBatch, ToDeviceBatch, ToDeviceBatchWithTxnId, ToDevicePayload } from "./models/ToDeviceMessage";
|
||||
import { MatrixScheduler } from "./scheduler";
|
||||
|
||||
@@ -48,14 +48,18 @@ export class ToDeviceMessageQueue {
|
||||
public async queueBatch(batch: ToDeviceBatch): Promise<void> {
|
||||
const batches: ToDeviceBatchWithTxnId[] = [];
|
||||
for (let i = 0; i < batch.batch.length; i += MAX_BATCH_SIZE) {
|
||||
batches.push({
|
||||
const batchWithTxnId = {
|
||||
eventType: batch.eventType,
|
||||
batch: batch.batch.slice(i, i + MAX_BATCH_SIZE),
|
||||
txnId: this.client.makeTxnId(),
|
||||
});
|
||||
};
|
||||
batches.push(batchWithTxnId);
|
||||
const recips = batchWithTxnId.batch.map((msg) => `${msg.userId}:${msg.deviceId}`);
|
||||
logger.info(`Created batch of to-device messages with txn id ${batchWithTxnId.txnId} for ${recips}`);
|
||||
}
|
||||
|
||||
await this.client.store.saveToDeviceBatches(batches);
|
||||
logger.info(`Enqueued to-device messages with txn ids ${batches.map((batch) => batch.txnId)}`);
|
||||
this.sendQueue();
|
||||
}
|
||||
|
||||
@@ -68,7 +72,7 @@ export class ToDeviceMessageQueue {
|
||||
logger.debug("Attempting to send queued to-device messages");
|
||||
|
||||
this.sending = true;
|
||||
let headBatch: IndexedToDeviceBatch;
|
||||
let headBatch: IndexedToDeviceBatch | null;
|
||||
try {
|
||||
while (this.running) {
|
||||
headBatch = await this.client.store.getOldestToDeviceBatch();
|
||||
@@ -86,11 +90,11 @@ export class ToDeviceMessageQueue {
|
||||
++this.retryAttempts;
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
// eslint-disable-next-line new-cap
|
||||
const retryDelay = MatrixScheduler.RETRY_BACKOFF_RATELIMIT(null, this.retryAttempts, e);
|
||||
const retryDelay = MatrixScheduler.RETRY_BACKOFF_RATELIMIT(null, this.retryAttempts, <MatrixError>e);
|
||||
if (retryDelay === -1) {
|
||||
// the scheduler function doesn't differentiate between fatal errors and just getting
|
||||
// bored and giving up for now
|
||||
if (Math.floor(e.httpStatus / 100) === 4) {
|
||||
if (Math.floor((<MatrixError>e).httpStatus! / 100) === 4) {
|
||||
logger.error("Fatal error when sending to-device message - dropping to-device batch!", e);
|
||||
await this.client.store.removeToDeviceBatch(headBatch!.id);
|
||||
} else {
|
||||
@@ -118,7 +122,9 @@ export class ToDeviceMessageQueue {
|
||||
contentMap[item.userId][item.deviceId] = item.payload;
|
||||
}
|
||||
|
||||
logger.info(`Sending batch of ${batch.batch.length} to-device messages with ID ${batch.id}`);
|
||||
logger.info(
|
||||
`Sending batch of ${batch.batch.length} to-device messages with ID ${batch.id} and txnId ${batch.txnId}`,
|
||||
);
|
||||
|
||||
await this.client.sendToDevice(batch.eventType, contentMap, batch.txnId);
|
||||
}
|
||||
|
||||
+49
-33
@@ -32,6 +32,28 @@ export enum AutoDiscoveryAction {
|
||||
FAIL_ERROR = "FAIL_ERROR",
|
||||
}
|
||||
|
||||
enum AutoDiscoveryError {
|
||||
Invalid = "Invalid homeserver discovery response",
|
||||
GenericFailure = "Failed to get autodiscovery configuration from server",
|
||||
InvalidHsBaseUrl = "Invalid base_url for m.homeserver",
|
||||
InvalidHomeserver = "Homeserver URL does not appear to be a valid Matrix homeserver",
|
||||
InvalidIsBaseUrl = "Invalid base_url for m.identity_server",
|
||||
InvalidIdentityServer = "Identity server URL does not appear to be a valid identity server",
|
||||
InvalidIs = "Invalid identity server discovery response",
|
||||
MissingWellknown = "No .well-known JSON file found",
|
||||
InvalidJson = "Invalid JSON",
|
||||
}
|
||||
|
||||
interface WellKnownConfig extends Omit<IWellKnownConfig, "error"> {
|
||||
state: AutoDiscoveryAction;
|
||||
error?: IWellKnownConfig["error"] | null;
|
||||
}
|
||||
|
||||
interface ClientConfig {
|
||||
"m.homeserver": WellKnownConfig;
|
||||
"m.identity_server": WellKnownConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utilities for automatically discovery resources, such as homeservers
|
||||
* for users to log in to.
|
||||
@@ -42,36 +64,25 @@ export class AutoDiscovery {
|
||||
// translate the meaning of the states in the spec, but also
|
||||
// support our own if needed.
|
||||
|
||||
public static readonly ERROR_INVALID = "Invalid homeserver discovery response";
|
||||
public static readonly ERROR_INVALID = AutoDiscoveryError.Invalid;
|
||||
|
||||
public static readonly ERROR_GENERIC_FAILURE = "Failed to get autodiscovery configuration from server";
|
||||
public static readonly ERROR_GENERIC_FAILURE = AutoDiscoveryError.GenericFailure;
|
||||
|
||||
public static readonly ERROR_INVALID_HS_BASE_URL = "Invalid base_url for m.homeserver";
|
||||
public static readonly ERROR_INVALID_HS_BASE_URL = AutoDiscoveryError.InvalidHsBaseUrl;
|
||||
|
||||
public static readonly ERROR_INVALID_HOMESERVER = "Homeserver URL does not appear to be a valid Matrix homeserver";
|
||||
public static readonly ERROR_INVALID_HOMESERVER = AutoDiscoveryError.InvalidHomeserver;
|
||||
|
||||
public static readonly ERROR_INVALID_IS_BASE_URL = "Invalid base_url for m.identity_server";
|
||||
public static readonly ERROR_INVALID_IS_BASE_URL = AutoDiscoveryError.InvalidIsBaseUrl;
|
||||
|
||||
// eslint-disable-next-line
|
||||
public static readonly ERROR_INVALID_IDENTITY_SERVER = "Identity server URL does not appear to be a valid identity server";
|
||||
public static readonly ERROR_INVALID_IDENTITY_SERVER = AutoDiscoveryError.InvalidIdentityServer;
|
||||
|
||||
public static readonly ERROR_INVALID_IS = "Invalid identity server discovery response";
|
||||
public static readonly ERROR_INVALID_IS = AutoDiscoveryError.InvalidIs;
|
||||
|
||||
public static readonly ERROR_MISSING_WELLKNOWN = "No .well-known JSON file found";
|
||||
public static readonly ERROR_MISSING_WELLKNOWN = AutoDiscoveryError.MissingWellknown;
|
||||
|
||||
public static readonly ERROR_INVALID_JSON = "Invalid JSON";
|
||||
public static readonly ERROR_INVALID_JSON = AutoDiscoveryError.InvalidJson;
|
||||
|
||||
public static readonly ALL_ERRORS = [
|
||||
AutoDiscovery.ERROR_INVALID,
|
||||
AutoDiscovery.ERROR_GENERIC_FAILURE,
|
||||
AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
|
||||
AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
|
||||
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
|
||||
AutoDiscovery.ERROR_INVALID_IS,
|
||||
AutoDiscovery.ERROR_MISSING_WELLKNOWN,
|
||||
AutoDiscovery.ERROR_INVALID_JSON,
|
||||
];
|
||||
public static readonly ALL_ERRORS = Object.keys(AutoDiscoveryError);
|
||||
|
||||
/**
|
||||
* The auto discovery failed. The client is expected to communicate
|
||||
@@ -120,13 +131,13 @@ export class AutoDiscovery {
|
||||
* configuration, which may include error states. Rejects on unexpected
|
||||
* failure, not when verification fails.
|
||||
*/
|
||||
public static async fromDiscoveryConfig(wellknown: any): Promise<IClientWellKnown> {
|
||||
public static async fromDiscoveryConfig(wellknown: any): Promise<ClientConfig> {
|
||||
// Step 1 is to get the config, which is provided to us here.
|
||||
|
||||
// We default to an error state to make the first few checks easier to
|
||||
// write. We'll update the properties of this object over the duration
|
||||
// of this function.
|
||||
const clientConfig = {
|
||||
const clientConfig: ClientConfig = {
|
||||
"m.homeserver": {
|
||||
state: AutoDiscovery.FAIL_ERROR,
|
||||
error: AutoDiscovery.ERROR_INVALID,
|
||||
@@ -197,7 +208,7 @@ export class AutoDiscovery {
|
||||
if (wellknown["m.identity_server"]) {
|
||||
// We prepare a failing identity server response to save lines later
|
||||
// in this branch.
|
||||
const failingClientConfig = {
|
||||
const failingClientConfig: ClientConfig = {
|
||||
"m.homeserver": clientConfig["m.homeserver"],
|
||||
"m.identity_server": {
|
||||
state: AutoDiscovery.FAIL_PROMPT,
|
||||
@@ -279,7 +290,7 @@ export class AutoDiscovery {
|
||||
* configuration, which may include error states. Rejects on unexpected
|
||||
* failure, not when discovery fails.
|
||||
*/
|
||||
public static async findClientConfig(domain: string): Promise<IClientWellKnown> {
|
||||
public static async findClientConfig(domain: string): Promise<ClientConfig> {
|
||||
if (!domain || typeof(domain) !== "string" || domain.length === 0) {
|
||||
throw new Error("'domain' must be a string of non-zero length");
|
||||
}
|
||||
@@ -298,7 +309,7 @@ export class AutoDiscovery {
|
||||
// We default to an error state to make the first few checks easier to
|
||||
// write. We'll update the properties of this object over the duration
|
||||
// of this function.
|
||||
const clientConfig = {
|
||||
const clientConfig: ClientConfig = {
|
||||
"m.homeserver": {
|
||||
state: AutoDiscovery.FAIL_ERROR,
|
||||
error: AutoDiscovery.ERROR_INVALID,
|
||||
@@ -347,7 +358,7 @@ export class AutoDiscovery {
|
||||
* @returns {Promise<object>} Resolves to the domain's client config. Can
|
||||
* be an empty object.
|
||||
*/
|
||||
public static async getRawClientConfig(domain: string): Promise<IClientWellKnown> {
|
||||
public static async getRawClientConfig(domain?: string): Promise<IClientWellKnown> {
|
||||
if (!domain || typeof(domain) !== "string" || domain.length === 0) {
|
||||
throw new Error("'domain' must be a string of non-zero length");
|
||||
}
|
||||
@@ -367,18 +378,18 @@ export class AutoDiscovery {
|
||||
* @return {string|boolean} The sanitized URL or a falsey value if the URL is invalid.
|
||||
* @private
|
||||
*/
|
||||
private static sanitizeWellKnownUrl(url: string): string | boolean {
|
||||
private static sanitizeWellKnownUrl(url: string): string | false {
|
||||
if (!url) return false;
|
||||
|
||||
try {
|
||||
let parsed = null;
|
||||
let parsed: URL | undefined;
|
||||
try {
|
||||
parsed = new URL(url);
|
||||
} catch (e) {
|
||||
logger.error("Could not parse url", e);
|
||||
}
|
||||
|
||||
if (!parsed || !parsed.hostname) return false;
|
||||
if (!parsed?.hostname) return false;
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
|
||||
|
||||
const port = parsed.port ? `:${parsed.port}` : "";
|
||||
@@ -448,12 +459,17 @@ export class AutoDiscovery {
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err as Error | string | undefined;
|
||||
const error = err as AutoDiscoveryError | string | undefined;
|
||||
let reason = "";
|
||||
if (typeof error === "object") {
|
||||
reason = (<Error>error)?.message;
|
||||
}
|
||||
|
||||
return {
|
||||
error,
|
||||
raw: {},
|
||||
action: AutoDiscoveryAction.FAIL_PROMPT,
|
||||
reason: (<Error>error)?.message || "General failure",
|
||||
reason: reason || "General failure",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -463,7 +479,7 @@ export class AutoDiscovery {
|
||||
action: AutoDiscoveryAction.SUCCESS,
|
||||
};
|
||||
} catch (err) {
|
||||
const error = err as Error | string | undefined;
|
||||
const error = err as Error;
|
||||
return {
|
||||
error,
|
||||
raw: {},
|
||||
|
||||
+734
-417
File diff suppressed because it is too large
Load Diff
+14
-15
@@ -118,12 +118,12 @@ export function makeEmoteMessage(body: string) {
|
||||
/** Location content helpers */
|
||||
|
||||
export const getTextForLocationEvent = (
|
||||
uri: string,
|
||||
uri: string | undefined,
|
||||
assetType: LocationAssetType,
|
||||
timestamp: number,
|
||||
description?: string,
|
||||
timestamp?: number,
|
||||
description?: string | null,
|
||||
): string => {
|
||||
const date = `at ${new Date(timestamp).toISOString()}`;
|
||||
const date = `at ${new Date(timestamp!).toISOString()}`;
|
||||
const assetName = assetType === LocationAssetType.Self ? 'User' : undefined;
|
||||
const quotedDescription = description ? `"${description}"` : undefined;
|
||||
|
||||
@@ -139,8 +139,7 @@ export const getTextForLocationEvent = (
|
||||
/**
|
||||
* Generates the content for a Location event
|
||||
* @param uri a geo:// uri for the location
|
||||
* @param timestamp the timestamp when the location was correct (milliseconds since
|
||||
* the UNIX epoch)
|
||||
* @param timestamp the timestamp when the location was correct (milliseconds since the UNIX epoch)
|
||||
* @param description the (optional) label for this location on the map
|
||||
* @param assetType the (optional) asset type of this location e.g. "m.self"
|
||||
* @param text optional. A text for the location
|
||||
@@ -148,10 +147,10 @@ export const getTextForLocationEvent = (
|
||||
export const makeLocationContent = (
|
||||
// this is first but optional
|
||||
// to avoid a breaking change
|
||||
text: string | undefined,
|
||||
uri: string,
|
||||
text?: string,
|
||||
uri?: string,
|
||||
timestamp?: number,
|
||||
description?: string,
|
||||
description?: string | null,
|
||||
assetType?: LocationAssetType,
|
||||
): LegacyLocationEventContent & MLocationEventContent => {
|
||||
const defaultedText = text ??
|
||||
@@ -188,7 +187,7 @@ export const parseLocationEvent = (wireEventContent: LocationEventWireContent):
|
||||
const assetType = asset?.type ?? LocationAssetType.Self;
|
||||
const fallbackText = text ?? wireEventContent.body;
|
||||
|
||||
return makeLocationContent(fallbackText, geoUri, timestamp, description, assetType);
|
||||
return makeLocationContent(fallbackText, geoUri, timestamp ?? undefined, description, assetType);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -202,7 +201,7 @@ export type MakeTopicContent = (
|
||||
export const makeTopicContent: MakeTopicContent = (topic, htmlTopic) => {
|
||||
const renderings = [{ body: topic, mimetype: "text/plain" }];
|
||||
if (isProvided(htmlTopic)) {
|
||||
renderings.push({ body: htmlTopic, mimetype: "text/html" });
|
||||
renderings.push({ body: htmlTopic!, mimetype: "text/html" });
|
||||
}
|
||||
return { topic, [M_TOPIC.name]: renderings };
|
||||
};
|
||||
@@ -248,14 +247,14 @@ export const makeBeaconInfoContent: MakeBeaconInfoContent = (
|
||||
|
||||
export type BeaconInfoState = MBeaconInfoContent & {
|
||||
assetType?: LocationAssetType;
|
||||
timestamp: number;
|
||||
timestamp?: number;
|
||||
};
|
||||
/**
|
||||
* Flatten beacon info event content
|
||||
*/
|
||||
export const parseBeaconInfoContent = (content: MBeaconInfoEventContent): BeaconInfoState => {
|
||||
const { description, timeout, live } = content;
|
||||
const timestamp = M_TIMESTAMP.findIn<number>(content);
|
||||
const timestamp = M_TIMESTAMP.findIn<number>(content) ?? undefined;
|
||||
const asset = M_ASSET.findIn<MAssetContent>(content);
|
||||
|
||||
return {
|
||||
@@ -291,14 +290,14 @@ export const makeBeaconContent: MakeBeaconContent = (
|
||||
},
|
||||
});
|
||||
|
||||
export type BeaconLocationState = MLocationContent & {
|
||||
export type BeaconLocationState = Omit<MLocationContent, "uri"> & {
|
||||
uri?: string; // override from MLocationContent to allow optionals
|
||||
timestamp?: number;
|
||||
};
|
||||
|
||||
export const parseBeaconContent = (content: MBeaconEventContent): BeaconLocationState => {
|
||||
const location = M_LOCATION.findIn<MLocationContent>(content);
|
||||
const timestamp = M_TIMESTAMP.findIn<number>(content);
|
||||
const timestamp = M_TIMESTAMP.findIn<number>(content) ?? undefined;
|
||||
|
||||
return {
|
||||
description: location?.description,
|
||||
|
||||
+29
-28
@@ -29,7 +29,7 @@ import { DeviceInfo } from "./deviceinfo";
|
||||
import { SecretStorage } from "./SecretStorage";
|
||||
import { ICrossSigningKey, ISignedKey, MatrixClient } from "../client";
|
||||
import { OlmDevice } from "./OlmDevice";
|
||||
import { ICryptoCallbacks } from "../matrix";
|
||||
import { ICryptoCallbacks } from ".";
|
||||
import { ISignatures } from "../@types/signed";
|
||||
import { CryptoStore, SecretStorePrivateKeys } from "./store/base";
|
||||
import { ISecretStorageKeyInfo } from "./api";
|
||||
@@ -44,8 +44,8 @@ function publicKeyFromKeyInfo(keyInfo: ICrossSigningKey): string {
|
||||
}
|
||||
|
||||
export interface ICacheCallbacks {
|
||||
getCrossSigningKeyCache?(type: string, expectedPublicKey?: string): Promise<Uint8Array>;
|
||||
storeCrossSigningKeyCache?(type: string, key: Uint8Array): Promise<void>;
|
||||
getCrossSigningKeyCache?(type: string, expectedPublicKey?: string): Promise<Uint8Array | null>;
|
||||
storeCrossSigningKeyCache?(type: string, key?: Uint8Array): Promise<void>;
|
||||
}
|
||||
|
||||
export interface ICrossSigningInfo {
|
||||
@@ -114,10 +114,10 @@ export class CrossSigningInfo {
|
||||
}
|
||||
|
||||
if (expectedPubkey === undefined) {
|
||||
expectedPubkey = this.getId(type);
|
||||
expectedPubkey = this.getId(type)!;
|
||||
}
|
||||
|
||||
function validateKey(key: Uint8Array): [string, PkSigning] {
|
||||
function validateKey(key: Uint8Array | null): [string, PkSigning] | undefined {
|
||||
if (!key) return;
|
||||
const signing = new global.Olm.PkSigning();
|
||||
const gotPubkey = signing.init_with_seed(key);
|
||||
@@ -127,7 +127,7 @@ export class CrossSigningInfo {
|
||||
signing.free();
|
||||
}
|
||||
|
||||
let privkey;
|
||||
let privkey: Uint8Array | null = null;
|
||||
if (this.cacheCallbacks.getCrossSigningKeyCache && shouldCache) {
|
||||
privkey = await this.cacheCallbacks.getCrossSigningKeyCache(type, expectedPubkey);
|
||||
}
|
||||
@@ -141,7 +141,7 @@ export class CrossSigningInfo {
|
||||
const result = validateKey(privkey);
|
||||
if (result) {
|
||||
if (this.cacheCallbacks.storeCrossSigningKeyCache && shouldCache) {
|
||||
await this.cacheCallbacks.storeCrossSigningKeyCache(type, privkey);
|
||||
await this.cacheCallbacks.storeCrossSigningKeyCache(type, privkey!);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -169,7 +169,9 @@ export class CrossSigningInfo {
|
||||
* with, or null if it is not present or not encrypted with a trusted
|
||||
* key
|
||||
*/
|
||||
public async isStoredInSecretStorage(secretStorage: SecretStorage): Promise<Record<string, object>> {
|
||||
public async isStoredInSecretStorage(
|
||||
secretStorage: SecretStorage<MatrixClient | undefined>,
|
||||
): Promise<Record<string, object> | null> {
|
||||
// check what SSSS keys have encrypted the master key (if any)
|
||||
const stored = await secretStorage.isStored("m.cross_signing.master") || {};
|
||||
// then check which of those SSSS keys have also encrypted the SSK and USK
|
||||
@@ -196,7 +198,7 @@ export class CrossSigningInfo {
|
||||
*/
|
||||
public static async storeInSecretStorage(
|
||||
keys: Map<string, Uint8Array>,
|
||||
secretStorage: SecretStorage,
|
||||
secretStorage: SecretStorage<undefined>,
|
||||
): Promise<void> {
|
||||
for (const [type, privateKey] of keys) {
|
||||
const encodedKey = encodeBase64(privateKey);
|
||||
@@ -213,7 +215,7 @@ export class CrossSigningInfo {
|
||||
* @param {SecretStorage} secretStorage The secret store using account data
|
||||
* @return {Uint8Array} The private key
|
||||
*/
|
||||
public static async getFromSecretStorage(type: string, secretStorage: SecretStorage): Promise<Uint8Array> {
|
||||
public static async getFromSecretStorage(type: string, secretStorage: SecretStorage): Promise<Uint8Array | null> {
|
||||
const encodedKey = await secretStorage.get(`m.cross_signing.${type}`);
|
||||
if (!encodedKey) {
|
||||
return null;
|
||||
@@ -233,7 +235,7 @@ export class CrossSigningInfo {
|
||||
if (!cacheCallbacks) return false;
|
||||
const types = type ? [type] : ["master", "self_signing", "user_signing"];
|
||||
for (const t of types) {
|
||||
if (!await cacheCallbacks.getCrossSigningKeyCache(t)) {
|
||||
if (!await cacheCallbacks.getCrossSigningKeyCache?.(t)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -250,7 +252,7 @@ export class CrossSigningInfo {
|
||||
const cacheCallbacks = this.cacheCallbacks;
|
||||
if (!cacheCallbacks) return keys;
|
||||
for (const type of ["master", "self_signing", "user_signing"]) {
|
||||
const privKey = await cacheCallbacks.getCrossSigningKeyCache(type);
|
||||
const privKey = await cacheCallbacks.getCrossSigningKeyCache?.(type);
|
||||
if (!privKey) {
|
||||
continue;
|
||||
}
|
||||
@@ -268,7 +270,7 @@ export class CrossSigningInfo {
|
||||
*
|
||||
* @return {string} the ID
|
||||
*/
|
||||
public getId(type = "master"): string {
|
||||
public getId(type = "master"): string | null {
|
||||
if (!this.keys[type]) return null;
|
||||
const keyInfo = this.keys[type];
|
||||
return publicKeyFromKeyInfo(keyInfo);
|
||||
@@ -433,10 +435,9 @@ export class CrossSigningInfo {
|
||||
// if everything checks out, then save the keys
|
||||
if (keys.master) {
|
||||
this.keys.master = keys.master;
|
||||
// if the master key is set, then the old self-signing and
|
||||
// user-signing keys are obsolete
|
||||
this.keys.self_signing = null;
|
||||
this.keys.user_signing = null;
|
||||
// if the master key is set, then the old self-signing and user-signing keys are obsolete
|
||||
delete this.keys["self_signing"];
|
||||
delete this.keys["user_signing"];
|
||||
}
|
||||
if (keys.self_signing) {
|
||||
this.keys.self_signing = keys.self_signing;
|
||||
@@ -469,7 +470,7 @@ export class CrossSigningInfo {
|
||||
}
|
||||
}
|
||||
|
||||
public async signUser(key: CrossSigningInfo): Promise<ICrossSigningKey> {
|
||||
public async signUser(key: CrossSigningInfo): Promise<ICrossSigningKey | undefined> {
|
||||
if (!this.keys.user_signing) {
|
||||
logger.info("No user signing key: not signing user");
|
||||
return;
|
||||
@@ -477,7 +478,7 @@ export class CrossSigningInfo {
|
||||
return this.signObject(key.keys.master, "user_signing");
|
||||
}
|
||||
|
||||
public async signDevice(userId: string, device: DeviceInfo): Promise<ISignedKey> {
|
||||
public async signDevice(userId: string, device: DeviceInfo): Promise<ISignedKey | undefined> {
|
||||
if (userId !== this.userId) {
|
||||
throw new Error(
|
||||
`Trying to sign ${userId}'s device; can only sign our own device`,
|
||||
@@ -521,9 +522,9 @@ export class CrossSigningInfo {
|
||||
return new UserTrustLevel(false, false, userCrossSigning.firstUse);
|
||||
}
|
||||
|
||||
let userTrusted;
|
||||
let userTrusted: boolean;
|
||||
const userMaster = userCrossSigning.keys.master;
|
||||
const uskId = this.getId('user_signing');
|
||||
const uskId = this.getId('user_signing')!;
|
||||
try {
|
||||
pkVerify(userMaster, uskId, this.userId);
|
||||
userTrusted = true;
|
||||
@@ -567,7 +568,7 @@ export class CrossSigningInfo {
|
||||
const deviceObj = deviceToObject(device, userCrossSigning.userId);
|
||||
try {
|
||||
// if we can verify the user's SSK from their master key...
|
||||
pkVerify(userSSK, userCrossSigning.getId(), userCrossSigning.userId);
|
||||
pkVerify(userSSK, userCrossSigning.getId()!, userCrossSigning.userId);
|
||||
// ...and this device's key from their SSK...
|
||||
pkVerify(deviceObj, publicKeyFromKeyInfo(userSSK), userCrossSigning.userId);
|
||||
// ...then we trust this device as much as far as we trust the user
|
||||
@@ -723,7 +724,7 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O
|
||||
},
|
||||
storeCrossSigningKeyCache: async function(
|
||||
type: keyof SecretStorePrivateKeys,
|
||||
key: Uint8Array,
|
||||
key?: Uint8Array,
|
||||
): Promise<void> {
|
||||
if (!(key instanceof Uint8Array)) {
|
||||
throw new Error(
|
||||
@@ -752,7 +753,7 @@ export type KeysDuringVerification = [[string, PkSigning], [string, PkSigning],
|
||||
* @param {string} userId The user ID being verified
|
||||
* @param {string} deviceId The device ID being verified
|
||||
*/
|
||||
export function requestKeysDuringVerification(
|
||||
export async function requestKeysDuringVerification(
|
||||
baseApis: MatrixClient,
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
@@ -766,7 +767,7 @@ export function requestKeysDuringVerification(
|
||||
// it. We return here in order to test.
|
||||
return new Promise<KeysDuringVerification | void>((resolve, reject) => {
|
||||
const client = baseApis;
|
||||
const original = client.crypto.crossSigningInfo;
|
||||
const original = client.crypto!.crossSigningInfo;
|
||||
|
||||
// We already have all of the infrastructure we need to validate and
|
||||
// cache cross-signing keys, so instead of replicating that, here we set
|
||||
@@ -801,7 +802,7 @@ export function requestKeysDuringVerification(
|
||||
|
||||
// also request and cache the key backup key
|
||||
const backupKeyPromise = (async () => {
|
||||
const cachedKey = await client.crypto.getSessionBackupPrivateKey();
|
||||
const cachedKey = await client.crypto!.getSessionBackupPrivateKey();
|
||||
if (!cachedKey) {
|
||||
logger.info("No cached backup key found. Requesting...");
|
||||
const secretReq = client.requestSecret(
|
||||
@@ -811,13 +812,13 @@ export function requestKeysDuringVerification(
|
||||
logger.info("Got key backup key, decoding...");
|
||||
const decodedKey = decodeBase64(base64Key);
|
||||
logger.info("Decoded backup key, storing...");
|
||||
await client.crypto.storeSessionBackupPrivateKey(
|
||||
await client.crypto!.storeSessionBackupPrivateKey(
|
||||
Uint8Array.from(decodedKey),
|
||||
);
|
||||
logger.info("Backup key stored. Starting backup restore...");
|
||||
const backupInfo = await client.getKeyBackupVersion();
|
||||
// no need to await for this - just let it go in the bg
|
||||
client.restoreKeyBackupWithCache(undefined, undefined, backupInfo).then(() => {
|
||||
client.restoreKeyBackupWithCache(undefined, undefined, backupInfo!).then(() => {
|
||||
logger.info("Backup restored.");
|
||||
});
|
||||
}
|
||||
|
||||
+41
-44
@@ -26,7 +26,7 @@ import { CrossSigningInfo, ICrossSigningInfo } from './CrossSigning';
|
||||
import * as olmlib from './olmlib';
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import { chunkPromises, defer, IDeferred, sleep } from '../utils';
|
||||
import { IDownloadKeyResult, MatrixClient } from "../client";
|
||||
import { DeviceKeys, IDownloadKeyResult, Keys, MatrixClient, SigningKeys } from "../client";
|
||||
import { OlmDevice } from "./OlmDevice";
|
||||
import { CryptoStore } from "./store/base";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
@@ -81,24 +81,24 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
|
||||
// The 'next_batch' sync token at the point the data was written,
|
||||
// ie. a token representing the point immediately after the
|
||||
// moment represented by the snapshot in the db.
|
||||
private syncToken: string = null;
|
||||
private syncToken: string | null = null;
|
||||
|
||||
private keyDownloadsInProgressByUser: { [userId: string]: Promise<void> } = {};
|
||||
private keyDownloadsInProgressByUser = new Map<string, Promise<void>>();
|
||||
|
||||
// Set whenever changes are made other than setting the sync token
|
||||
private dirty = false;
|
||||
|
||||
// Promise resolved when device data is saved
|
||||
private savePromise: Promise<boolean> = null;
|
||||
private savePromise: Promise<boolean> | null = null;
|
||||
// Function that resolves the save promise
|
||||
private resolveSavePromise: (saved: boolean) => void = null;
|
||||
private resolveSavePromise: ((saved: boolean) => void) | null = null;
|
||||
// The time the save is scheduled for
|
||||
private savePromiseTime: number = null;
|
||||
private savePromiseTime: number | null = null;
|
||||
// The timer used to delay the save
|
||||
private saveTimer: ReturnType<typeof setTimeout> = null;
|
||||
private saveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
// True if we have fetched data from the server or loaded a non-empty
|
||||
// set of device data from the store
|
||||
private hasFetched: boolean = null;
|
||||
private hasFetched: boolean | null = null;
|
||||
|
||||
private readonly serialiser: DeviceListUpdateSerialiser;
|
||||
|
||||
@@ -127,7 +127,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
|
||||
deviceData.crossSigningInfo || {} : {};
|
||||
this.deviceTrackingStatus = deviceData ?
|
||||
deviceData.trackingStatus : {};
|
||||
this.syncToken = deviceData ? deviceData.syncToken : null;
|
||||
this.syncToken = deviceData?.syncToken ?? null;
|
||||
this.userByIdentityKey = {};
|
||||
for (const user of Object.keys(this.devices)) {
|
||||
const userDevices = this.devices[user];
|
||||
@@ -181,7 +181,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
|
||||
if (this.savePromiseTime && targetTime < this.savePromiseTime) {
|
||||
// There's a save scheduled but for after we would like: cancel
|
||||
// it & schedule one for the time we want
|
||||
clearTimeout(this.saveTimer);
|
||||
clearTimeout(this.saveTimer!);
|
||||
this.saveTimer = null;
|
||||
this.savePromiseTime = null;
|
||||
// (but keep the save promise since whatever called save before
|
||||
@@ -216,13 +216,13 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
|
||||
devices: this.devices,
|
||||
crossSigningInfo: this.crossSigningInfo,
|
||||
trackingStatus: this.deviceTrackingStatus,
|
||||
syncToken: this.syncToken,
|
||||
syncToken: this.syncToken ?? undefined,
|
||||
}, txn);
|
||||
},
|
||||
).then(() => {
|
||||
// The device list is considered dirty until the write completes.
|
||||
this.dirty = false;
|
||||
resolveSavePromise(true);
|
||||
resolveSavePromise?.(true);
|
||||
}, err => {
|
||||
logger.error('Failed to save device tracking data', this.syncToken);
|
||||
logger.error(err);
|
||||
@@ -238,7 +238,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
|
||||
*
|
||||
* @return {string} The sync token
|
||||
*/
|
||||
public getSyncToken(): string {
|
||||
public getSyncToken(): string | null {
|
||||
return this.syncToken;
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
|
||||
*
|
||||
* @param {string} st The sync token
|
||||
*/
|
||||
public setSyncToken(st: string): void {
|
||||
public setSyncToken(st: string | null): void {
|
||||
this.syncToken = st;
|
||||
}
|
||||
|
||||
@@ -272,14 +272,14 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
|
||||
|
||||
userIds.forEach((u) => {
|
||||
const trackingStatus = this.deviceTrackingStatus[u];
|
||||
if (this.keyDownloadsInProgressByUser[u]) {
|
||||
if (this.keyDownloadsInProgressByUser.has(u)) {
|
||||
// already a key download in progress/queued for this user; its results
|
||||
// will be good enough for us.
|
||||
logger.log(
|
||||
`downloadKeys: already have a download in progress for ` +
|
||||
`${u}: awaiting its result`,
|
||||
);
|
||||
promises.push(this.keyDownloadsInProgressByUser[u]);
|
||||
promises.push(this.keyDownloadsInProgressByUser.get(u)!);
|
||||
} else if (forceDownload || trackingStatus != TrackingStatus.UpToDate) {
|
||||
usersToDownload.push(u);
|
||||
}
|
||||
@@ -341,7 +341,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
|
||||
if (!devs) {
|
||||
return null;
|
||||
}
|
||||
const res = [];
|
||||
const res: DeviceInfo[] = [];
|
||||
for (const deviceId in devs) {
|
||||
if (devs.hasOwnProperty(deviceId)) {
|
||||
res.push(DeviceInfo.fromStorage(devs[deviceId], deviceId));
|
||||
@@ -362,7 +362,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
|
||||
return this.devices[userId];
|
||||
}
|
||||
|
||||
public getStoredCrossSigningForUser(userId: string): CrossSigningInfo {
|
||||
public getStoredCrossSigningForUser(userId: string): CrossSigningInfo | null {
|
||||
if (!this.crossSigningInfo[userId]) return null;
|
||||
|
||||
return CrossSigningInfo.fromStorage(this.crossSigningInfo[userId], userId);
|
||||
@@ -382,9 +382,9 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
|
||||
* @return {module:crypto/deviceinfo?} device, or undefined
|
||||
* if we don't know about this device
|
||||
*/
|
||||
public getStoredDevice(userId: string, deviceId: string): DeviceInfo {
|
||||
public getStoredDevice(userId: string, deviceId: string): DeviceInfo | undefined {
|
||||
const devs = this.devices[userId];
|
||||
if (!devs || !devs[deviceId]) {
|
||||
if (!devs?.[deviceId]) {
|
||||
return undefined;
|
||||
}
|
||||
return DeviceInfo.fromStorage(devs[deviceId], deviceId);
|
||||
@@ -398,11 +398,8 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
|
||||
*
|
||||
* @return {string} user ID
|
||||
*/
|
||||
public getUserByIdentityKey(algorithm: string, senderKey: string): string {
|
||||
if (
|
||||
algorithm !== olmlib.OLM_ALGORITHM &&
|
||||
algorithm !== olmlib.MEGOLM_ALGORITHM
|
||||
) {
|
||||
public getUserByIdentityKey(algorithm: string, senderKey: string): string | null {
|
||||
if (algorithm !== olmlib.OLM_ALGORITHM && algorithm !== olmlib.MEGOLM_ALGORITHM) {
|
||||
// we only deal in olm keys
|
||||
return null;
|
||||
}
|
||||
@@ -557,7 +554,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
|
||||
public refreshOutdatedDeviceLists(): Promise<void> {
|
||||
this.saveIfDirty();
|
||||
|
||||
const usersToDownload = [];
|
||||
const usersToDownload: string[] = [];
|
||||
for (const userId of Object.keys(this.deviceTrackingStatus)) {
|
||||
const stat = this.deviceTrackingStatus[userId];
|
||||
if (stat == TrackingStatus.PendingDownload) {
|
||||
@@ -617,7 +614,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const prom = this.serialiser.updateDevicesForUsers(users, this.syncToken).then(() => {
|
||||
const prom = this.serialiser.updateDevicesForUsers(users, this.syncToken!).then(() => {
|
||||
finished(true);
|
||||
}, (e) => {
|
||||
logger.error(
|
||||
@@ -628,7 +625,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
|
||||
});
|
||||
|
||||
users.forEach((u) => {
|
||||
this.keyDownloadsInProgressByUser[u] = prom;
|
||||
this.keyDownloadsInProgressByUser.set(u, prom);
|
||||
const stat = this.deviceTrackingStatus[u];
|
||||
if (stat == TrackingStatus.PendingDownload) {
|
||||
this.deviceTrackingStatus[u] = TrackingStatus.DownloadInProgress;
|
||||
@@ -643,11 +640,11 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
|
||||
// we may have queued up another download request for this user
|
||||
// since we started this request. If that happens, we should
|
||||
// ignore the completion of the first one.
|
||||
if (this.keyDownloadsInProgressByUser[u] !== prom) {
|
||||
if (this.keyDownloadsInProgressByUser.get(u) !== prom) {
|
||||
logger.log('Another update in the queue for', u, '- not marking up-to-date');
|
||||
return;
|
||||
}
|
||||
delete this.keyDownloadsInProgressByUser[u];
|
||||
this.keyDownloadsInProgressByUser.delete(u);
|
||||
const stat = this.deviceTrackingStatus[u];
|
||||
if (stat == TrackingStatus.DownloadInProgress) {
|
||||
if (success) {
|
||||
@@ -687,9 +684,9 @@ class DeviceListUpdateSerialiser {
|
||||
|
||||
// deferred which is resolved when the queued users are downloaded.
|
||||
// non-null indicates that we have users queued for download.
|
||||
private queuedQueryDeferred: IDeferred<void> = null;
|
||||
private queuedQueryDeferred?: IDeferred<void>;
|
||||
|
||||
private syncToken: string = null; // The sync token we send with the requests
|
||||
private syncToken?: string; // The sync token we send with the requests
|
||||
|
||||
/*
|
||||
* @param {object} baseApis Base API object
|
||||
@@ -748,7 +745,7 @@ class DeviceListUpdateSerialiser {
|
||||
const downloadUsers = Object.keys(this.keyDownloadsQueuedByUser);
|
||||
this.keyDownloadsQueuedByUser = {};
|
||||
const deferred = this.queuedQueryDeferred;
|
||||
this.queuedQueryDeferred = null;
|
||||
this.queuedQueryDeferred = undefined;
|
||||
|
||||
logger.log('Starting key download for', downloadUsers);
|
||||
this.downloadInProgress = true;
|
||||
@@ -785,9 +782,9 @@ class DeviceListUpdateSerialiser {
|
||||
try {
|
||||
await this.processQueryResponseForUser(
|
||||
userId, dk[userId], {
|
||||
master: masterKeys[userId],
|
||||
self_signing: ssks[userId],
|
||||
user_signing: usks[userId],
|
||||
master: masterKeys?.[userId],
|
||||
self_signing: ssks?.[userId],
|
||||
user_signing: usks?.[userId],
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -800,7 +797,7 @@ class DeviceListUpdateSerialiser {
|
||||
logger.log('Completed key download for ' + downloadUsers);
|
||||
|
||||
this.downloadInProgress = false;
|
||||
deferred.resolve();
|
||||
deferred?.resolve();
|
||||
|
||||
// if we have queued users, fire off another request.
|
||||
if (this.queuedQueryDeferred) {
|
||||
@@ -809,19 +806,19 @@ class DeviceListUpdateSerialiser {
|
||||
}, (e) => {
|
||||
logger.warn('Error downloading keys for ' + downloadUsers + ':', e);
|
||||
this.downloadInProgress = false;
|
||||
deferred.reject(e);
|
||||
deferred?.reject(e);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
return deferred!.promise;
|
||||
}
|
||||
|
||||
private async processQueryResponseForUser(
|
||||
userId: string,
|
||||
dkResponse: IDownloadKeyResult["device_keys"]["user_id"],
|
||||
dkResponse: DeviceKeys,
|
||||
crossSigningResponse: {
|
||||
master: IDownloadKeyResult["master_keys"]["user_id"];
|
||||
self_signing: IDownloadKeyResult["master_keys"]["user_id"]; // eslint-disable-line camelcase
|
||||
user_signing: IDownloadKeyResult["user_signing_keys"]["user_id"]; // eslint-disable-line camelcase
|
||||
master?: Keys;
|
||||
self_signing?: SigningKeys;
|
||||
user_signing?: SigningKeys;
|
||||
},
|
||||
): Promise<void> {
|
||||
logger.log('got device keys for ' + userId + ':', dkResponse);
|
||||
@@ -840,7 +837,7 @@ class DeviceListUpdateSerialiser {
|
||||
|
||||
await updateStoredDeviceKeysForUser(
|
||||
this.olmDevice, userId, userStore, dkResponse || {},
|
||||
this.baseApis.getUserId(), this.baseApis.deviceId,
|
||||
this.baseApis.getUserId()!, this.baseApis.deviceId!,
|
||||
);
|
||||
|
||||
// put the updates into the object that will be returned as our results
|
||||
|
||||
@@ -15,20 +15,19 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from "../logger";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { IContent, MatrixEvent } from "../models/event";
|
||||
import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning";
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import { Method, ClientPrefix } from "../http-api";
|
||||
import { Crypto, IBootstrapCrossSigningOpts } from "./index";
|
||||
import { Crypto, ICryptoCallbacks, IBootstrapCrossSigningOpts } from "./index";
|
||||
import {
|
||||
ClientEvent,
|
||||
CrossSigningKeys,
|
||||
ClientEventHandlerMap,
|
||||
CrossSigningKeys,
|
||||
ICrossSigningKey,
|
||||
ICryptoCallbacks,
|
||||
ISignedKey,
|
||||
KeySignatures,
|
||||
} from "../matrix";
|
||||
} from "../client";
|
||||
import { ISecretStorageKeyInfo } from "./api";
|
||||
import { IKeyBackupInfo } from "./keybackup";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
@@ -53,10 +52,10 @@ export class EncryptionSetupBuilder {
|
||||
public readonly crossSigningCallbacks: CrossSigningCallbacks;
|
||||
public readonly ssssCryptoCallbacks: SSSSCryptoCallbacks;
|
||||
|
||||
private crossSigningKeys: ICrossSigningKeys = null;
|
||||
private keySignatures: KeySignatures = null;
|
||||
private keyBackupInfo: IKeyBackupInfo = null;
|
||||
private sessionBackupPrivateKey: Uint8Array;
|
||||
private crossSigningKeys?: ICrossSigningKeys;
|
||||
private keySignatures?: KeySignatures;
|
||||
private keyBackupInfo?: IKeyBackupInfo;
|
||||
private sessionBackupPrivateKey?: Uint8Array;
|
||||
|
||||
/**
|
||||
* @param {Object.<String, MatrixEvent>} accountData pre-existing account data, will only be read, not written.
|
||||
@@ -162,14 +161,14 @@ export class EncryptionSetupBuilder {
|
||||
for (const type of ["master", "self_signing", "user_signing"]) {
|
||||
logger.log(`Cache ${type} cross-signing private key locally`);
|
||||
const privateKey = this.crossSigningCallbacks.privateKeys.get(type);
|
||||
await cacheCallbacks.storeCrossSigningKeyCache(type, privateKey);
|
||||
await cacheCallbacks.storeCrossSigningKeyCache?.(type, privateKey);
|
||||
}
|
||||
// store own cross-sign pubkeys as trusted
|
||||
await crypto.cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
crypto.cryptoStore.storeCrossSigningKeys(
|
||||
txn, this.crossSigningKeys.keys);
|
||||
txn, this.crossSigningKeys!.keys);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -195,9 +194,9 @@ export class EncryptionSetupOperation {
|
||||
*/
|
||||
constructor(
|
||||
private readonly accountData: Map<string, object>,
|
||||
private readonly crossSigningKeys: ICrossSigningKeys,
|
||||
private readonly keyBackupInfo: IKeyBackupInfo,
|
||||
private readonly keySignatures: KeySignatures,
|
||||
private readonly crossSigningKeys?: ICrossSigningKeys,
|
||||
private readonly keyBackupInfo?: IKeyBackupInfo,
|
||||
private readonly keySignatures?: KeySignatures,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -215,7 +214,7 @@ export class EncryptionSetupOperation {
|
||||
|
||||
// We must only call `uploadDeviceSigningKeys` from inside this auth
|
||||
// helper to ensure we properly handle auth errors.
|
||||
await this.crossSigningKeys.authUpload(authDict => {
|
||||
await this.crossSigningKeys.authUpload?.(authDict => {
|
||||
return baseApis.uploadDeviceSigningKeys(authDict, keys as CrossSigningKeys);
|
||||
});
|
||||
|
||||
@@ -281,15 +280,15 @@ class AccountDataClientAdapter
|
||||
* @param {String} type
|
||||
* @return {Promise<Object>} the content of the account data
|
||||
*/
|
||||
public getAccountDataFromServer(type: string): Promise<any> {
|
||||
return Promise.resolve(this.getAccountData(type));
|
||||
public getAccountDataFromServer<T extends {[k: string]: any}>(type: string): Promise<T> {
|
||||
return Promise.resolve(this.getAccountData(type) as T);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} type
|
||||
* @return {Object} the content of the account data
|
||||
*/
|
||||
public getAccountData(type: string): MatrixEvent {
|
||||
public getAccountData(type: string): IContent | null {
|
||||
const modifiedValue = this.values.get(type);
|
||||
if (modifiedValue) {
|
||||
return modifiedValue;
|
||||
@@ -329,7 +328,7 @@ class CrossSigningCallbacks implements ICryptoCallbacks, ICacheCallbacks {
|
||||
public readonly privateKeys = new Map<string, Uint8Array>();
|
||||
|
||||
// cache callbacks
|
||||
public getCrossSigningKeyCache(type: string, expectedPublicKey: string): Promise<Uint8Array> {
|
||||
public getCrossSigningKeyCache(type: string, expectedPublicKey: string): Promise<Uint8Array | null> {
|
||||
return this.getCrossSigningKey(type, expectedPublicKey);
|
||||
}
|
||||
|
||||
@@ -339,8 +338,8 @@ class CrossSigningCallbacks implements ICryptoCallbacks, ICacheCallbacks {
|
||||
}
|
||||
|
||||
// non-cache callbacks
|
||||
public getCrossSigningKey(type: string, expectedPubkey: string): Promise<Uint8Array> {
|
||||
return Promise.resolve(this.privateKeys.get(type));
|
||||
public getCrossSigningKey(type: string, expectedPubkey: string): Promise<Uint8Array | null> {
|
||||
return Promise.resolve(this.privateKeys.get(type) ?? null);
|
||||
}
|
||||
|
||||
public saveCrossSigningKeys(privateKeys: Record<string, Uint8Array>) {
|
||||
|
||||
+134
-127
@@ -15,9 +15,8 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { Account, InboundGroupSession, OutboundGroupSession, Session, Utility } from "@matrix-org/olm";
|
||||
import { Logger } from "loglevel";
|
||||
|
||||
import { logger } from '../logger';
|
||||
import { logger, PrefixedLogger } from '../logger';
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import * as algorithms from './algorithms';
|
||||
import { CryptoStore, IProblem, ISessionInfo, IWithheld } from "./store/base";
|
||||
@@ -121,9 +120,9 @@ interface IInboundGroupSessionKey {
|
||||
chain_index: number;
|
||||
key: string;
|
||||
forwarding_curve25519_key_chain: string[];
|
||||
sender_claimed_ed25519_key: string;
|
||||
sender_claimed_ed25519_key: string | null;
|
||||
shared_history: boolean;
|
||||
untrusted: boolean;
|
||||
untrusted?: boolean;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
@@ -145,9 +144,9 @@ export class OlmDevice {
|
||||
public pickleKey = "DEFAULT_KEY"; // set by consumers
|
||||
|
||||
// don't know these until we load the account from storage in init()
|
||||
public deviceCurve25519Key: string = null;
|
||||
public deviceEd25519Key: string = null;
|
||||
private maxOneTimeKeys: number = null;
|
||||
public deviceCurve25519Key: string | null = null;
|
||||
public deviceEd25519Key: string | null = null;
|
||||
private maxOneTimeKeys: number | null = null;
|
||||
|
||||
// we don't bother stashing outboundgroupsessions in the cryptoStore -
|
||||
// instead we keep them here.
|
||||
@@ -266,8 +265,8 @@ export class OlmDevice {
|
||||
lastReceivedMessageTs: session.lastReceivedMessageTs,
|
||||
};
|
||||
this.cryptoStore.storeEndToEndSession(
|
||||
deviceKey,
|
||||
sessionId,
|
||||
deviceKey!,
|
||||
sessionId!,
|
||||
sessionInfo,
|
||||
txn,
|
||||
);
|
||||
@@ -358,7 +357,7 @@ export class OlmDevice {
|
||||
// is not exactly the same thing you get in method _getSession
|
||||
// see documentation of IndexedDBCryptoStore.getAllEndToEndSessions
|
||||
this.cryptoStore.getAllEndToEndSessions(txn, (pickledSession) => {
|
||||
result.sessions.push(pickledSession);
|
||||
result.sessions!.push(pickledSession!);
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -384,8 +383,8 @@ export class OlmDevice {
|
||||
func: (unpickledSessionInfo: IUnpickledSessionInfo) => void,
|
||||
): void {
|
||||
this.cryptoStore.getEndToEndSession(
|
||||
deviceKey, sessionId, txn, (sessionInfo: ISessionInfo) => {
|
||||
this.unpickleSession(sessionInfo, func);
|
||||
deviceKey, sessionId, txn, (sessionInfo: ISessionInfo | null) => {
|
||||
this.unpickleSession(sessionInfo!, func);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -405,7 +404,7 @@ export class OlmDevice {
|
||||
): void {
|
||||
const session = new global.Olm.Session();
|
||||
try {
|
||||
session.unpickle(this.pickleKey, sessionInfo.session);
|
||||
session.unpickle(this.pickleKey, sessionInfo.session!);
|
||||
const unpickledSessInfo: IUnpickledSessionInfo = Object.assign({}, sessionInfo, { session });
|
||||
|
||||
func(unpickledSessInfo);
|
||||
@@ -491,7 +490,7 @@ export class OlmDevice {
|
||||
* @return {number} number of keys
|
||||
*/
|
||||
public maxNumberOfOneTimeKeys(): number {
|
||||
return this.maxOneTimeKeys;
|
||||
return this.maxOneTimeKeys ?? -1;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -554,7 +553,7 @@ export class OlmDevice {
|
||||
});
|
||||
},
|
||||
);
|
||||
return result;
|
||||
return result!;
|
||||
}
|
||||
|
||||
public async forgetOldFallbackKey(): Promise<void> {
|
||||
@@ -607,7 +606,7 @@ export class OlmDevice {
|
||||
},
|
||||
logger.withPrefix("[createOutboundSession]"),
|
||||
);
|
||||
return newSessionId;
|
||||
return newSessionId!;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -668,7 +667,7 @@ export class OlmDevice {
|
||||
logger.withPrefix("[createInboundSession]"),
|
||||
);
|
||||
|
||||
return result;
|
||||
return result!;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -681,7 +680,7 @@ export class OlmDevice {
|
||||
public async getSessionIdsForDevice(theirDeviceIdentityKey: string): Promise<string[]> {
|
||||
const log = logger.withPrefix("[getSessionIdsForDevice]");
|
||||
|
||||
if (this.sessionsInProgress[theirDeviceIdentityKey]) {
|
||||
if (theirDeviceIdentityKey in this.sessionsInProgress) {
|
||||
log.debug(`Waiting for Olm session for ${theirDeviceIdentityKey} to be created`);
|
||||
try {
|
||||
await this.sessionsInProgress[theirDeviceIdentityKey];
|
||||
@@ -703,7 +702,7 @@ export class OlmDevice {
|
||||
log,
|
||||
);
|
||||
|
||||
return sessionIds;
|
||||
return sessionIds!;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -714,13 +713,13 @@ export class OlmDevice {
|
||||
* @param {boolean} nowait Don't wait for an in-progress session to complete.
|
||||
* This should only be set to true of the calling function is the function
|
||||
* that marked the session as being in-progress.
|
||||
* @param {Logger} [log] A possibly customised log
|
||||
* @param {PrefixedLogger} [log] A possibly customised log
|
||||
* @return {Promise<?string>} session id, or null if no established session
|
||||
*/
|
||||
public async getSessionIdForDevice(
|
||||
theirDeviceIdentityKey: string,
|
||||
nowait = false,
|
||||
log?: Logger,
|
||||
log?: PrefixedLogger,
|
||||
): Promise<string | null> {
|
||||
const sessionInfos = await this.getSessionInfoForDevice(theirDeviceIdentityKey, nowait, log);
|
||||
|
||||
@@ -771,7 +770,7 @@ export class OlmDevice {
|
||||
): Promise<{ sessionId: string, lastReceivedMessageTs: number, hasReceivedMessage: boolean }[]> {
|
||||
log = log.withPrefix("[getSessionInfoForDevice]");
|
||||
|
||||
if (this.sessionsInProgress[deviceIdentityKey] && !nowait) {
|
||||
if (deviceIdentityKey in this.sessionsInProgress && !nowait) {
|
||||
log.debug(`Waiting for Olm session for ${deviceIdentityKey} to be created`);
|
||||
try {
|
||||
await this.sessionsInProgress[deviceIdentityKey];
|
||||
@@ -780,7 +779,11 @@ export class OlmDevice {
|
||||
// return an empty result
|
||||
}
|
||||
}
|
||||
const info = [];
|
||||
const info: {
|
||||
lastReceivedMessageTs: number;
|
||||
hasReceivedMessage: boolean;
|
||||
sessionId: string;
|
||||
}[] = [];
|
||||
|
||||
await this.cryptoStore.doTxn(
|
||||
'readonly', [IndexedDBCryptoStore.STORE_SESSIONS],
|
||||
@@ -790,9 +793,9 @@ export class OlmDevice {
|
||||
for (const sessionId of sessionIds) {
|
||||
this.unpickleSession(sessions[sessionId], (sessInfo: IUnpickledSessionInfo) => {
|
||||
info.push({
|
||||
lastReceivedMessageTs: sessInfo.lastReceivedMessageTs,
|
||||
lastReceivedMessageTs: sessInfo.lastReceivedMessageTs!,
|
||||
hasReceivedMessage: sessInfo.session.has_received_message(),
|
||||
sessionId: sessionId,
|
||||
sessionId,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -916,7 +919,7 @@ export class OlmDevice {
|
||||
await this.cryptoStore.storeEndToEndSessionProblem(deviceKey, type, fixed);
|
||||
}
|
||||
|
||||
public sessionMayHaveProblems(deviceKey: string, timestamp: number): Promise<IProblem> {
|
||||
public sessionMayHaveProblems(deviceKey: string, timestamp: number): Promise<IProblem | null> {
|
||||
return this.cryptoStore.getEndToEndSessionProblem(deviceKey, timestamp);
|
||||
}
|
||||
|
||||
@@ -1056,10 +1059,14 @@ export class OlmDevice {
|
||||
senderKey: string,
|
||||
sessionId: string,
|
||||
txn: unknown,
|
||||
func: (session: InboundGroupSession, data: InboundGroupSessionData, withheld?: IWithheld) => void,
|
||||
func: (
|
||||
session: InboundGroupSession | null,
|
||||
data: InboundGroupSessionData | null,
|
||||
withheld: IWithheld | null,
|
||||
) => void,
|
||||
): void {
|
||||
this.cryptoStore.getEndToEndInboundGroupSession(
|
||||
senderKey, sessionId, txn, (sessionData: InboundGroupSessionData, withheld: IWithheld | null) => {
|
||||
senderKey, sessionId, txn, (sessionData: InboundGroupSessionData | null, withheld: IWithheld | null) => {
|
||||
if (sessionData === null) {
|
||||
func(null, null, withheld);
|
||||
return;
|
||||
@@ -1112,94 +1119,94 @@ export class OlmDevice {
|
||||
IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS,
|
||||
], (txn) => {
|
||||
/* if we already have this session, consider updating it */
|
||||
this.getInboundGroupSession(
|
||||
roomId, senderKey, sessionId, txn,
|
||||
(existingSession: InboundGroupSession, existingSessionData: InboundGroupSessionData) => {
|
||||
// new session.
|
||||
const session = new global.Olm.InboundGroupSession();
|
||||
try {
|
||||
if (exportFormat) {
|
||||
session.import_session(sessionKey);
|
||||
} else {
|
||||
session.create(sessionKey);
|
||||
}
|
||||
if (sessionId != session.session_id()) {
|
||||
throw new Error(
|
||||
"Mismatched group session ID from senderKey: " +
|
||||
senderKey,
|
||||
);
|
||||
}
|
||||
|
||||
if (existingSession) {
|
||||
logger.log(
|
||||
"Update for megolm session "
|
||||
+ senderKey + "/" + sessionId,
|
||||
);
|
||||
if (existingSession.first_known_index() <= session.first_known_index()) {
|
||||
if (!existingSessionData.untrusted || extraSessionData.untrusted) {
|
||||
// existing session has less-than-or-equal index
|
||||
// (i.e. can decrypt at least as much), and the
|
||||
// new session's trust does not win over the old
|
||||
// session's trust, so keep it
|
||||
logger.log(`Keeping existing megolm session ${sessionId}`);
|
||||
return;
|
||||
}
|
||||
if (existingSession.first_known_index() < session.first_known_index()) {
|
||||
// We want to upgrade the existing session's trust,
|
||||
// but we can't just use the new session because we'll
|
||||
// lose the lower index. Check that the sessions connect
|
||||
// properly, and then manually set the existing session
|
||||
// as trusted.
|
||||
if (
|
||||
existingSession.export_session(session.first_known_index())
|
||||
=== session.export_session(session.first_known_index())
|
||||
) {
|
||||
logger.info(
|
||||
"Upgrading trust of existing megolm session " +
|
||||
sessionId + " based on newly-received trusted session",
|
||||
);
|
||||
existingSessionData.untrusted = false;
|
||||
this.cryptoStore.storeEndToEndInboundGroupSession(
|
||||
senderKey, sessionId, existingSessionData, txn,
|
||||
);
|
||||
} else {
|
||||
logger.warn(
|
||||
"Newly-received megolm session " + sessionId +
|
||||
" does not match existing session! Keeping existing session",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// If the sessions have the same index, go ahead and store the new trusted one.
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"Storing megolm session " + senderKey + "/" + sessionId +
|
||||
" with first index " + session.first_known_index(),
|
||||
);
|
||||
|
||||
const sessionData = Object.assign({}, extraSessionData, {
|
||||
room_id: roomId,
|
||||
session: session.pickle(this.pickleKey),
|
||||
keysClaimed: keysClaimed,
|
||||
forwardingCurve25519KeyChain: forwardingCurve25519KeyChain,
|
||||
});
|
||||
|
||||
this.cryptoStore.storeEndToEndInboundGroupSession(
|
||||
senderKey, sessionId, sessionData, txn,
|
||||
);
|
||||
|
||||
if (!existingSession && extraSessionData.sharedHistory) {
|
||||
this.cryptoStore.addSharedHistoryInboundGroupSession(
|
||||
roomId, senderKey, sessionId, txn,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
session.free();
|
||||
this.getInboundGroupSession(roomId, senderKey, sessionId, txn, (
|
||||
existingSession: InboundGroupSession | null,
|
||||
existingSessionData: InboundGroupSessionData | null,
|
||||
) => {
|
||||
// new session.
|
||||
const session = new global.Olm.InboundGroupSession();
|
||||
try {
|
||||
if (exportFormat) {
|
||||
session.import_session(sessionKey);
|
||||
} else {
|
||||
session.create(sessionKey);
|
||||
}
|
||||
},
|
||||
);
|
||||
if (sessionId != session.session_id()) {
|
||||
throw new Error(
|
||||
"Mismatched group session ID from senderKey: " +
|
||||
senderKey,
|
||||
);
|
||||
}
|
||||
|
||||
if (existingSession) {
|
||||
logger.log(
|
||||
"Update for megolm session "
|
||||
+ senderKey + "/" + sessionId,
|
||||
);
|
||||
if (existingSession.first_known_index() <= session.first_known_index()) {
|
||||
if (!existingSessionData!.untrusted || extraSessionData.untrusted) {
|
||||
// existing session has less-than-or-equal index
|
||||
// (i.e. can decrypt at least as much), and the
|
||||
// new session's trust does not win over the old
|
||||
// session's trust, so keep it
|
||||
logger.log(`Keeping existing megolm session ${sessionId}`);
|
||||
return;
|
||||
}
|
||||
if (existingSession.first_known_index() < session.first_known_index()) {
|
||||
// We want to upgrade the existing session's trust,
|
||||
// but we can't just use the new session because we'll
|
||||
// lose the lower index. Check that the sessions connect
|
||||
// properly, and then manually set the existing session
|
||||
// as trusted.
|
||||
if (
|
||||
existingSession.export_session(session.first_known_index())
|
||||
=== session.export_session(session.first_known_index())
|
||||
) {
|
||||
logger.info(
|
||||
"Upgrading trust of existing megolm session " +
|
||||
sessionId + " based on newly-received trusted session",
|
||||
);
|
||||
existingSessionData!.untrusted = false;
|
||||
this.cryptoStore.storeEndToEndInboundGroupSession(
|
||||
senderKey, sessionId, existingSessionData!, txn,
|
||||
);
|
||||
} else {
|
||||
logger.warn(
|
||||
"Newly-received megolm session " + sessionId +
|
||||
" does not match existing session! Keeping existing session",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// If the sessions have the same index, go ahead and store the new trusted one.
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"Storing megolm session " + senderKey + "/" + sessionId +
|
||||
" with first index " + session.first_known_index(),
|
||||
);
|
||||
|
||||
const sessionData = Object.assign({}, extraSessionData, {
|
||||
room_id: roomId,
|
||||
session: session.pickle(this.pickleKey),
|
||||
keysClaimed: keysClaimed,
|
||||
forwardingCurve25519KeyChain: forwardingCurve25519KeyChain,
|
||||
});
|
||||
|
||||
this.cryptoStore.storeEndToEndInboundGroupSession(
|
||||
senderKey, sessionId, sessionData, txn,
|
||||
);
|
||||
|
||||
if (!existingSession && extraSessionData.sharedHistory) {
|
||||
this.cryptoStore.addSharedHistoryInboundGroupSession(
|
||||
roomId, senderKey, sessionId, txn,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
session.free();
|
||||
}
|
||||
});
|
||||
},
|
||||
logger.withPrefix("[addInboundGroupSession]"),
|
||||
);
|
||||
@@ -1261,7 +1268,7 @@ export class OlmDevice {
|
||||
eventId: string,
|
||||
timestamp: number,
|
||||
): Promise<IDecryptedGroupMessage | null> {
|
||||
let result: IDecryptedGroupMessage;
|
||||
let result: IDecryptedGroupMessage | null = null;
|
||||
// when the localstorage crypto store is used as an indexeddb backend,
|
||||
// exceptions thrown from within the inner function are not passed through
|
||||
// to the top level, so we store exceptions in a variable and raise them at
|
||||
@@ -1275,7 +1282,7 @@ export class OlmDevice {
|
||||
], (txn) => {
|
||||
this.getInboundGroupSession(
|
||||
roomId, senderKey, sessionId, txn, (session, sessionData, withheld) => {
|
||||
if (session === null) {
|
||||
if (session === null || sessionData === null) {
|
||||
if (withheld) {
|
||||
error = new algorithms.DecryptionError(
|
||||
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
|
||||
@@ -1292,7 +1299,7 @@ export class OlmDevice {
|
||||
try {
|
||||
res = session.decrypt(body);
|
||||
} catch (e) {
|
||||
if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX' && withheld) {
|
||||
if ((<Error>e)?.message === 'OLM.UNKNOWN_MESSAGE_INDEX' && withheld) {
|
||||
error = new algorithms.DecryptionError(
|
||||
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
|
||||
calculateWithheldMessage(withheld),
|
||||
@@ -1301,7 +1308,7 @@ export class OlmDevice {
|
||||
},
|
||||
);
|
||||
} else {
|
||||
error = e;
|
||||
error = <Error>e;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1350,7 +1357,7 @@ export class OlmDevice {
|
||||
forwardingCurve25519KeyChain: (
|
||||
sessionData.forwardingCurve25519KeyChain || []
|
||||
),
|
||||
untrusted: sessionData.untrusted,
|
||||
untrusted: !!sessionData.untrusted,
|
||||
};
|
||||
},
|
||||
);
|
||||
@@ -1358,10 +1365,10 @@ export class OlmDevice {
|
||||
logger.withPrefix("[decryptGroupMessage]"),
|
||||
);
|
||||
|
||||
if (error) {
|
||||
if (error!) {
|
||||
throw error;
|
||||
}
|
||||
return result;
|
||||
return result!;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1404,7 +1411,7 @@ export class OlmDevice {
|
||||
logger.withPrefix("[hasInboundSessionKeys]"),
|
||||
);
|
||||
|
||||
return result;
|
||||
return result!;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1431,8 +1438,8 @@ export class OlmDevice {
|
||||
senderKey: string,
|
||||
sessionId: string,
|
||||
chainIndex?: number,
|
||||
): Promise<IInboundGroupSessionKey> {
|
||||
let result: IInboundGroupSessionKey;
|
||||
): Promise<IInboundGroupSessionKey | null> {
|
||||
let result: IInboundGroupSessionKey | null = null;
|
||||
await this.cryptoStore.doTxn(
|
||||
'readonly', [
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
|
||||
@@ -1440,7 +1447,7 @@ export class OlmDevice {
|
||||
], (txn) => {
|
||||
this.getInboundGroupSession(
|
||||
roomId, senderKey, sessionId, txn, (session, sessionData) => {
|
||||
if (session === null) {
|
||||
if (session === null || sessionData === null) {
|
||||
result = null;
|
||||
return;
|
||||
}
|
||||
@@ -1520,7 +1527,7 @@ export class OlmDevice {
|
||||
},
|
||||
logger.withPrefix("[getSharedHistoryInboundGroupSessionsForRoom]"),
|
||||
);
|
||||
return result;
|
||||
return result!;
|
||||
}
|
||||
|
||||
// Utilities
|
||||
|
||||
@@ -75,16 +75,32 @@ export enum RoomKeyRequestState {
|
||||
CancellationPendingAndWillResend,
|
||||
}
|
||||
|
||||
interface RequestMessageBase {
|
||||
requesting_device_id: string;
|
||||
request_id: string;
|
||||
}
|
||||
|
||||
interface RequestMessageRequest extends RequestMessageBase {
|
||||
action: "request";
|
||||
body: IRoomKeyRequestBody;
|
||||
}
|
||||
|
||||
interface RequestMessageCancellation extends RequestMessageBase {
|
||||
action: "request_cancellation";
|
||||
}
|
||||
|
||||
type RequestMessage = RequestMessageRequest | RequestMessageCancellation;
|
||||
|
||||
export class OutgoingRoomKeyRequestManager {
|
||||
// handle for the delayed call to sendOutgoingRoomKeyRequests. Non-null
|
||||
// if the callback has been set, or if it is still running.
|
||||
private sendOutgoingRoomKeyRequestsTimer: ReturnType<typeof setTimeout> = null;
|
||||
private sendOutgoingRoomKeyRequestsTimer?: ReturnType<typeof setTimeout>;
|
||||
|
||||
// sanity check to ensure that we don't end up with two concurrent runs
|
||||
// of sendOutgoingRoomKeyRequests
|
||||
private sendOutgoingRoomKeyRequestsRunning = false;
|
||||
|
||||
private clientRunning = false;
|
||||
private clientRunning = true;
|
||||
|
||||
constructor(
|
||||
private readonly baseApis: MatrixClient,
|
||||
@@ -92,13 +108,6 @@ export class OutgoingRoomKeyRequestManager {
|
||||
private readonly cryptoStore: CryptoStore,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Called when the client is started. Sets background processes running.
|
||||
*/
|
||||
public start(): void {
|
||||
this.clientRunning = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the client is stopped. Stops any running background processes.
|
||||
*/
|
||||
@@ -369,43 +378,42 @@ export class OutgoingRoomKeyRequestManager {
|
||||
// look for and send any queued requests. Runs itself recursively until
|
||||
// there are no more requests, or there is an error (in which case, the
|
||||
// timer will be restarted before the promise resolves).
|
||||
private sendOutgoingRoomKeyRequests(): Promise<void> {
|
||||
private async sendOutgoingRoomKeyRequests(): Promise<void> {
|
||||
if (!this.clientRunning) {
|
||||
this.sendOutgoingRoomKeyRequestsTimer = null;
|
||||
return Promise.resolve();
|
||||
this.sendOutgoingRoomKeyRequestsTimer = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
return this.cryptoStore.getOutgoingRoomKeyRequestByState([
|
||||
const req = await this.cryptoStore.getOutgoingRoomKeyRequestByState([
|
||||
RoomKeyRequestState.CancellationPending,
|
||||
RoomKeyRequestState.CancellationPendingAndWillResend,
|
||||
RoomKeyRequestState.Unsent,
|
||||
]).then((req: OutgoingRoomKeyRequest) => {
|
||||
if (!req) {
|
||||
this.sendOutgoingRoomKeyRequestsTimer = null;
|
||||
return;
|
||||
}
|
||||
]);
|
||||
|
||||
let prom;
|
||||
if (!req) {
|
||||
this.sendOutgoingRoomKeyRequestsTimer = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (req.state) {
|
||||
case RoomKeyRequestState.Unsent:
|
||||
prom = this.sendOutgoingRoomKeyRequest(req);
|
||||
await this.sendOutgoingRoomKeyRequest(req);
|
||||
break;
|
||||
case RoomKeyRequestState.CancellationPending:
|
||||
prom = this.sendOutgoingRoomKeyRequestCancellation(req);
|
||||
await this.sendOutgoingRoomKeyRequestCancellation(req);
|
||||
break;
|
||||
case RoomKeyRequestState.CancellationPendingAndWillResend:
|
||||
prom = this.sendOutgoingRoomKeyRequestCancellation(req, true);
|
||||
await this.sendOutgoingRoomKeyRequestCancellation(req, true);
|
||||
break;
|
||||
}
|
||||
|
||||
return prom.then(() => {
|
||||
// go around the loop again
|
||||
return this.sendOutgoingRoomKeyRequests();
|
||||
}).catch((e) => {
|
||||
logger.error("Error sending room key request; will retry later.", e);
|
||||
this.sendOutgoingRoomKeyRequestsTimer = null;
|
||||
});
|
||||
});
|
||||
// go around the loop again
|
||||
return this.sendOutgoingRoomKeyRequests();
|
||||
} catch (e) {
|
||||
logger.error("Error sending room key request; will retry later.", e);
|
||||
this.sendOutgoingRoomKeyRequestsTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// given a RoomKeyRequest, send it and update the request record
|
||||
@@ -416,16 +424,14 @@ export class OutgoingRoomKeyRequestManager {
|
||||
`(id ${req.requestId})`,
|
||||
);
|
||||
|
||||
const requestMessage = {
|
||||
const requestMessage: RequestMessage = {
|
||||
action: "request",
|
||||
requesting_device_id: this.deviceId,
|
||||
request_id: req.requestId,
|
||||
body: req.requestBody,
|
||||
};
|
||||
|
||||
return this.sendMessageToDevices(
|
||||
requestMessage, req.recipients, req.requestTxnId || req.requestId,
|
||||
).then(() => {
|
||||
return this.sendMessageToDevices(requestMessage, req.recipients, req.requestTxnId || req.requestId).then(() => {
|
||||
return this.cryptoStore.updateOutgoingRoomKeyRequest(
|
||||
req.requestId, RoomKeyRequestState.Unsent,
|
||||
{ state: RoomKeyRequestState.Sent },
|
||||
@@ -443,7 +449,7 @@ export class OutgoingRoomKeyRequestManager {
|
||||
`(cancellation id ${req.cancellationTxnId})`,
|
||||
);
|
||||
|
||||
const requestMessage = {
|
||||
const requestMessage: RequestMessage = {
|
||||
action: "request_cancellation",
|
||||
requesting_device_id: this.deviceId,
|
||||
request_id: req.requestId,
|
||||
@@ -467,7 +473,11 @@ export class OutgoingRoomKeyRequestManager {
|
||||
}
|
||||
|
||||
// send a RoomKeyRequest to a list of recipients
|
||||
private sendMessageToDevices(message, recipients, txnId: string): Promise<{}> {
|
||||
private sendMessageToDevices(
|
||||
message: RequestMessage,
|
||||
recipients: IRoomKeyRequestRecipient[],
|
||||
txnId?: string,
|
||||
): Promise<{}> {
|
||||
const contentMap: Record<string, Record<string, Record<string, any>>> = {};
|
||||
for (const recip of recipients) {
|
||||
if (!contentMap[recip.userId]) {
|
||||
@@ -480,15 +490,13 @@ export class OutgoingRoomKeyRequestManager {
|
||||
}
|
||||
}
|
||||
|
||||
function stringifyRequestBody(requestBody) {
|
||||
function stringifyRequestBody(requestBody: IRoomKeyRequestBody): string {
|
||||
// we assume that the request is for megolm keys, which are identified by
|
||||
// room id and session id
|
||||
return requestBody.room_id + " / " + requestBody.session_id;
|
||||
}
|
||||
|
||||
function stringifyRecipientList(recipients) {
|
||||
return '['
|
||||
+ recipients.map((r) => `${r.userId}:${r.deviceId}`).join(",")
|
||||
+ ']';
|
||||
function stringifyRecipientList(recipients: IRoomKeyRequestRecipient[]): string {
|
||||
return `[${recipients.map((r) => `${r.userId}:${r.deviceId}`).join(",")}]`;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,12 +38,12 @@ export class RoomList {
|
||||
// Object of roomId -> room e2e info object (body of the m.room.encryption event)
|
||||
private roomEncryption: Record<string, IRoomEncryption> = {};
|
||||
|
||||
constructor(private readonly cryptoStore: CryptoStore) {}
|
||||
constructor(private readonly cryptoStore?: CryptoStore) {}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
await this.cryptoStore.doTxn(
|
||||
await this.cryptoStore!.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
|
||||
this.cryptoStore.getEndToEndRooms(txn, (result) => {
|
||||
this.cryptoStore!.getEndToEndRooms(txn, (result) => {
|
||||
this.roomEncryption = result;
|
||||
});
|
||||
},
|
||||
@@ -63,9 +63,9 @@ export class RoomList {
|
||||
// as it prevents the Crypto::setRoomEncryption from calling
|
||||
// this twice for consecutive m.room.encryption events
|
||||
this.roomEncryption[roomId] = roomInfo;
|
||||
await this.cryptoStore.doTxn(
|
||||
await this.cryptoStore!.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
|
||||
this.cryptoStore.storeEndToEndRoom(roomId, roomInfo, txn);
|
||||
this.cryptoStore!.storeEndToEndRoom(roomId, roomInfo, txn);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+35
-45
@@ -19,10 +19,12 @@ import * as olmlib from './olmlib';
|
||||
import { encodeBase64 } from './olmlib';
|
||||
import { randomString } from '../randomstring';
|
||||
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from './aes';
|
||||
import { ClientEvent, ICryptoCallbacks, MatrixEvent } from '../matrix';
|
||||
import { ClientEventHandlerMap, MatrixClient } from "../client";
|
||||
import { ICryptoCallbacks } from ".";
|
||||
import { IContent, MatrixEvent } from "../models/event";
|
||||
import { ClientEvent, ClientEventHandlerMap, MatrixClient } from "../client";
|
||||
import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from './api';
|
||||
import { TypedEventEmitter } from '../models/typed-event-emitter';
|
||||
import { defer, IDeferred } from "../utils";
|
||||
|
||||
export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2";
|
||||
|
||||
@@ -39,15 +41,14 @@ export interface ISecretRequest {
|
||||
export interface IAccountDataClient extends TypedEventEmitter<ClientEvent.AccountData, ClientEventHandlerMap> {
|
||||
// Subset of MatrixClient (which also uses any for the event content)
|
||||
getAccountDataFromServer: <T extends {[k: string]: any}>(eventType: string) => Promise<T>;
|
||||
getAccountData: (eventType: string) => MatrixEvent;
|
||||
getAccountData: (eventType: string) => IContent | null;
|
||||
setAccountData: (eventType: string, content: any) => Promise<{}>;
|
||||
}
|
||||
|
||||
interface ISecretRequestInternal {
|
||||
name: string;
|
||||
devices: string[];
|
||||
resolve: (reason: string) => void;
|
||||
reject: (error: Error) => void;
|
||||
deferred: IDeferred<string>;
|
||||
}
|
||||
|
||||
interface IDecryptors {
|
||||
@@ -66,7 +67,7 @@ interface ISecretInfo {
|
||||
* Implements Secure Secret Storage and Sharing (MSC1946)
|
||||
* @module crypto/SecretStorage
|
||||
*/
|
||||
export class SecretStorage {
|
||||
export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
|
||||
private requests = new Map<string, ISecretRequestInternal>();
|
||||
|
||||
// In it's pure javascript days, this was relying on some proper Javascript-style
|
||||
@@ -80,7 +81,7 @@ export class SecretStorage {
|
||||
constructor(
|
||||
private readonly accountDataAdapter: IAccountDataClient,
|
||||
private readonly cryptoCallbacks: ICryptoCallbacks,
|
||||
private readonly baseApis?: MatrixClient,
|
||||
private readonly baseApis: B,
|
||||
) {}
|
||||
|
||||
public async getDefaultKeyId(): Promise<string | null> {
|
||||
@@ -129,13 +130,11 @@ export class SecretStorage {
|
||||
*/
|
||||
public async addKey(
|
||||
algorithm: string,
|
||||
opts: IAddSecretStorageKeyOpts,
|
||||
opts: IAddSecretStorageKeyOpts = {},
|
||||
keyId?: string,
|
||||
): Promise<SecretStorageKeyObject> {
|
||||
const keyInfo = { algorithm } as ISecretStorageKeyInfo;
|
||||
|
||||
if (!opts) opts = {} as IAddSecretStorageKeyOpts;
|
||||
|
||||
if (opts.name) {
|
||||
keyInfo.name = opts.name;
|
||||
}
|
||||
@@ -182,7 +181,7 @@ export class SecretStorage {
|
||||
* the form [keyId, keyInfo]. Otherwise, null is returned.
|
||||
* XXX: why is this an array when addKey returns an object?
|
||||
*/
|
||||
public async getKey(keyId: string): Promise<SecretStorageKeyTuple | null> {
|
||||
public async getKey(keyId?: string | null): Promise<SecretStorageKeyTuple | null> {
|
||||
if (!keyId) {
|
||||
keyId = await this.getDefaultKeyId();
|
||||
}
|
||||
@@ -237,7 +236,7 @@ export class SecretStorage {
|
||||
* @param {Array} keys The IDs of the keys to use to encrypt the secret
|
||||
* or null/undefined to use the default key.
|
||||
*/
|
||||
public async store(name: string, secret: string, keys?: string[]): Promise<void> {
|
||||
public async store(name: string, secret: string, keys?: string[] | null): Promise<void> {
|
||||
const encrypted: Record<string, IEncryptedPayload> = {};
|
||||
|
||||
if (!keys) {
|
||||
@@ -284,7 +283,7 @@ export class SecretStorage {
|
||||
*
|
||||
* @return {string} the contents of the secret
|
||||
*/
|
||||
public async get(name: string): Promise<string> {
|
||||
public async get(name: string): Promise<string | undefined> {
|
||||
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretInfo>(name);
|
||||
if (!secretInfo) {
|
||||
return;
|
||||
@@ -376,21 +375,11 @@ export class SecretStorage {
|
||||
* @param {string} name the name of the secret to request
|
||||
* @param {string[]} devices the devices to request the secret from
|
||||
*/
|
||||
public request(name: string, devices: string[]): ISecretRequest {
|
||||
public request(this: SecretStorage<MatrixClient>, name: string, devices: string[]): ISecretRequest {
|
||||
const requestId = this.baseApis.makeTxnId();
|
||||
|
||||
let resolve: (s: string) => void;
|
||||
let reject: (e: Error) => void;
|
||||
const promise = new Promise<string>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
this.requests.set(requestId, {
|
||||
name,
|
||||
devices,
|
||||
resolve,
|
||||
reject,
|
||||
});
|
||||
const deferred = defer<string>();
|
||||
this.requests.set(requestId, { name, devices, deferred });
|
||||
|
||||
const cancel = (reason: string) => {
|
||||
// send cancellation event
|
||||
@@ -404,12 +393,12 @@ export class SecretStorage {
|
||||
toDevice[device] = cancelData;
|
||||
}
|
||||
this.baseApis.sendToDevice("m.secret.request", {
|
||||
[this.baseApis.getUserId()]: toDevice,
|
||||
[this.baseApis.getUserId()!]: toDevice,
|
||||
});
|
||||
|
||||
// and reject the promise so that anyone waiting on it will be
|
||||
// notified
|
||||
reject(new Error(reason || "Cancelled"));
|
||||
deferred.reject(new Error(reason || "Cancelled"));
|
||||
};
|
||||
|
||||
// send request to devices
|
||||
@@ -425,22 +414,23 @@ export class SecretStorage {
|
||||
}
|
||||
logger.info(`Request secret ${name} from ${devices}, id ${requestId}`);
|
||||
this.baseApis.sendToDevice("m.secret.request", {
|
||||
[this.baseApis.getUserId()]: toDevice,
|
||||
[this.baseApis.getUserId()!]: toDevice,
|
||||
});
|
||||
|
||||
return {
|
||||
requestId,
|
||||
promise,
|
||||
promise: deferred.promise,
|
||||
cancel,
|
||||
};
|
||||
}
|
||||
|
||||
public async onRequestReceived(event: MatrixEvent): Promise<void> {
|
||||
public async onRequestReceived(this: SecretStorage<MatrixClient>, event: MatrixEvent): Promise<void> {
|
||||
const sender = event.getSender();
|
||||
const content = event.getContent();
|
||||
if (sender !== this.baseApis.getUserId()
|
||||
|| !(content.name && content.action
|
||||
&& content.requesting_device_id && content.request_id)) {
|
||||
&& content.requesting_device_id && content.request_id)
|
||||
) {
|
||||
// ignore requests from anyone else, for now
|
||||
return;
|
||||
}
|
||||
@@ -498,25 +488,25 @@ export class SecretStorage {
|
||||
};
|
||||
const encryptedContent = {
|
||||
algorithm: olmlib.OLM_ALGORITHM,
|
||||
sender_key: this.baseApis.crypto.olmDevice.deviceCurve25519Key,
|
||||
sender_key: this.baseApis.crypto!.olmDevice.deviceCurve25519Key,
|
||||
ciphertext: {},
|
||||
};
|
||||
await olmlib.ensureOlmSessionsForDevices(
|
||||
this.baseApis.crypto.olmDevice,
|
||||
this.baseApis.crypto!.olmDevice,
|
||||
this.baseApis,
|
||||
{
|
||||
[sender]: [
|
||||
this.baseApis.getStoredDevice(sender, deviceId),
|
||||
this.baseApis.getStoredDevice(sender, deviceId)!,
|
||||
],
|
||||
},
|
||||
);
|
||||
await olmlib.encryptMessageForDevice(
|
||||
encryptedContent.ciphertext,
|
||||
this.baseApis.getUserId(),
|
||||
this.baseApis.deviceId,
|
||||
this.baseApis.crypto.olmDevice,
|
||||
this.baseApis.getUserId()!,
|
||||
this.baseApis.deviceId!,
|
||||
this.baseApis.crypto!.olmDevice,
|
||||
sender,
|
||||
this.baseApis.getStoredDevice(sender, deviceId),
|
||||
this.baseApis.getStoredDevice(sender, deviceId)!,
|
||||
payload,
|
||||
);
|
||||
const contentMap = {
|
||||
@@ -533,7 +523,7 @@ export class SecretStorage {
|
||||
}
|
||||
}
|
||||
|
||||
public onSecretReceived(event: MatrixEvent): void {
|
||||
public onSecretReceived(this: SecretStorage<MatrixClient>, event: MatrixEvent): void {
|
||||
if (event.getSender() !== this.baseApis.getUserId()) {
|
||||
// we shouldn't be receiving secrets from anyone else, so ignore
|
||||
// because someone could be trying to send us bogus data
|
||||
@@ -547,7 +537,7 @@ export class SecretStorage {
|
||||
|
||||
const content = event.getContent();
|
||||
|
||||
const senderKeyUser = this.baseApis.crypto.deviceList.getUserByIdentityKey(
|
||||
const senderKeyUser = this.baseApis.crypto!.deviceList.getUserByIdentityKey(
|
||||
olmlib.OLM_ALGORITHM,
|
||||
event.getSenderKey() || "",
|
||||
);
|
||||
@@ -561,9 +551,9 @@ export class SecretStorage {
|
||||
if (requestControl) {
|
||||
// make sure that the device that sent it is one of the devices that
|
||||
// we requested from
|
||||
const deviceInfo = this.baseApis.crypto.deviceList.getDeviceByIdentityKey(
|
||||
const deviceInfo = this.baseApis.crypto!.deviceList.getDeviceByIdentityKey(
|
||||
olmlib.OLM_ALGORITHM,
|
||||
event.getSenderKey(),
|
||||
event.getSenderKey()!,
|
||||
);
|
||||
if (!deviceInfo) {
|
||||
logger.log(
|
||||
@@ -578,7 +568,7 @@ export class SecretStorage {
|
||||
// unsure that the sender is trusted. In theory, this check is
|
||||
// unnecessary since we only accept secret shares from devices that
|
||||
// we requested from, but it doesn't hurt.
|
||||
const deviceTrust = this.baseApis.crypto.checkDeviceInfoTrust(event.getSender(), deviceInfo);
|
||||
const deviceTrust = this.baseApis.crypto!.checkDeviceInfoTrust(event.getSender()!, deviceInfo);
|
||||
if (!deviceTrust.isVerified()) {
|
||||
logger.log("secret share from unverified device");
|
||||
return;
|
||||
@@ -588,7 +578,7 @@ export class SecretStorage {
|
||||
`Successfully received secret ${requestControl.name} ` +
|
||||
`from ${deviceInfo.deviceId}`,
|
||||
);
|
||||
requestControl.resolve(content.secret);
|
||||
requestControl.deferred.resolve(content.secret);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ import { IRoomEncryption } from "../RoomList";
|
||||
*/
|
||||
export const ENCRYPTION_CLASSES = new Map<string, new (params: IParams) => EncryptionAlgorithm>();
|
||||
|
||||
type DecryptionClassParams = Omit<IParams, "deviceId" | "config">;
|
||||
export type DecryptionClassParams<P extends IParams = IParams> = Omit<P, "deviceId" | "config">;
|
||||
|
||||
/**
|
||||
* map of registered encryption algorithm classes. Map from string to {@link
|
||||
@@ -52,7 +52,7 @@ export interface IParams {
|
||||
crypto: Crypto;
|
||||
olmDevice: OlmDevice;
|
||||
baseApis: MatrixClient;
|
||||
roomId: string;
|
||||
roomId?: string;
|
||||
config: IRoomEncryption & object;
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ export abstract class EncryptionAlgorithm {
|
||||
protected readonly crypto: Crypto;
|
||||
protected readonly olmDevice: OlmDevice;
|
||||
protected readonly baseApis: MatrixClient;
|
||||
protected readonly roomId: string;
|
||||
protected readonly roomId?: string;
|
||||
|
||||
constructor(params: IParams) {
|
||||
this.userId = params.userId;
|
||||
@@ -148,7 +148,7 @@ export abstract class DecryptionAlgorithm {
|
||||
protected readonly crypto: Crypto;
|
||||
protected readonly olmDevice: OlmDevice;
|
||||
protected readonly baseApis: MatrixClient;
|
||||
protected readonly roomId: string;
|
||||
protected readonly roomId?: string;
|
||||
|
||||
constructor(params: DecryptionClassParams) {
|
||||
this.userId = params.userId;
|
||||
@@ -242,7 +242,7 @@ export abstract class DecryptionAlgorithm {
|
||||
export class DecryptionError extends Error {
|
||||
public readonly detailedString: string;
|
||||
|
||||
constructor(public readonly code: string, msg: string, details?: Record<string, string>) {
|
||||
constructor(public readonly code: string, msg: string, details?: Record<string, string | Error>) {
|
||||
super(msg);
|
||||
this.code = code;
|
||||
this.name = 'DecryptionError';
|
||||
@@ -250,7 +250,7 @@ export class DecryptionError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
function detailedStringForDecryptionError(err: DecryptionError, details?: Record<string, string>): string {
|
||||
function detailedStringForDecryptionError(err: DecryptionError, details?: Record<string, string | Error>): string {
|
||||
let result = err.name + '[msg: ' + err.message;
|
||||
|
||||
if (details) {
|
||||
@@ -272,7 +272,11 @@ function detailedStringForDecryptionError(err: DecryptionError, details?: Record
|
||||
* @extends Error
|
||||
*/
|
||||
export class UnknownDeviceError extends Error {
|
||||
constructor(msg: string, public readonly devices: Record<string, Record<string, object>>) {
|
||||
constructor(
|
||||
msg: string,
|
||||
public readonly devices: Record<string, Record<string, object>>,
|
||||
public event?: MatrixEvent,
|
||||
) {
|
||||
super(msg);
|
||||
this.name = "UnknownDeviceError";
|
||||
this.devices = devices;
|
||||
@@ -292,11 +296,11 @@ export class UnknownDeviceError extends Error {
|
||||
* module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm}
|
||||
* implementation
|
||||
*/
|
||||
export function registerAlgorithm(
|
||||
export function registerAlgorithm<P extends IParams = IParams>(
|
||||
algorithm: string,
|
||||
encryptor: new (params: IParams) => EncryptionAlgorithm,
|
||||
decryptor: new (params: Omit<IParams, "deviceId">) => DecryptionAlgorithm,
|
||||
encryptor: new (params: P) => EncryptionAlgorithm,
|
||||
decryptor: new (params: DecryptionClassParams<P>) => DecryptionAlgorithm,
|
||||
): void {
|
||||
ENCRYPTION_CLASSES.set(algorithm, encryptor);
|
||||
DECRYPTION_CLASSES.set(algorithm, decryptor);
|
||||
ENCRYPTION_CLASSES.set(algorithm, encryptor as new (params: IParams) => EncryptionAlgorithm);
|
||||
DECRYPTION_CLASSES.set(algorithm, decryptor as new (params: DecryptionClassParams) => DecryptionAlgorithm);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import { logger } from '../../logger';
|
||||
import * as olmlib from "../olmlib";
|
||||
import {
|
||||
DecryptionAlgorithm,
|
||||
DecryptionClassParams,
|
||||
DecryptionError,
|
||||
EncryptionAlgorithm,
|
||||
IParams,
|
||||
@@ -36,9 +37,11 @@ import { DeviceInfo } from "../deviceinfo";
|
||||
import { IOlmSessionResult } from "../olmlib";
|
||||
import { DeviceInfoMap } from "../DeviceList";
|
||||
import { MatrixEvent } from "../../models/event";
|
||||
import { IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index";
|
||||
import { EventType, MsgType } from '../../@types/event';
|
||||
import { IEncryptedContent, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index";
|
||||
import { RoomKeyRequestState } from '../OutgoingRoomKeyRequestManager';
|
||||
import { OlmGroupSessionExtraData } from "../../@types/crypto";
|
||||
import { MatrixError } from "../../http-api";
|
||||
|
||||
// determine whether the key can be shared with invitees
|
||||
export function isRoomSharedHistory(room: Room): boolean {
|
||||
@@ -102,12 +105,6 @@ interface IPayload extends Partial<IMessage> {
|
||||
algorithm?: string;
|
||||
sender_key?: string;
|
||||
}
|
||||
|
||||
interface IEncryptedContent {
|
||||
algorithm: string;
|
||||
sender_key: string;
|
||||
ciphertext: Record<string, string>;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
interface SharedWithData {
|
||||
@@ -249,8 +246,11 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
startTime: number;
|
||||
};
|
||||
|
||||
constructor(params: IParams) {
|
||||
protected readonly roomId: string;
|
||||
|
||||
constructor(params: IParams & Required<Pick<IParams, "roomId">>) {
|
||||
super(params);
|
||||
this.roomId = params.roomId;
|
||||
|
||||
this.sessionRotationPeriodMsgs = params.config?.rotation_period_msgs ?? 100;
|
||||
this.sessionRotationPeriodMs = params.config?.rotation_period_ms ?? 7 * 24 * 3600 * 1000;
|
||||
@@ -491,13 +491,13 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
const key = this.olmDevice.getOutboundGroupSessionKey(sessionId);
|
||||
|
||||
await this.olmDevice.addInboundGroupSession(
|
||||
this.roomId, this.olmDevice.deviceCurve25519Key, [], sessionId,
|
||||
key.key, { ed25519: this.olmDevice.deviceEd25519Key }, false,
|
||||
this.roomId, this.olmDevice.deviceCurve25519Key!, [], sessionId,
|
||||
key.key, { ed25519: this.olmDevice.deviceEd25519Key! }, false,
|
||||
{ sharedHistory },
|
||||
);
|
||||
|
||||
// don't wait for it to complete
|
||||
this.crypto.backupManager.backupGroupSession(this.olmDevice.deviceCurve25519Key, sessionId);
|
||||
this.crypto.backupManager.backupGroupSession(this.olmDevice.deviceCurve25519Key!, sessionId);
|
||||
|
||||
return new OutboundSessionInfo(sessionId, sharedHistory);
|
||||
}
|
||||
@@ -928,7 +928,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
room_id: this.roomId,
|
||||
session_id: session.sessionId,
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
sender_key: this.olmDevice.deviceCurve25519Key,
|
||||
sender_key: this.olmDevice.deviceCurve25519Key!,
|
||||
};
|
||||
|
||||
const userDeviceMaps = this.splitDevices(devicesByUser);
|
||||
@@ -975,7 +975,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
logger.debug(`Getting devices in ${this.roomId}`);
|
||||
const [devicesInRoom, blocked] = await this.getDevicesInRoom(room);
|
||||
|
||||
if (this.crypto.getGlobalErrorOnUnknownDevices()) {
|
||||
if (this.crypto.globalErrorOnUnknownDevices) {
|
||||
// Drop unknown devices for now. When the message gets sent, we'll
|
||||
// throw an error, but we'll still be prepared to send to the known
|
||||
// devices.
|
||||
@@ -1019,11 +1019,16 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
}
|
||||
}
|
||||
|
||||
const [devicesInRoom, blocked] = await this.getDevicesInRoom(room);
|
||||
/**
|
||||
* When using in-room messages and the room has encryption enabled,
|
||||
* clients should ensure that encryption does not hinder the verification.
|
||||
*/
|
||||
const forceDistributeToUnverified = this.isVerificationEvent(eventType, content);
|
||||
const [devicesInRoom, blocked] = await this.getDevicesInRoom(room, forceDistributeToUnverified);
|
||||
|
||||
// check if any of these devices are not yet known to the user.
|
||||
// if so, warn the user so they can verify or ignore.
|
||||
if (this.crypto.getGlobalErrorOnUnknownDevices()) {
|
||||
if (this.crypto.globalErrorOnUnknownDevices) {
|
||||
this.checkForUnknownDevices(devicesInRoom);
|
||||
}
|
||||
|
||||
@@ -1053,6 +1058,26 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
return encryptedContent;
|
||||
}
|
||||
|
||||
private isVerificationEvent(eventType: string, content: object): boolean {
|
||||
switch (eventType) {
|
||||
case EventType.KeyVerificationCancel:
|
||||
case EventType.KeyVerificationDone:
|
||||
case EventType.KeyVerificationMac:
|
||||
case EventType.KeyVerificationStart:
|
||||
case EventType.KeyVerificationKey:
|
||||
case EventType.KeyVerificationReady:
|
||||
case EventType.KeyVerificationAccept: {
|
||||
return true;
|
||||
}
|
||||
case EventType.RoomMessage: {
|
||||
return content['msgtype'] === MsgType.KeyVerificationRequest;
|
||||
}
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces the current outbound group session to be discarded such
|
||||
* that another one will be created next time an event is sent.
|
||||
@@ -1119,6 +1144,8 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
* Get the list of unblocked devices for all users in the room
|
||||
*
|
||||
* @param {module:models/room} room
|
||||
* @param forceDistributeToUnverified if set to true will include the unverified devices
|
||||
* even if setting is set to block them (useful for verification)
|
||||
*
|
||||
* @return {Promise} Promise which resolves to an array whose
|
||||
* first element is a map from userId to deviceId to deviceInfo indicating
|
||||
@@ -1126,14 +1153,17 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
* element is a map from userId to deviceId to data indicating the devices
|
||||
* that are in the room but that have been blocked
|
||||
*/
|
||||
private async getDevicesInRoom(room: Room): Promise<[DeviceInfoMap, IBlockedMap]> {
|
||||
private async getDevicesInRoom(
|
||||
room: Room,
|
||||
forceDistributeToUnverified = false,
|
||||
): Promise<[DeviceInfoMap, IBlockedMap]> {
|
||||
const members = await room.getEncryptionTargetMembers();
|
||||
const roomMembers = members.map(function(u) {
|
||||
return u.userId;
|
||||
});
|
||||
|
||||
// The global value is treated as a default for when rooms don't specify a value.
|
||||
let isBlacklisting = this.crypto.getGlobalBlacklistUnverifiedDevices();
|
||||
let isBlacklisting = this.crypto.globalBlacklistUnverifiedDevices;
|
||||
const isRoomBlacklisting = room.getBlacklistUnverifiedDevices();
|
||||
if (typeof isRoomBlacklisting === 'boolean') {
|
||||
isBlacklisting = isRoomBlacklisting;
|
||||
@@ -1162,7 +1192,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
const deviceTrust = this.crypto.checkDeviceTrust(userId, deviceId);
|
||||
|
||||
if (userDevices[deviceId].isBlocked() ||
|
||||
(!deviceTrust.isVerified() && isBlacklisting)
|
||||
(!deviceTrust.isVerified() && isBlacklisting && !forceDistributeToUnverified)
|
||||
) {
|
||||
if (!blocked[userId]) {
|
||||
blocked[userId] = {};
|
||||
@@ -1200,6 +1230,13 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
// this gets stubbed out by the unit tests.
|
||||
private olmlib = olmlib;
|
||||
|
||||
protected readonly roomId: string;
|
||||
|
||||
constructor(params: DecryptionClassParams<IParams & Required<Pick<IParams, "roomId">>>) {
|
||||
super(params);
|
||||
this.roomId = params.roomId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
@@ -1229,21 +1266,21 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
// (fixes https://github.com/vector-im/element-web/issues/5001)
|
||||
this.addEventToPendingList(event);
|
||||
|
||||
let res: IDecryptedGroupMessage;
|
||||
let res: IDecryptedGroupMessage | null;
|
||||
try {
|
||||
res = await this.olmDevice.decryptGroupMessage(
|
||||
event.getRoomId(), content.sender_key, content.session_id, content.ciphertext,
|
||||
event.getId(), event.getTs(),
|
||||
event.getRoomId()!, content.sender_key, content.session_id, content.ciphertext,
|
||||
event.getId()!, event.getTs(),
|
||||
);
|
||||
} catch (e) {
|
||||
if (e.name === "DecryptionError") {
|
||||
if ((<Error>e).name === "DecryptionError") {
|
||||
// re-throw decryption errors as-is
|
||||
throw e;
|
||||
}
|
||||
|
||||
let errorCode = "OLM_DECRYPT_GROUP_MESSAGE_ERROR";
|
||||
|
||||
if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
|
||||
if ((<MatrixError>e)?.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
|
||||
this.requestKeysForEvent(event);
|
||||
|
||||
errorCode = 'OLM_UNKNOWN_MESSAGE_INDEX';
|
||||
@@ -1333,7 +1370,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
const recipients = event.getKeyRequestRecipients(this.userId);
|
||||
|
||||
this.crypto.requestRoomKey({
|
||||
room_id: event.getRoomId(),
|
||||
room_id: event.getRoomId()!,
|
||||
algorithm: wireContent.algorithm,
|
||||
sender_key: wireContent.sender_key,
|
||||
session_id: wireContent.session_id,
|
||||
@@ -1354,7 +1391,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
if (!this.pendingEvents.has(senderKey)) {
|
||||
this.pendingEvents.set(senderKey, new Map<string, Set<MatrixEvent>>());
|
||||
}
|
||||
const senderPendingEvents = this.pendingEvents.get(senderKey);
|
||||
const senderPendingEvents = this.pendingEvents.get(senderKey)!;
|
||||
if (!senderPendingEvents.has(sessionId)) {
|
||||
senderPendingEvents.set(sessionId, new Set());
|
||||
}
|
||||
@@ -1380,9 +1417,9 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
|
||||
pendingEvents.delete(event);
|
||||
if (pendingEvents.size === 0) {
|
||||
senderPendingEvents.delete(sessionId);
|
||||
senderPendingEvents!.delete(sessionId);
|
||||
}
|
||||
if (senderPendingEvents.size === 0) {
|
||||
if (senderPendingEvents!.size === 0) {
|
||||
this.pendingEvents.delete(senderKey);
|
||||
}
|
||||
}
|
||||
@@ -1394,7 +1431,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
*/
|
||||
public async onRoomKeyEvent(event: MatrixEvent): Promise<void> {
|
||||
const content = event.getContent<Partial<IMessage["content"]>>();
|
||||
let senderKey = event.getSenderKey();
|
||||
let senderKey = event.getSenderKey()!;
|
||||
let forwardingKeyChain: string[] = [];
|
||||
let exportFormat = false;
|
||||
let keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>;
|
||||
@@ -1424,7 +1461,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
olmlib.OLM_ALGORITHM,
|
||||
senderKey,
|
||||
);
|
||||
const senderKeyUser = this.baseApis.crypto.deviceList.getUserByIdentityKey(
|
||||
const senderKeyUser = this.baseApis.crypto!.deviceList.getUserByIdentityKey(
|
||||
olmlib.OLM_ALGORITHM,
|
||||
senderKey,
|
||||
);
|
||||
@@ -1433,7 +1470,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
return;
|
||||
}
|
||||
const outgoingRequests = deviceInfo ? await this.crypto.cryptoStore.getOutgoingRoomKeyRequestsByTarget(
|
||||
event.getSender(), deviceInfo.deviceId, [RoomKeyRequestState.Sent],
|
||||
event.getSender()!, deviceInfo.deviceId, [RoomKeyRequestState.Sent],
|
||||
) : [];
|
||||
const weRequested = outgoingRequests.some((req) => (
|
||||
req.requestBody.room_id === content.room_id && req.requestBody.session_id === content.session_id
|
||||
@@ -1493,7 +1530,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
// that room later
|
||||
if (!room) {
|
||||
const parkedData = {
|
||||
senderId: event.getSender(),
|
||||
senderId: event.getSender()!,
|
||||
senderKey: content.sender_key,
|
||||
sessionId: content.session_id,
|
||||
sessionKey: content.session_key,
|
||||
@@ -1503,14 +1540,17 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
await this.crypto.cryptoStore.doTxn(
|
||||
'readwrite',
|
||||
['parked_shared_history'],
|
||||
(txn) => this.crypto.cryptoStore.addParkedSharedHistory(content.room_id, parkedData, txn),
|
||||
(txn) => this.crypto.cryptoStore.addParkedSharedHistory(content.room_id!, parkedData, txn),
|
||||
logger.withPrefix("[addParkedSharedHistory]"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const sendingDevice = this.crypto.deviceList.getDeviceByIdentityKey(olmlib.OLM_ALGORITHM, senderKey);
|
||||
const deviceTrust = this.crypto.checkDeviceInfoTrust(event.getSender(), sendingDevice);
|
||||
const sendingDevice = this.crypto.deviceList.getDeviceByIdentityKey(
|
||||
olmlib.OLM_ALGORITHM,
|
||||
senderKey,
|
||||
) ?? undefined;
|
||||
const deviceTrust = this.crypto.checkDeviceInfoTrust(event.getSender()!, sendingDevice);
|
||||
|
||||
if (fromUs && !deviceTrust.isVerified()) {
|
||||
return;
|
||||
@@ -1574,7 +1614,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
const senderKey = content.sender_key;
|
||||
|
||||
if (content.code === "m.no_olm") {
|
||||
const sender = event.getSender();
|
||||
const sender = event.getSender()!;
|
||||
logger.warn(
|
||||
`${sender}:${senderKey} was unable to establish an olm session with us`,
|
||||
);
|
||||
@@ -1614,6 +1654,9 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: switch this to use encryptAndSendToDevices() rather than duplicating it?
|
||||
|
||||
await olmlib.ensureOlmSessionsForDevices(
|
||||
this.olmDevice, this.baseApis, { [sender]: [device] }, false,
|
||||
);
|
||||
@@ -1668,9 +1711,11 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
public shareKeysWithDevice(keyRequest: IncomingRoomKeyRequest): void {
|
||||
const userId = keyRequest.userId;
|
||||
const deviceId = keyRequest.deviceId;
|
||||
const deviceInfo = this.crypto.getStoredDevice(userId, deviceId);
|
||||
const deviceInfo = this.crypto.getStoredDevice(userId, deviceId)!;
|
||||
const body = keyRequest.requestBody;
|
||||
|
||||
// XXX: switch this to use encryptAndSendToDevices()?
|
||||
|
||||
this.olmlib.ensureOlmSessionsForDevices(
|
||||
this.olmDevice, this.baseApis, {
|
||||
[userId]: [deviceInfo],
|
||||
@@ -1709,7 +1754,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
this.olmDevice,
|
||||
userId,
|
||||
deviceInfo,
|
||||
payload,
|
||||
payload!,
|
||||
).then(() => {
|
||||
const contentMap = {
|
||||
[userId]: {
|
||||
@@ -1736,12 +1781,12 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
"algorithm": olmlib.MEGOLM_ALGORITHM,
|
||||
"room_id": roomId,
|
||||
"sender_key": senderKey,
|
||||
"sender_claimed_ed25519_key": key.sender_claimed_ed25519_key,
|
||||
"sender_claimed_ed25519_key": key!.sender_claimed_ed25519_key!,
|
||||
"session_id": sessionId,
|
||||
"session_key": key.key,
|
||||
"chain_index": key.chain_index,
|
||||
"forwarding_curve25519_key_chain": key.forwarding_curve25519_key_chain,
|
||||
"org.matrix.msc3061.shared_history": key.shared_history || false,
|
||||
"session_key": key!.key,
|
||||
"chain_index": key!.chain_index,
|
||||
"forwarding_curve25519_key_chain": key!.forwarding_curve25519_key_chain,
|
||||
"org.matrix.msc3061.shared_history": key!.shared_history || false,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1864,6 +1909,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
for (const [senderKey, sessionId] of sharedHistorySessions) {
|
||||
const payload = await this.buildKeyForwardingMessage(this.roomId, senderKey, sessionId);
|
||||
|
||||
// FIXME: use encryptAndSendToDevices() rather than duplicating it here.
|
||||
const promises: Promise<unknown>[] = [];
|
||||
const contentMap: Record<string, Record<string, IEncryptedContent>> = {};
|
||||
for (const [userId, devices] of Object.entries(devicesByUser)) {
|
||||
@@ -1871,7 +1917,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
for (const deviceInfo of devices) {
|
||||
const encryptedContent: IEncryptedContent = {
|
||||
algorithm: olmlib.OLM_ALGORITHM,
|
||||
sender_key: this.olmDevice.deviceCurve25519Key,
|
||||
sender_key: this.olmDevice.deviceCurve25519Key!,
|
||||
ciphertext: {},
|
||||
};
|
||||
contentMap[userId][deviceInfo.deviceId] = encryptedContent;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user