Compare commits
262 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e15cf9976f | |||
| bdc3926417 | |||
| 1f58ee7f2c | |||
| 2e28e9117a | |||
| a58a36e062 | |||
| 6cf6a0c522 | |||
| 02aa3edda4 | |||
| e04ea02c62 | |||
| 64197bf4db | |||
| c309fe6942 | |||
| 8408f36c12 | |||
| dcd8f91e02 | |||
| cabe14d7e2 | |||
| c019f2bb19 | |||
| baeb4acddf | |||
| 4a6e9a0f8f | |||
| ea5ce8d1d8 | |||
| 41854918a5 | |||
| 495642d041 | |||
| c7210b9e9d | |||
| 83563c7a01 | |||
| 2fcc4811dd | |||
| c392bc455d | |||
| b8711f15fd | |||
| 81f3aef960 | |||
| d6b8332567 | |||
| 85b34b46c5 | |||
| 4179f2978d | |||
| 97df6db49c | |||
| 3e693fab23 | |||
| a34d06c7c2 | |||
| 7b10fa367d | |||
| 7f5d7091de | |||
| 96f673ae92 | |||
| 79faee7a67 | |||
| 89d2984432 | |||
| bc78784688 | |||
| eb058edb1b | |||
| 4847d78b42 | |||
| 94f1eda830 | |||
| bc2a182ee9 | |||
| 789aec732a | |||
| 3246114772 | |||
| 39cf1863f1 | |||
| aa1e118f18 | |||
| c10152e098 | |||
| 6b7efbcd91 | |||
| d23c3cb8b2 | |||
| 9e37980e2d | |||
| de176dbd66 | |||
| ac10b40f67 | |||
| f35298a326 | |||
| d7bf0f85c0 | |||
| 1d87f5b163 | |||
| 3e97067b3e | |||
| 3ce582d004 | |||
| 3e48c76a77 | |||
| 8e29f8ead0 | |||
| 185ded4ebc | |||
| 3564a3546f | |||
| c6090325b3 | |||
| 999e355136 | |||
| 7de4164444 | |||
| e2ce379b56 | |||
| 424212cd65 | |||
| c7c16256df | |||
| 8a4c95ee72 | |||
| c3d422f5fb | |||
| fdb80ad259 | |||
| 981acf0044 | |||
| f00f70bfb8 | |||
| 12cc7be31c | |||
| 628bcbf33a | |||
| c4ca0b2e07 | |||
| d7442147b9 | |||
| b1566ee540 | |||
| ca98d9ff11 | |||
| bba4a35665 | |||
| 896f6227a0 | |||
| 4d10cf3074 | |||
| bb23df9423 | |||
| d02559cf3c | |||
| ec6272aa3d | |||
| 695b773f8b | |||
| 030abe1563 | |||
| 22f10f71b8 | |||
| 9ca3e7272e | |||
| 64119ef915 | |||
| 9ac7165e99 | |||
| 6168cedf32 | |||
| 7c34deecb6 | |||
| cef5507ab1 | |||
| 9b372d23ca | |||
| e9fef19c8f | |||
| 9ebc91aa5a | |||
| fcb75d547e | |||
| 33c9af952e | |||
| 3b66b28e71 | |||
| 7d37bb1edb | |||
| 0f717a9306 | |||
| ce776b9989 | |||
| ff1b0e51ea | |||
| 21e66a5c34 | |||
| aead401005 | |||
| af9525ed5f | |||
| 1ebcac37cc | |||
| 51a4cc5e18 | |||
| 48baa6315c | |||
| efc87d8084 | |||
| aab873fc58 | |||
| 52ed04c825 | |||
| fdd1428e19 | |||
| ec2405ac99 | |||
| b31712f2ff | |||
| 61e2606bc4 | |||
| 45f6c5b079 | |||
| b83c372848 | |||
| 6d58a54039 | |||
| 3872c5f099 | |||
| 4f86eee250 | |||
| 618242ef3c | |||
| 96ee5b1256 | |||
| 8af0ff111b | |||
| a04800f030 | |||
| 4683fbe848 | |||
| c973b26fa2 | |||
| 7b96c730b8 | |||
| f8bf6083de | |||
| 447319737a | |||
| 39e127b4e3 | |||
| 15ef8fabb7 | |||
| df42014ef5 | |||
| b765b18381 | |||
| 6cc8e4436c | |||
| 7f1fe46c7c | |||
| b2a10e6db3 | |||
| a0aa5074ed | |||
| 70a033c2fd | |||
| fcf12b49e3 | |||
| 284a475dfb | |||
| 193c38523c | |||
| 11ac3d9e58 | |||
| 071d5e71e4 | |||
| acd220b8a9 | |||
| 74b30246d0 | |||
| 9c17eb6c14 | |||
| 8293011ee2 | |||
| 4c5f416b32 | |||
| 66c678e9fb | |||
| 8a892ede23 | |||
| 2dd06e368e | |||
| fc501de081 | |||
| ada401f4c0 | |||
| 41d762171e | |||
| 5b6bebc1d7 | |||
| 7b5e137ec0 | |||
| 6e0901258c | |||
| 559fbdda26 | |||
| 72dac9a107 | |||
| 349c2c2587 | |||
| 08a9073bd5 | |||
| 9841f92415 | |||
| ed91bd9c11 | |||
| c953fc9fb7 | |||
| bf78a64d82 | |||
| ae849fdd46 | |||
| 39cf212628 | |||
| 224e592701 | |||
| 16d791b038 | |||
| c4006d752a | |||
| a9e7a46c56 | |||
| 4a7365f32f | |||
| a071a82a03 | |||
| 8d018f9c2d | |||
| 6f81371e61 | |||
| ccab6985ad | |||
| 569adc7b0c | |||
| b6369cc2bd | |||
| e6e079f487 | |||
| 683e7fba4a | |||
| 2c8eece5ca | |||
| 4a4d493856 | |||
| 11d8f562c5 | |||
| 7799804762 | |||
| 83a1e07380 | |||
| 7f3123ed65 | |||
| 5a88a6c62a | |||
| 8a7fd270e4 | |||
| 12cecbdcf1 | |||
| 53a45a34df | |||
| fa2eeac5b8 | |||
| 720248466f | |||
| 43bfa0c020 | |||
| c17deb0806 | |||
| 79ccd7c330 | |||
| 9de9ff76b5 | |||
| c0090852ad | |||
| 3870e3395d | |||
| a0f3e5d3bf | |||
| 720ea0e12e | |||
| 9a98e8008f | |||
| ad8bb5d2cd | |||
| 9a731cdf4f | |||
| d1ede036e2 | |||
| d3f08fec03 | |||
| d692a5dbe2 | |||
| 4362297edc | |||
| 1606274c36 | |||
| 8d6d262e5f | |||
| 3577aa98b5 | |||
| e0df53c2ed | |||
| 6611cfa253 | |||
| 25296bb486 | |||
| 31c4f6c16b | |||
| 22271d22f8 | |||
| 3a1897629a | |||
| 9d3ac66cf8 | |||
| a4ad4ed2cf | |||
| 7fd55a61bf | |||
| 847766c114 | |||
| c8c39052a7 | |||
| 6592b2c205 | |||
| fc91153be4 | |||
| 5511a6ef8c | |||
| 19e02e894f | |||
| c54d61e158 | |||
| 44da9040f4 | |||
| 995f5bf7d7 | |||
| ad16b26247 | |||
| aaf3702c66 | |||
| 74147b9943 | |||
| 815370c5f9 | |||
| a01d8e3174 | |||
| 007b7dd242 | |||
| 77d6def1cc | |||
| b318a77ece | |||
| 1a90259326 | |||
| f46ecf970c | |||
| dd98d7eb2c | |||
| f3dc1c4ca2 | |||
| 305b83f8ea | |||
| acc488da64 | |||
| 7217f83db9 | |||
| 37ea905faa | |||
| 78de55b835 | |||
| cb410f463a | |||
| 72f9d5e6f9 | |||
| c389de98f3 | |||
| 20745dc9ac | |||
| 9410902049 | |||
| a6badbb7fa | |||
| 0b65b199e3 | |||
| c1138bc085 | |||
| c0f7df8c3b | |||
| e085609572 | |||
| 0a4f86a79e | |||
| 5d6ff6c7f9 | |||
| ffcdfe166e | |||
| e1aa7d335b | |||
| 29643e745c | |||
| 54622ce424 | |||
| ca2ae24d46 |
@@ -1,12 +1,15 @@
|
||||
{
|
||||
"sourceMaps": true,
|
||||
"presets": [
|
||||
["@babel/preset-env", {
|
||||
"targets": {
|
||||
"node": 10
|
||||
},
|
||||
"modules": "commonjs"
|
||||
}],
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"targets": {
|
||||
"node": 10
|
||||
},
|
||||
"modules": "commonjs"
|
||||
}
|
||||
],
|
||||
"@babel/preset-typescript"
|
||||
],
|
||||
"plugins": [
|
||||
|
||||
+79
-53
@@ -1,12 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
"matrix-org",
|
||||
"import",
|
||||
],
|
||||
extends: [
|
||||
"plugin:matrix-org/babel",
|
||||
"plugin:import/typescript",
|
||||
],
|
||||
plugins: ["matrix-org", "import", "jsdoc"],
|
||||
extends: ["plugin:matrix-org/babel", "plugin:import/typescript"],
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
@@ -27,63 +21,95 @@ module.exports = {
|
||||
"padded-blocks": ["error"],
|
||||
"no-extend-native": ["error"],
|
||||
"camelcase": ["error"],
|
||||
"no-multi-spaces": ["error", { "ignoreEOLComments": true }],
|
||||
"space-before-function-paren": ["error", {
|
||||
"anonymous": "never",
|
||||
"named": "never",
|
||||
"asyncArrow": "always",
|
||||
}],
|
||||
"no-multi-spaces": ["error", { ignoreEOLComments: true }],
|
||||
"space-before-function-paren": [
|
||||
"error",
|
||||
{
|
||||
anonymous: "never",
|
||||
named: "never",
|
||||
asyncArrow: "always",
|
||||
},
|
||||
],
|
||||
"arrow-parens": "off",
|
||||
"prefer-promise-reject-errors": "off",
|
||||
"quotes": "off",
|
||||
"indent": "off",
|
||||
"no-constant-condition": "off",
|
||||
"no-async-promise-executor": "off",
|
||||
// We use a `logger` intermediary module
|
||||
"no-console": "error",
|
||||
|
||||
// restrict EventEmitters to force callers to use TypedEventEmitter
|
||||
"no-restricted-imports": ["error", {
|
||||
name: "events",
|
||||
message: "Please use TypedEventEmitter instead"
|
||||
}],
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
name: "events",
|
||||
message: "Please use TypedEventEmitter instead",
|
||||
},
|
||||
],
|
||||
|
||||
"import/no-restricted-paths": ["error", {
|
||||
"zones": [{
|
||||
"target": "./src/",
|
||||
"from": "./src/index.ts",
|
||||
"message": "The package index is dynamic between src and lib depending on " +
|
||||
"whether release or development, target the specific module or matrix.ts instead",
|
||||
}],
|
||||
}],
|
||||
"import/no-restricted-paths": [
|
||||
"error",
|
||||
{
|
||||
zones: [
|
||||
{
|
||||
target: "./src/",
|
||||
from: "./src/index.ts",
|
||||
message:
|
||||
"The package index is dynamic between src and lib depending on " +
|
||||
"whether release or development, target the specific module or matrix.ts instead",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [{
|
||||
files: [
|
||||
"**/*.ts",
|
||||
],
|
||||
extends: [
|
||||
"plugin:matrix-org/typescript",
|
||||
],
|
||||
rules: {
|
||||
// TypeScript has its own version of this
|
||||
"@babel/no-invalid-this": "off",
|
||||
overrides: [
|
||||
{
|
||||
files: ["**/*.ts"],
|
||||
plugins: ["eslint-plugin-tsdoc"],
|
||||
extends: ["plugin:matrix-org/typescript"],
|
||||
rules: {
|
||||
// TypeScript has its own version of this
|
||||
"@babel/no-invalid-this": "off",
|
||||
|
||||
// We're okay being explicit at the moment
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
// We disable this while we're transitioning
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
// We'd rather not do this but we do
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
// We're okay with assertion errors when we ask for them
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
// We're okay being explicit at the moment
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
// We disable this while we're transitioning
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
// We'd rather not do this but we do
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
// We're okay with assertion errors when we ask for them
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
|
||||
// The non-TypeScript rule produces false positives
|
||||
"func-call-spacing": "off",
|
||||
"@typescript-eslint/func-call-spacing": ["error"],
|
||||
// The non-TypeScript rule produces false positives
|
||||
"func-call-spacing": "off",
|
||||
"@typescript-eslint/func-call-spacing": ["error"],
|
||||
|
||||
"quotes": "off",
|
||||
// We use a `logger` intermediary module
|
||||
"no-console": "error",
|
||||
"quotes": "off",
|
||||
// We use a `logger` intermediary module
|
||||
"no-console": "error",
|
||||
},
|
||||
},
|
||||
}],
|
||||
{
|
||||
// We don't need amazing docs in our spec files
|
||||
files: ["src/**/*.ts"],
|
||||
rules: {
|
||||
"tsdoc/syntax": "error",
|
||||
// We use some select jsdoc rules as the tsdoc linter has only one rule
|
||||
"jsdoc/no-types": "error",
|
||||
"jsdoc/empty-tags": "error",
|
||||
"jsdoc/check-property-names": "error",
|
||||
"jsdoc/check-values": "error",
|
||||
// These need a bit more work before we can enable
|
||||
// "jsdoc/check-param-names": "error",
|
||||
// "jsdoc/check-indentation": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["spec/**/*.ts"],
|
||||
rules: {
|
||||
// We don't need super strict typing in test utilities
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-member-accessibility": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -38,4 +38,5 @@ cee7f7a280a8c20bafc21c0a2911f60851f7a7ca
|
||||
7ed65407e6cdf292ce3cf659310c68d19dcd52b2
|
||||
# Switch to ESLint from JSHint (Google eslint rules as a base)
|
||||
e057956ede9ad1a931ff8050c411aca7907e0394
|
||||
|
||||
# prettier
|
||||
349c2c2587c2885bb69eda4aa078b5383724cf5e
|
||||
|
||||
+6
-4
@@ -1,4 +1,6 @@
|
||||
* @matrix-org/element-web
|
||||
|
||||
/src/webrtc @matrix-org/element-call-reviewers
|
||||
/spec/*/webrtc @matrix-org/element-call-reviewers
|
||||
* @matrix-org/element-web
|
||||
/.github/workflows/** @matrix-org/element-web-app-team
|
||||
/package.json @matrix-org/element-web-app-team
|
||||
/yarn.lock @matrix-org/element-web-app-team
|
||||
/src/webrtc @matrix-org/element-call-reviewers
|
||||
/spec/*/webrtc @matrix-org/element-call-reviewers
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
## Checklist
|
||||
|
||||
* [ ] Tests written for new code (and old code if feasible)
|
||||
* [ ] Linter and other CI checks pass
|
||||
* [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md))
|
||||
- [ ] Tests written for new code (and old code if feasible)
|
||||
- [ ] Linter and other CI checks pass
|
||||
- [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md))
|
||||
|
||||
<!--
|
||||
If you would like to specify text for the changelog entry other than your PR title, add the following:
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"github>matrix-org/renovate-config-element-web"
|
||||
]
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["github>matrix-org/renovate-config-element-web"]
|
||||
}
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
name: Backport
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- closed
|
||||
- labeled
|
||||
branches:
|
||||
- develop
|
||||
pull_request_target:
|
||||
types:
|
||||
- closed
|
||||
- labeled
|
||||
branches:
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
name: Backport
|
||||
runs-on: ubuntu-latest
|
||||
# Only react to merged PRs for security reasons.
|
||||
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
|
||||
if: >
|
||||
github.event.pull_request.merged
|
||||
&& (
|
||||
github.event.action == 'closed'
|
||||
|| (
|
||||
github.event.action == 'labeled'
|
||||
&& contains(github.event.label.name, 'backport')
|
||||
)
|
||||
)
|
||||
steps:
|
||||
- uses: tibdex/backport@v2
|
||||
with:
|
||||
labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>"
|
||||
# We can't use GITHUB_TOKEN here or CI won't run on the new PR
|
||||
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
backport:
|
||||
name: Backport
|
||||
runs-on: ubuntu-latest
|
||||
# Only react to merged PRs for security reasons.
|
||||
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
|
||||
if: >
|
||||
github.event.pull_request.merged
|
||||
&& (
|
||||
github.event.action == 'closed'
|
||||
|| (
|
||||
github.event.action == 'labeled'
|
||||
&& contains(github.event.label.name, 'backport')
|
||||
)
|
||||
)
|
||||
steps:
|
||||
- uses: tibdex/backport@v2
|
||||
with:
|
||||
labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>"
|
||||
# We can't use GITHUB_TOKEN here or CI won't run on the new PR
|
||||
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
name: Deploy documentation PR preview
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ "Static Analysis" ]
|
||||
types:
|
||||
- completed
|
||||
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
|
||||
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@bd10f381a96414ce2b13a11bfa89902ba7cea07f # v2.24.3
|
||||
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
|
||||
- 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
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
name: Notify Downstream Projects
|
||||
on:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
push:
|
||||
branches: [develop]
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
notify-downstream:
|
||||
# Only respect triggers from our develop branch, ignore that of forks
|
||||
if: github.repository == 'matrix-org/matrix-js-sdk'
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- repo: vector-im/element-web
|
||||
event: element-web-notify
|
||||
- repo: matrix-org/matrix-react-sdk
|
||||
event: upstream-sdk-notify
|
||||
notify-downstream:
|
||||
# Only respect triggers from our develop branch, ignore that of forks
|
||||
if: github.repository == 'matrix-org/matrix-js-sdk'
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- repo: vector-im/element-web
|
||||
event: element-web-notify
|
||||
- repo: matrix-org/matrix-react-sdk
|
||||
event: upstream-sdk-notify
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
repository: ${{ matrix.repo }}
|
||||
event-type: ${{ matrix.event }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
repository: ${{ matrix.repo }}
|
||||
event-type: ${{ matrix.event }}
|
||||
|
||||
@@ -1,91 +1,91 @@
|
||||
name: Pull Request
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [ opened, edited, labeled, unlabeled, synchronize ]
|
||||
workflow_call:
|
||||
inputs:
|
||||
labels:
|
||||
type: string
|
||||
default: "T-Defect,T-Deprecation,T-Enhancement,T-Task"
|
||||
required: false
|
||||
description: "No longer used, uses allchange logic now, will be removed at a later date"
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
pull_request_target:
|
||||
types: [opened, edited, labeled, unlabeled, synchronize]
|
||||
workflow_call:
|
||||
inputs:
|
||||
labels:
|
||||
type: string
|
||||
default: "T-Defect,T-Deprecation,T-Enhancement,T-Task"
|
||||
required: false
|
||||
description: "No longer used, uses allchange logic now, will be removed at a later date"
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref }}
|
||||
jobs:
|
||||
changelog:
|
||||
name: Preview Changelog
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: matrix-org/allchange@main
|
||||
with:
|
||||
ghToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
requireLabel: true
|
||||
changelog:
|
||||
name: Preview Changelog
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: matrix-org/allchange@main
|
||||
with:
|
||||
ghToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
requireLabel: true
|
||||
|
||||
prevent-blocked:
|
||||
name: Prevent Blocked
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: Add notice
|
||||
uses: actions/github-script@v6
|
||||
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
|
||||
with:
|
||||
script: |
|
||||
core.setFailed("Preventing merge whilst PR is marked blocked!");
|
||||
prevent-blocked:
|
||||
name: Prevent Blocked
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: Add notice
|
||||
uses: actions/github-script@v6
|
||||
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
|
||||
with:
|
||||
script: |
|
||||
core.setFailed("Preventing merge whilst PR is marked blocked!");
|
||||
|
||||
community-prs:
|
||||
name: Label Community PRs
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action == 'opened'
|
||||
steps:
|
||||
- name: Check membership
|
||||
uses: tspascoal/get-user-teams-membership@v1
|
||||
id: teams
|
||||
with:
|
||||
username: ${{ github.event.pull_request.user.login }}
|
||||
organization: matrix-org
|
||||
team: Core Team
|
||||
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
community-prs:
|
||||
name: Label Community PRs
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action == 'opened'
|
||||
steps:
|
||||
- name: Check membership
|
||||
uses: tspascoal/get-user-teams-membership@v2
|
||||
id: teams
|
||||
with:
|
||||
username: ${{ github.event.pull_request.user.login }}
|
||||
organization: matrix-org
|
||||
team: Core Team
|
||||
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
- name: Add label
|
||||
if: ${{ steps.teams.outputs.isTeamMember == 'false' }}
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.addLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ['Z-Community-PR']
|
||||
});
|
||||
- name: Add label
|
||||
if: ${{ steps.teams.outputs.isTeamMember == 'false' }}
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.addLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ['Z-Community-PR']
|
||||
});
|
||||
|
||||
close-if-fork-develop:
|
||||
name: Forbid develop branch fork contributions
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.event.action == 'opened' &&
|
||||
github.event.pull_request.head.ref == 'develop' &&
|
||||
github.event.pull_request.head.repo.full_name != github.repository
|
||||
steps:
|
||||
- name: Close pull request
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: "Thanks for opening this pull request, unfortunately we do not accept contributions from the main" +
|
||||
" branch of your fork, please re-open once you switch to an alternative branch for everyone's sanity." +
|
||||
" See https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md",
|
||||
});
|
||||
close-if-fork-develop:
|
||||
name: Forbid develop branch fork contributions
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.event.action == 'opened' &&
|
||||
github.event.pull_request.head.ref == 'develop' &&
|
||||
github.event.pull_request.head.repo.full_name != github.repository
|
||||
steps:
|
||||
- name: Close pull request
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: "Thanks for opening this pull request, unfortunately we do not accept contributions from the main" +
|
||||
" branch of your fork, please re-open once you switch to an alternative branch for everyone's sanity." +
|
||||
" See https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md",
|
||||
});
|
||||
|
||||
github.rest.pulls.update({
|
||||
pull_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'closed'
|
||||
});
|
||||
github.rest.pulls.update({
|
||||
pull_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'closed'
|
||||
});
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
# Must only be called from `release#published` triggers
|
||||
name: Publish to npm
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
NPM_TOKEN:
|
||||
required: true
|
||||
workflow_call:
|
||||
secrets:
|
||||
NPM_TOKEN:
|
||||
required: true
|
||||
jobs:
|
||||
npm:
|
||||
name: Publish to npm
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v3
|
||||
npm:
|
||||
name: Publish to npm
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --pure-lockfile"
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --pure-lockfile"
|
||||
|
||||
- name: 🚀 Publish to npm
|
||||
id: npm-publish
|
||||
uses: JS-DevTools/npm-publish@v1
|
||||
with:
|
||||
token: ${{ secrets.NPM_TOKEN }}
|
||||
access: public
|
||||
tag: next
|
||||
- name: 🚀 Publish to npm
|
||||
id: npm-publish
|
||||
uses: JS-DevTools/npm-publish@v1
|
||||
with:
|
||||
token: ${{ secrets.NPM_TOKEN }}
|
||||
access: public
|
||||
tag: next
|
||||
|
||||
- name: 🎖️ Add `latest` dist-tag to final releases
|
||||
if: github.event.release.prerelease == false
|
||||
run: |
|
||||
package=$(cat package.json | jq -er .name)
|
||||
npm dist-tag add "$package@$release" latest
|
||||
env:
|
||||
# JS-DevTools/npm-publish overrides `NODE_AUTH_TOKEN` with `INPUT_TOKEN` in .npmrc
|
||||
INPUT_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
release: ${{ steps.npm-publish.outputs.version }}
|
||||
- name: 🎖️ Add `latest` dist-tag to final releases
|
||||
if: github.event.release.prerelease == false
|
||||
run: |
|
||||
package=$(cat package.json | jq -er .name)
|
||||
npm dist-tag add "$package@$release" latest
|
||||
env:
|
||||
# JS-DevTools/npm-publish overrides `NODE_AUTH_TOKEN` with `INPUT_TOKEN` in .npmrc
|
||||
INPUT_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
release: ${{ steps.npm-publish.outputs.version }}
|
||||
|
||||
@@ -1,58 +1,59 @@
|
||||
name: Release Process
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
release:
|
||||
types: [published]
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
jsdoc:
|
||||
name: Publish Documentation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v3
|
||||
jsdoc:
|
||||
name: Publish Documentation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --pure-lockfile"
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --pure-lockfile"
|
||||
|
||||
- name: 📖 Generate JSDoc
|
||||
run: "yarn gendoc"
|
||||
- name: 📖 Generate JSDoc
|
||||
run: "yarn gendoc"
|
||||
|
||||
- name: 📋 Copy to temp
|
||||
run: |
|
||||
cp -a "./_docs" "$RUNNER_TEMP/"
|
||||
- name: 📋 Copy to temp
|
||||
run: |
|
||||
cp -a "./_docs" "$RUNNER_TEMP/"
|
||||
|
||||
- name: 🧮 Checkout gh-pages
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: gh-pages
|
||||
- name: 🧮 Checkout gh-pages
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: gh-pages
|
||||
|
||||
- name: 🔪 Prepare
|
||||
run: |
|
||||
tag="${{ github.ref_name }}"
|
||||
VERSION="${tag#v}"
|
||||
[ ! -e "$VERSION" ] || rm -r $VERSION
|
||||
cp -r $RUNNER_TEMP/docs/ $VERSION
|
||||
- name: 🔪 Prepare
|
||||
env:
|
||||
GITHUB_REF_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
[ ! -e "$VERSION" ] || rm -r $VERSION
|
||||
cp -r $RUNNER_TEMP/_docs/ $VERSION
|
||||
|
||||
# Add the new directory to the index if it isn't there already
|
||||
if ! grep -q ">Version $VERSION</a>" index.html; then
|
||||
perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print
|
||||
"<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' "$VERSION" index.html
|
||||
fi
|
||||
# Add the new directory to the index if it isn't there already
|
||||
if ! grep -q ">Version $VERSION</a>" index.html; then
|
||||
perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print
|
||||
"<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' "$VERSION" index.html
|
||||
fi
|
||||
|
||||
- name: 🚀 Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
keep_files: true
|
||||
publish_dir: .
|
||||
- name: 🚀 Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
keep_files: true
|
||||
publish_dir: .
|
||||
|
||||
npm:
|
||||
name: Publish
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
npm:
|
||||
name: Publish
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
@@ -1,50 +1,52 @@
|
||||
# Must only be called from a workflow_run in the context of the upstream repo
|
||||
name: SonarCloud
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
SONAR_TOKEN:
|
||||
required: true
|
||||
inputs:
|
||||
extra_args:
|
||||
type: string
|
||||
required: false
|
||||
description: "Extra args to pass to SonarCloud"
|
||||
workflow_call:
|
||||
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
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
steps:
|
||||
# We create the status here and then update it to success/failure in the `report` stage
|
||||
# This provides an easy link to this workflow_run from the PR before Cypress is done.
|
||||
- uses: Sibz/github-status-action@v1
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: pending
|
||||
context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
|
||||
sha: ${{ github.event.workflow_run.head_sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
sonarqube:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
steps:
|
||||
# We create the status here and then update it to success/failure in the `report` stage
|
||||
# This provides an easy link to this workflow_run from the PR before Cypress is done.
|
||||
- uses: Sibz/github-status-action@v1
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: pending
|
||||
context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
|
||||
sha: ${{ github.event.workflow_run.head_sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- name: "🩻 SonarCloud Scan"
|
||||
id: sonarcloud
|
||||
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' }}
|
||||
version_cmd: 'cat package.json | jq -r .version'
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
revision: ${{ github.event.workflow_run.head_sha }}
|
||||
token: ${{ secrets.SONAR_TOKEN }}
|
||||
coverage_run_id: ${{ github.event.workflow_run.id }}
|
||||
coverage_workflow_name: tests.yml
|
||||
coverage_extract_path: coverage
|
||||
extra_args: ${{ inputs.extra_args }}
|
||||
- name: "🩻 SonarCloud Scan"
|
||||
id: sonarcloud
|
||||
uses: matrix-org/sonarcloud-workflow-action@v2.3
|
||||
# workflow_run fails report against the develop commit always, we don't want that for PRs
|
||||
continue-on-error: ${{ github.event.workflow_run.head_branch != 'develop' }}
|
||||
with:
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
version_cmd: "cat package.json | jq -r .version"
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
revision: ${{ github.event.workflow_run.head_sha }}
|
||||
token: ${{ secrets.SONAR_TOKEN }}
|
||||
coverage_run_id: ${{ github.event.workflow_run.id }}
|
||||
coverage_workflow_name: tests.yml
|
||||
coverage_extract_path: coverage
|
||||
extra_args: ${{ inputs.extra_args }}
|
||||
|
||||
- uses: Sibz/github-status-action@v1
|
||||
if: always()
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: ${{ steps.sonarcloud.outcome == 'success' && 'success' || 'failure' }}
|
||||
context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
|
||||
sha: ${{ github.event.workflow_run.head_sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
- uses: Sibz/github-status-action@v1
|
||||
if: always()
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: ${{ steps.sonarcloud.outcome == 'success' && 'success' || 'failure' }}
|
||||
context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
|
||||
sha: ${{ github.event.workflow_run.head_sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
name: SonarQube
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ "Tests" ]
|
||||
types:
|
||||
- completed
|
||||
workflow_run:
|
||||
workflows: ["Tests"]
|
||||
types:
|
||||
- completed
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
|
||||
cancel-in-progress: true
|
||||
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
|
||||
# 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:
|
||||
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 }}
|
||||
extra_args: -Dsonar.javascript.lcov.reportPaths=${{ needs.prepare.outputs.reportPaths }} -Dsonar.testExecutionReportPaths=${{ needs.prepare.outputs.testExecutionReportPaths }}
|
||||
|
||||
@@ -1,74 +1,74 @@
|
||||
name: Static Analysis
|
||||
on:
|
||||
pull_request: { }
|
||||
push:
|
||||
branches: [ develop, master ]
|
||||
pull_request: {}
|
||||
push:
|
||||
branches: [develop, master]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
ts_lint:
|
||||
name: "Typescript Syntax Check"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
ts_lint:
|
||||
name: "Typescript Syntax Check"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
|
||||
- name: Typecheck
|
||||
run: "yarn run lint:types"
|
||||
- name: Typecheck
|
||||
run: "yarn run lint:types"
|
||||
|
||||
- name: Switch js-sdk to release mode
|
||||
run: |
|
||||
scripts/switch_package_to_release.js
|
||||
yarn install
|
||||
yarn run build:compile
|
||||
yarn run build:types
|
||||
- name: Switch js-sdk to release mode
|
||||
run: |
|
||||
scripts/switch_package_to_release.js
|
||||
yarn install
|
||||
yarn run build:compile
|
||||
yarn run build:types
|
||||
|
||||
- name: Typecheck (release mode)
|
||||
run: "yarn run lint:types"
|
||||
- name: Typecheck (release mode)
|
||||
run: "yarn run lint:types"
|
||||
|
||||
js_lint:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
js_lint:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
|
||||
- name: Run Linter
|
||||
run: "yarn run lint:js"
|
||||
- name: Run Linter
|
||||
run: "yarn run lint:js"
|
||||
|
||||
docs:
|
||||
name: "JSDoc Checker"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
docs:
|
||||
name: "JSDoc Checker"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
|
||||
- name: Generate Docs
|
||||
run: "yarn run gendoc"
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: docs
|
||||
path: _docs
|
||||
# We'll only use this in a workflow_run, then we're done with it
|
||||
retention-days: 1
|
||||
- name: Generate Docs
|
||||
run: "yarn run gendoc"
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: docs
|
||||
path: _docs
|
||||
# We'll only use this in a workflow_run, then we're done with it
|
||||
retention-days: 1
|
||||
|
||||
+51
-42
@@ -1,52 +1,61 @@
|
||||
name: Tests
|
||||
on:
|
||||
pull_request: { }
|
||||
push:
|
||||
branches: [ develop, master ]
|
||||
pull_request: {}
|
||||
push:
|
||||
branches: [develop, master]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
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
|
||||
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: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
node-version: ${{ matrix.node }}
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: "yarn install"
|
||||
- name: Install dependencies
|
||||
run: "yarn install"
|
||||
|
||||
- name: Build
|
||||
if: matrix.specs == 'browserify'
|
||||
run: "yarn build"
|
||||
- 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: 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 --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: Run tests with coverage and metrics
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
run: |
|
||||
yarn coverage --ci --reporters github-actions '--reporters=<rootDir>/spec/slowReporter.js' --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
|
||||
with:
|
||||
name: coverage
|
||||
path: |
|
||||
coverage
|
||||
!coverage/lcov-report
|
||||
- name: Run tests with coverage
|
||||
if: github.ref != 'refs/heads/develop'
|
||||
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
|
||||
with:
|
||||
name: coverage
|
||||
path: |
|
||||
coverage
|
||||
!coverage/lcov-report
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
name: Upgrade Dependencies
|
||||
on:
|
||||
workflow_dispatch: { }
|
||||
workflow_call:
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
workflow_dispatch: {}
|
||||
workflow_call:
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
jobs:
|
||||
upgrade:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
upgrade:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: Upgrade
|
||||
run: yarn upgrade && yarn install
|
||||
- name: Upgrade
|
||||
run: yarn upgrade && yarn install
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/upgrade-deps
|
||||
delete-branch: true
|
||||
title: Upgrade dependencies
|
||||
labels: |
|
||||
Dependencies
|
||||
T-Task
|
||||
|
||||
- name: Enable automerge
|
||||
uses: peter-evans/enable-pull-request-automerge@v2
|
||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/upgrade-deps
|
||||
delete-branch: true
|
||||
title: Upgrade dependencies
|
||||
labels: |
|
||||
Dependencies
|
||||
T-Task
|
||||
|
||||
- name: Enable automerge
|
||||
uses: peter-evans/enable-pull-request-automerge@v2
|
||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/_docs
|
||||
.DS_Store
|
||||
|
||||
/.npmrc
|
||||
/*.log
|
||||
package-lock.json
|
||||
.lock-wscript
|
||||
build/Release
|
||||
coverage
|
||||
lib-cov
|
||||
out
|
||||
/dist
|
||||
/lib
|
||||
/examples/browser/lib
|
||||
/examples/crypto-browser/lib
|
||||
/examples/voip/lib
|
||||
|
||||
# version file and tarball created by `npm pack` / `yarn pack`
|
||||
/git-revision.txt
|
||||
/matrix-js-sdk-*.tgz
|
||||
|
||||
.vscode
|
||||
.vscode/
|
||||
|
||||
# This file is owned, parsed, and generated by allchange, which doesn't comply with prettier
|
||||
/CHANGELOG.md
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require("eslint-plugin-matrix-org/.prettierrc.js");
|
||||
+107
@@ -1,3 +1,110 @@
|
||||
Changes in [23.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.2.0) (2023-01-31)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Implement decryption via the rust sdk ([\#3074](https://github.com/matrix-org/matrix-js-sdk/pull/3074)).
|
||||
* Handle edits which are bundled with an event, per MSC3925 ([\#3045](https://github.com/matrix-org/matrix-js-sdk/pull/3045)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Add null check for our own member event ([\#3082](https://github.com/matrix-org/matrix-js-sdk/pull/3082)).
|
||||
* Handle group call getting initialised twice in quick succession ([\#3078](https://github.com/matrix-org/matrix-js-sdk/pull/3078)). Fixes vector-im/element-call#847.
|
||||
* Correctly handle limited sync responses by resetting the thread timeline ([\#3056](https://github.com/matrix-org/matrix-js-sdk/pull/3056)). Fixes vector-im/element-web#23952. Contributed by @justjanne.
|
||||
* Fix failure to start in firefox private browser ([\#3058](https://github.com/matrix-org/matrix-js-sdk/pull/3058)). Fixes vector-im/element-web#24216.
|
||||
* Fix spurious "Decryption key withheld" messages ([\#3061](https://github.com/matrix-org/matrix-js-sdk/pull/3061)). Fixes vector-im/element-web#23803.
|
||||
* Fix browser entrypoint ([\#3051](https://github.com/matrix-org/matrix-js-sdk/pull/3051)). Fixes #3013.
|
||||
|
||||
Changes in [23.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.1.1) (2023-01-20)
|
||||
==================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix backwards compability for environment not support Array.prototype.at ([\#3080](https://github.com/matrix-org/matrix-js-sdk/pull/3080)).
|
||||
|
||||
Changes in [23.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.1.0) (2023-01-18)
|
||||
==================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Remove extensible events v1 field population on legacy events ([\#3040](https://github.com/matrix-org/matrix-js-sdk/pull/3040)).
|
||||
|
||||
## ✨ Features
|
||||
* Improve hasUserReadEvent and getUserReadUpTo realibility with threads ([\#3031](https://github.com/matrix-org/matrix-js-sdk/pull/3031)). Fixes vector-im/element-web#24164.
|
||||
* Remove video track when muting video ([\#3028](https://github.com/matrix-org/matrix-js-sdk/pull/3028)). Fixes vector-im/element-call#209.
|
||||
* Make poll start event type available (PSG-962) ([\#3034](https://github.com/matrix-org/matrix-js-sdk/pull/3034)).
|
||||
* Add alt event type matching in Relations model ([\#3018](https://github.com/matrix-org/matrix-js-sdk/pull/3018)).
|
||||
* Remove usage of v1 Identity Server API ([\#3003](https://github.com/matrix-org/matrix-js-sdk/pull/3003)).
|
||||
* Add `device_id` to `/account/whoami` types ([\#3005](https://github.com/matrix-org/matrix-js-sdk/pull/3005)).
|
||||
* Implement MSC3912: Relation-based redactions ([\#2954](https://github.com/matrix-org/matrix-js-sdk/pull/2954)).
|
||||
* Introduce a mechanism for using the rust-crypto-sdk ([\#2969](https://github.com/matrix-org/matrix-js-sdk/pull/2969)).
|
||||
* Support MSC3391: Account data deletion ([\#2967](https://github.com/matrix-org/matrix-js-sdk/pull/2967)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix threaded cache receipt when event holds multiple receipts ([\#3026](https://github.com/matrix-org/matrix-js-sdk/pull/3026)).
|
||||
* Fix false key requests after verifying new device ([\#3029](https://github.com/matrix-org/matrix-js-sdk/pull/3029)). Fixes vector-im/element-web#24167 and vector-im/element-web#23333.
|
||||
* Avoid triggering decryption errors when decrypting redacted events ([\#3004](https://github.com/matrix-org/matrix-js-sdk/pull/3004)). Fixes vector-im/element-web#24084.
|
||||
* bugfix: upload OTKs in sliding sync mode ([\#3008](https://github.com/matrix-org/matrix-js-sdk/pull/3008)).
|
||||
* Apply edits discovered from sync after thread is initialised ([\#3002](https://github.com/matrix-org/matrix-js-sdk/pull/3002)). Fixes vector-im/element-web#23921.
|
||||
* Sliding sync: Fix issue where no unsubs are sent when switching rooms ([\#2991](https://github.com/matrix-org/matrix-js-sdk/pull/2991)).
|
||||
* Threads are missing from the timeline ([\#2996](https://github.com/matrix-org/matrix-js-sdk/pull/2996)). Fixes vector-im/element-web#24036.
|
||||
* Close all streams when a call ends ([\#2992](https://github.com/matrix-org/matrix-js-sdk/pull/2992)). Fixes vector-im/element-call#742.
|
||||
* Resume to-device message queue after resumed sync ([\#2920](https://github.com/matrix-org/matrix-js-sdk/pull/2920)). Fixes matrix-org/element-web-rageshakes#17170.
|
||||
* Fix browser entrypoint ([\#3051](https://github.com/matrix-org/matrix-js-sdk/pull/3051)). Fixes #3013.
|
||||
* Fix failure to start in firefox private browser ([\#3058](https://github.com/matrix-org/matrix-js-sdk/pull/3058)). Fixes vector-im/element-web#24216.
|
||||
* Correctly handle limited sync responses by resetting the thread timeline ([\#3056](https://github.com/matrix-org/matrix-js-sdk/pull/3056)). Fixes vector-im/element-web#23952.
|
||||
|
||||
Changes in [23.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.0.0) (2022-12-21)
|
||||
==================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Process `m.room.encryption` events before emitting `RoomMember` events ([\#2914](https://github.com/matrix-org/matrix-js-sdk/pull/2914)). Fixes vector-im/element-web#23819.
|
||||
* Don't expose `calls` on `GroupCall` ([\#2941](https://github.com/matrix-org/matrix-js-sdk/pull/2941)).
|
||||
|
||||
## ✨ Features
|
||||
* Support MSC3391: Account data deletion ([\#2967](https://github.com/matrix-org/matrix-js-sdk/pull/2967)).
|
||||
* Add a message ID on each to-device message ([\#2938](https://github.com/matrix-org/matrix-js-sdk/pull/2938)).
|
||||
* Enable multiple users' power levels to be set at once ([\#2892](https://github.com/matrix-org/matrix-js-sdk/pull/2892)). Contributed by @GoodGuyMarco.
|
||||
* Include pending events in thread summary and count again ([\#2922](https://github.com/matrix-org/matrix-js-sdk/pull/2922)). Fixes vector-im/element-web#23642.
|
||||
* Make GroupCall work better with widgets ([\#2935](https://github.com/matrix-org/matrix-js-sdk/pull/2935)).
|
||||
* Add method to get outgoing room key requests for a given event ([\#2930](https://github.com/matrix-org/matrix-js-sdk/pull/2930)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix messages loaded during initial fetch ending up out of order ([\#2971](https://github.com/matrix-org/matrix-js-sdk/pull/2971)). Fixes vector-im/element-web#23972.
|
||||
* Fix #23919: Root message for new thread loaded from network ([\#2965](https://github.com/matrix-org/matrix-js-sdk/pull/2965)). Fixes vector-im/element-web#23919.
|
||||
* Fix #23916: Prevent edits of the last message in a thread getting lost ([\#2951](https://github.com/matrix-org/matrix-js-sdk/pull/2951)). Fixes vector-im/element-web#23916 and vector-im/element-web#23942.
|
||||
* Fix infinite loop when restoring cached read receipts ([\#2963](https://github.com/matrix-org/matrix-js-sdk/pull/2963)). Fixes vector-im/element-web#23951.
|
||||
* Don't swallow errors coming from the shareSession call ([\#2962](https://github.com/matrix-org/matrix-js-sdk/pull/2962)). Fixes vector-im/element-web#23792.
|
||||
* Make sure that MegolmEncryption.setupPromise always resolves ([\#2960](https://github.com/matrix-org/matrix-js-sdk/pull/2960)).
|
||||
* Do not calculate highlight notifs for threads unknown to the room ([\#2957](https://github.com/matrix-org/matrix-js-sdk/pull/2957)).
|
||||
* Cache read receipts for unknown threads ([\#2953](https://github.com/matrix-org/matrix-js-sdk/pull/2953)).
|
||||
* bugfix: sliding sync initial room timelines shouldn't notify ([\#2933](https://github.com/matrix-org/matrix-js-sdk/pull/2933)).
|
||||
* Redo key sharing after own device verification ([\#2921](https://github.com/matrix-org/matrix-js-sdk/pull/2921)). Fixes vector-im/element-web#23333.
|
||||
* Move updated threads to the end of the thread list ([\#2923](https://github.com/matrix-org/matrix-js-sdk/pull/2923)). Fixes vector-im/element-web#23876.
|
||||
* Fix highlight notifications increasing when total notification is zero ([\#2937](https://github.com/matrix-org/matrix-js-sdk/pull/2937)). Fixes vector-im/element-web#23885.
|
||||
* Fix synthesizeReceipt ([\#2916](https://github.com/matrix-org/matrix-js-sdk/pull/2916)). Fixes vector-im/element-web#23827 vector-im/element-web#23754 and vector-im/element-web#23847.
|
||||
|
||||
Changes in [22.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v22.0.0) (2022-12-06)
|
||||
==================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Enable users to join group calls from multiple devices ([\#2902](https://github.com/matrix-org/matrix-js-sdk/pull/2902)).
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Deprecate a function containing a typo ([\#2904](https://github.com/matrix-org/matrix-js-sdk/pull/2904)).
|
||||
|
||||
## ✨ Features
|
||||
* sliding sync: add receipts extension ([\#2912](https://github.com/matrix-org/matrix-js-sdk/pull/2912)).
|
||||
* Define a spec support policy for the js-sdk ([\#2882](https://github.com/matrix-org/matrix-js-sdk/pull/2882)).
|
||||
* Further improvements to e2ee logging ([\#2900](https://github.com/matrix-org/matrix-js-sdk/pull/2900)).
|
||||
* sliding sync: add support for typing extension ([\#2893](https://github.com/matrix-org/matrix-js-sdk/pull/2893)).
|
||||
* Improve logging on Olm session errors ([\#2885](https://github.com/matrix-org/matrix-js-sdk/pull/2885)).
|
||||
* Improve logging of e2ee messages ([\#2884](https://github.com/matrix-org/matrix-js-sdk/pull/2884)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix 3pid invite acceptance not working due to mxid being sent in body ([\#2907](https://github.com/matrix-org/matrix-js-sdk/pull/2907)). Fixes vector-im/element-web#23823.
|
||||
* Don't hang up calls that haven't started yet ([\#2898](https://github.com/matrix-org/matrix-js-sdk/pull/2898)).
|
||||
* Read receipt accumulation for threads ([\#2881](https://github.com/matrix-org/matrix-js-sdk/pull/2881)).
|
||||
* Make GroupCall work better with widgets ([\#2935](https://github.com/matrix-org/matrix-js-sdk/pull/2935)).
|
||||
* Fix highlight notifications increasing when total notification is zero ([\#2937](https://github.com/matrix-org/matrix-js-sdk/pull/2937)). Fixes vector-im/element-web#23885.
|
||||
* Fix synthesizeReceipt ([\#2916](https://github.com/matrix-org/matrix-js-sdk/pull/2916)). Fixes vector-im/element-web#23827 vector-im/element-web#23754 and vector-im/element-web#23847.
|
||||
|
||||
Changes in [21.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v21.2.0) (2022-11-22)
|
||||
==================================================================================================
|
||||
|
||||
|
||||
+1
-3
@@ -1,5 +1,3 @@
|
||||
Contributing code to matrix-js-sdk
|
||||
==================================
|
||||
# Contributing code to matrix-js-sdk
|
||||
|
||||
matrix-js-sdk follows the same pattern as https://github.com/vector-im/element-web/blob/develop/CONTRIBUTING.md
|
||||
|
||||
|
||||
@@ -6,21 +6,25 @@
|
||||
[](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
|
||||
[](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
|
||||
|
||||
Matrix Javascript SDK
|
||||
=====================
|
||||
# Matrix JavaScript SDK
|
||||
|
||||
This is the [Matrix](https://matrix.org) Client-Server r0 SDK for
|
||||
JavaScript. This SDK can be run in a browser or in Node.js.
|
||||
This is the [Matrix](https://matrix.org) Client-Server SDK for JavaScript and TypeScript. This SDK can be run in a
|
||||
browser or in Node.js.
|
||||
|
||||
Quickstart
|
||||
==========
|
||||
The Matrix specification is constantly evolving - while this SDK aims for maximum backwards compatibility, it only
|
||||
guarantees that a feature will be supported for at least 4 spec releases. For example, if a feature the js-sdk supports
|
||||
is removed in v1.4 then the feature is _eligible_ for removal from the SDK when v1.8 is released. This SDK has no
|
||||
guarantee on implementing all features of any particular spec release, currently. This can mean that the SDK will call
|
||||
endpoints from before Matrix 1.1, for example.
|
||||
|
||||
# Quickstart
|
||||
|
||||
## In a browser
|
||||
|
||||
In a browser
|
||||
------------
|
||||
Download the browser version from
|
||||
https://github.com/matrix-org/matrix-js-sdk/releases/latest and add that as a
|
||||
``<script>`` to your page. There will be a global variable ``matrixcs``
|
||||
attached to ``window`` through which you can access the SDK. See below for how to
|
||||
`<script>` to your page. There will be a global variable `matrixcs`
|
||||
attached to `window` through which you can access the SDK. See below for how to
|
||||
include libolm to enable end-to-end-encryption.
|
||||
|
||||
The browser bundle supports recent versions of browsers. Typically this is ES2015
|
||||
@@ -29,8 +33,7 @@ or `> 0.5%, last 2 versions, Firefox ESR, not dead` if using
|
||||
|
||||
Please check [the working browser example](examples/browser) for more information.
|
||||
|
||||
In Node.js
|
||||
----------
|
||||
## In Node.js
|
||||
|
||||
Ensure you have the latest LTS version of Node.js installed.
|
||||
This library relies on `fetch` which is available in Node from v18.0.0 - it should work fine also with polyfills.
|
||||
@@ -39,14 +42,14 @@ If you wish to use a ponyfill or adapter of some sort then pass it as `fetchFn`
|
||||
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
|
||||
if you do not have it already.
|
||||
|
||||
``yarn add matrix-js-sdk``
|
||||
`yarn add matrix-js-sdk`
|
||||
|
||||
```javascript
|
||||
import * as sdk from "matrix-js-sdk";
|
||||
const client = sdk.createClient("https://matrix.org");
|
||||
client.publicRooms(function(err, data) {
|
||||
import * as sdk from "matrix-js-sdk";
|
||||
const client = sdk.createClient({ baseUrl: "https://matrix.org" });
|
||||
client.publicRooms(function (err, data) {
|
||||
console.log("Public Rooms: %s", JSON.stringify(data));
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
See below for how to include libolm to enable end-to-end-encryption. Please check
|
||||
@@ -55,14 +58,14 @@ See below for how to include libolm to enable end-to-end-encryption. Please chec
|
||||
To start the client:
|
||||
|
||||
```javascript
|
||||
await client.startClient({initialSyncLimit: 10});
|
||||
await client.startClient({ initialSyncLimit: 10 });
|
||||
```
|
||||
|
||||
You can perform a call to `/sync` to get the current state of the client:
|
||||
|
||||
```javascript
|
||||
client.once('sync', function(state, prevState, res) {
|
||||
if(state === 'PREPARED') {
|
||||
client.once("sync", function (state, prevState, res) {
|
||||
if (state === "PREPARED") {
|
||||
console.log("prepared");
|
||||
} else {
|
||||
console.log(state);
|
||||
@@ -75,8 +78,8 @@ To send a message:
|
||||
|
||||
```javascript
|
||||
const content = {
|
||||
"body": "message text",
|
||||
"msgtype": "m.text"
|
||||
body: "message text",
|
||||
msgtype: "m.text",
|
||||
};
|
||||
client.sendEvent("roomId", "m.room.message", content, "", (err, res) => {
|
||||
console.log(err);
|
||||
@@ -86,11 +89,11 @@ client.sendEvent("roomId", "m.room.message", content, "", (err, res) => {
|
||||
To listen for message events:
|
||||
|
||||
```javascript
|
||||
client.on("Room.timeline", function(event, room, toStartOfTimeline) {
|
||||
if (event.getType() !== "m.room.message") {
|
||||
return; // only use messages
|
||||
}
|
||||
console.log(event.event.content.body);
|
||||
client.on("Room.timeline", function (event, room, toStartOfTimeline) {
|
||||
if (event.getType() !== "m.room.message") {
|
||||
return; // only use messages
|
||||
}
|
||||
console.log(event.event.content.body);
|
||||
});
|
||||
```
|
||||
|
||||
@@ -98,73 +101,70 @@ By default, the `matrix-js-sdk` client uses the `MemoryStore` to store events as
|
||||
|
||||
```javascript
|
||||
Object.keys(client.store.rooms).forEach((roomId) => {
|
||||
client.getRoom(roomId).timeline.forEach(t => {
|
||||
console.log(t.event);
|
||||
});
|
||||
client.getRoom(roomId).timeline.forEach((t) => {
|
||||
console.log(t.event);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
What does this SDK do?
|
||||
----------------------
|
||||
## What does this SDK do?
|
||||
|
||||
This SDK provides a full object model around the Matrix Client-Server API and emits
|
||||
events for incoming data and state changes. Aside from wrapping the HTTP API, it:
|
||||
- Handles syncing (via `/initialSync` and `/events`)
|
||||
- Handles the generation of "friendly" room and member names.
|
||||
- Handles historical `RoomMember` information (e.g. display names).
|
||||
- Manages room member state across multiple events (e.g. it handles typing, power
|
||||
levels and membership changes).
|
||||
- Exposes high-level objects like `Rooms`, `RoomState`, `RoomMembers` and `Users`
|
||||
which can be listened to for things like name changes, new messages, membership
|
||||
changes, presence changes, and more.
|
||||
- Handle "local echo" of messages sent using the SDK. This means that messages
|
||||
that have just been sent will appear in the timeline as 'sending', until it
|
||||
completes. This is beneficial because it prevents there being a gap between
|
||||
hitting the send button and having the "remote echo" arrive.
|
||||
- Mark messages which failed to send as not sent.
|
||||
- Automatically retry requests to send messages due to network errors.
|
||||
- Automatically retry requests to send messages due to rate limiting errors.
|
||||
- Handle queueing of messages.
|
||||
- Handles pagination.
|
||||
- Handle assigning push actions for events.
|
||||
- Handles room initial sync on accepting invites.
|
||||
- Handles WebRTC calling.
|
||||
|
||||
- Handles syncing (via `/initialSync` and `/events`)
|
||||
- Handles the generation of "friendly" room and member names.
|
||||
- Handles historical `RoomMember` information (e.g. display names).
|
||||
- Manages room member state across multiple events (e.g. it handles typing, power
|
||||
levels and membership changes).
|
||||
- Exposes high-level objects like `Rooms`, `RoomState`, `RoomMembers` and `Users`
|
||||
which can be listened to for things like name changes, new messages, membership
|
||||
changes, presence changes, and more.
|
||||
- Handle "local echo" of messages sent using the SDK. This means that messages
|
||||
that have just been sent will appear in the timeline as 'sending', until it
|
||||
completes. This is beneficial because it prevents there being a gap between
|
||||
hitting the send button and having the "remote echo" arrive.
|
||||
- Mark messages which failed to send as not sent.
|
||||
- Automatically retry requests to send messages due to network errors.
|
||||
- Automatically retry requests to send messages due to rate limiting errors.
|
||||
- Handle queueing of messages.
|
||||
- Handles pagination.
|
||||
- Handle assigning push actions for events.
|
||||
- Handles room initial sync on accepting invites.
|
||||
- Handles WebRTC calling.
|
||||
|
||||
Later versions of the SDK will:
|
||||
- Expose a `RoomSummary` which would be suitable for a recents page.
|
||||
- Provide different pluggable storage layers (e.g. local storage, database-backed)
|
||||
|
||||
Usage
|
||||
=====
|
||||
- Expose a `RoomSummary` which would be suitable for a recents page.
|
||||
- Provide different pluggable storage layers (e.g. local storage, database-backed)
|
||||
|
||||
# Usage
|
||||
|
||||
Conventions
|
||||
-----------
|
||||
## Conventions
|
||||
|
||||
### Emitted events
|
||||
|
||||
The SDK will emit events using an ``EventEmitter``. It also
|
||||
emits object models (e.g. ``Rooms``, ``RoomMembers``) when they
|
||||
The SDK will emit events using an `EventEmitter`. It also
|
||||
emits object models (e.g. `Rooms`, `RoomMembers`) when they
|
||||
are updated.
|
||||
|
||||
```javascript
|
||||
// Listen for low-level MatrixEvents
|
||||
client.on("event", function(event) {
|
||||
// Listen for low-level MatrixEvents
|
||||
client.on("event", function (event) {
|
||||
console.log(event.getType());
|
||||
});
|
||||
});
|
||||
|
||||
// Listen for typing changes
|
||||
client.on("RoomMember.typing", function(event, member) {
|
||||
// Listen for typing changes
|
||||
client.on("RoomMember.typing", function (event, member) {
|
||||
if (member.typing) {
|
||||
console.log(member.name + " is typing...");
|
||||
console.log(member.name + " is typing...");
|
||||
} else {
|
||||
console.log(member.name + " stopped typing.");
|
||||
}
|
||||
else {
|
||||
console.log(member.name + " stopped typing.");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// start the client to setup the connection to the server
|
||||
client.startClient();
|
||||
// start the client to setup the connection to the server
|
||||
client.startClient();
|
||||
```
|
||||
|
||||
### Promises and Callbacks
|
||||
@@ -181,11 +181,11 @@ The typical usage is something like:
|
||||
});
|
||||
```
|
||||
|
||||
Alternatively, if you have a Node.js-style ``callback(err, result)`` function,
|
||||
Alternatively, if you have a Node.js-style `callback(err, result)` function,
|
||||
you can pass the result of the promise into it with something like:
|
||||
|
||||
```javascript
|
||||
matrixClient.someMethod(arg1, arg2).nodeify(callback);
|
||||
matrixClient.someMethod(arg1, arg2).nodeify(callback);
|
||||
```
|
||||
|
||||
The main thing to note is that it is problematic to discard the result of a
|
||||
@@ -193,61 +193,65 @@ promise-returning function, as that will cause exceptions to go unobserved.
|
||||
|
||||
Methods which return a promise show this in their documentation.
|
||||
|
||||
Many methods in the SDK support *both* Node.js-style callbacks *and* Promises,
|
||||
via an optional ``callback`` argument. The callback support is now deprecated:
|
||||
new methods do not include a ``callback`` argument, and in the future it may be
|
||||
Many methods in the SDK support _both_ Node.js-style callbacks _and_ Promises,
|
||||
via an optional `callback` argument. The callback support is now deprecated:
|
||||
new methods do not include a `callback` argument, and in the future it may be
|
||||
removed from existing methods.
|
||||
|
||||
Examples
|
||||
--------
|
||||
## Examples
|
||||
|
||||
This section provides some useful code snippets which demonstrate the
|
||||
core functionality of the SDK. These examples assume the SDK is setup like this:
|
||||
|
||||
```javascript
|
||||
import * as sdk from "matrix-js-sdk";
|
||||
const myUserId = "@example:localhost";
|
||||
const myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
|
||||
const matrixClient = sdk.createClient({
|
||||
baseUrl: "http://localhost:8008",
|
||||
accessToken: myAccessToken,
|
||||
userId: myUserId
|
||||
});
|
||||
import * as sdk from "matrix-js-sdk";
|
||||
const myUserId = "@example:localhost";
|
||||
const myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
|
||||
const matrixClient = sdk.createClient({
|
||||
baseUrl: "http://localhost:8008",
|
||||
accessToken: myAccessToken,
|
||||
userId: myUserId,
|
||||
});
|
||||
```
|
||||
|
||||
### Automatically join rooms when invited
|
||||
|
||||
```javascript
|
||||
matrixClient.on("RoomMember.membership", function(event, member) {
|
||||
if (member.membership === "invite" && member.userId === myUserId) {
|
||||
matrixClient.joinRoom(member.roomId).then(function() {
|
||||
console.log("Auto-joined %s", member.roomId);
|
||||
});
|
||||
}
|
||||
});
|
||||
matrixClient.on("RoomMember.membership", function (event, member) {
|
||||
if (member.membership === "invite" && member.userId === myUserId) {
|
||||
matrixClient.joinRoom(member.roomId).then(function () {
|
||||
console.log("Auto-joined %s", member.roomId);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
matrixClient.startClient();
|
||||
matrixClient.startClient();
|
||||
```
|
||||
|
||||
### Print out messages for all rooms
|
||||
|
||||
```javascript
|
||||
matrixClient.on("Room.timeline", function(event, room, toStartOfTimeline) {
|
||||
if (toStartOfTimeline) {
|
||||
return; // don't print paginated results
|
||||
}
|
||||
if (event.getType() !== "m.room.message") {
|
||||
return; // only print messages
|
||||
}
|
||||
console.log(
|
||||
// the room name will update with m.room.name events automatically
|
||||
"(%s) %s :: %s", room.name, event.getSender(), event.getContent().body
|
||||
);
|
||||
});
|
||||
matrixClient.on("Room.timeline", function (event, room, toStartOfTimeline) {
|
||||
if (toStartOfTimeline) {
|
||||
return; // don't print paginated results
|
||||
}
|
||||
if (event.getType() !== "m.room.message") {
|
||||
return; // only print messages
|
||||
}
|
||||
console.log(
|
||||
// the room name will update with m.room.name events automatically
|
||||
"(%s) %s :: %s",
|
||||
room.name,
|
||||
event.getSender(),
|
||||
event.getContent().body,
|
||||
);
|
||||
});
|
||||
|
||||
matrixClient.startClient();
|
||||
matrixClient.startClient();
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
(My Room) @megan:localhost :: Hello world
|
||||
(My Room) @megan:localhost :: how are you?
|
||||
@@ -259,27 +263,24 @@ Output:
|
||||
### Print out membership lists whenever they are changed
|
||||
|
||||
```javascript
|
||||
matrixClient.on("RoomState.members", function(event, state, member) {
|
||||
const room = matrixClient.getRoom(state.roomId);
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
const memberList = state.getMembers();
|
||||
console.log(room.name);
|
||||
console.log(Array(room.name.length + 1).join("=")); // underline
|
||||
for (var i = 0; i < memberList.length; i++) {
|
||||
console.log(
|
||||
"(%s) %s",
|
||||
memberList[i].membership,
|
||||
memberList[i].name
|
||||
);
|
||||
}
|
||||
});
|
||||
matrixClient.on("RoomState.members", function (event, state, member) {
|
||||
const room = matrixClient.getRoom(state.roomId);
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
const memberList = state.getMembers();
|
||||
console.log(room.name);
|
||||
console.log(Array(room.name.length + 1).join("=")); // underline
|
||||
for (var i = 0; i < memberList.length; i++) {
|
||||
console.log("(%s) %s", memberList[i].membership, memberList[i].name);
|
||||
}
|
||||
});
|
||||
|
||||
matrixClient.startClient();
|
||||
matrixClient.startClient();
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
My Room
|
||||
=======
|
||||
@@ -289,8 +290,7 @@ Output:
|
||||
(invite) @charlie:localhost
|
||||
```
|
||||
|
||||
API Reference
|
||||
=============
|
||||
# API Reference
|
||||
|
||||
A hosted reference can be found at
|
||||
http://matrix-org.github.io/matrix-js-sdk/index.html
|
||||
@@ -304,21 +304,20 @@ host the API reference from the source files like this:
|
||||
$ python -m http.server 8005
|
||||
```
|
||||
|
||||
Then visit ``http://localhost:8005`` to see the API docs.
|
||||
Then visit `http://localhost:8005` to see the API docs.
|
||||
|
||||
End-to-end encryption support
|
||||
=============================
|
||||
# End-to-end encryption support
|
||||
|
||||
The SDK supports end-to-end encryption via the Olm and Megolm protocols, using
|
||||
[libolm](https://gitlab.matrix.org/matrix-org/olm). It is left up to the
|
||||
application to make libolm available, via the ``Olm`` global.
|
||||
application to make libolm available, via the `Olm` global.
|
||||
|
||||
It is also necessary to call ``await matrixClient.initCrypto()`` after creating a new
|
||||
``MatrixClient`` (but **before** calling ``matrixClient.startClient()``) to
|
||||
It is also necessary to call `await matrixClient.initCrypto()` after creating a new
|
||||
`MatrixClient` (but **before** calling `matrixClient.startClient()`) to
|
||||
initialise the crypto layer.
|
||||
|
||||
If the ``Olm`` global is not available, the SDK will show a warning, as shown
|
||||
below; ``initCrypto()`` will also fail.
|
||||
If the `Olm` global is not available, the SDK will show a warning, as shown
|
||||
below; `initCrypto()` will also fail.
|
||||
|
||||
```
|
||||
Unable to load crypto module: crypto will be disabled: Error: global.Olm is not defined
|
||||
@@ -330,46 +329,51 @@ specification.
|
||||
|
||||
To provide the Olm library in a browser application:
|
||||
|
||||
* download the transpiled libolm (from https://packages.matrix.org/npm/olm/).
|
||||
* load ``olm.js`` as a ``<script>`` *before* ``browser-matrix.js``.
|
||||
- download the transpiled libolm (from https://packages.matrix.org/npm/olm/).
|
||||
- load `olm.js` as a `<script>` _before_ `browser-matrix.js`.
|
||||
|
||||
To provide the Olm library in a node.js application:
|
||||
|
||||
* ``yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz``
|
||||
(replace the URL with the latest version you want to use from
|
||||
- `yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz`
|
||||
(replace the URL with the latest version you want to use from
|
||||
https://packages.matrix.org/npm/olm/)
|
||||
* ``global.Olm = require('olm');`` *before* loading ``matrix-js-sdk``.
|
||||
- `global.Olm = require('olm');` _before_ loading `matrix-js-sdk`.
|
||||
|
||||
If you want to package Olm as dependency for your node.js application, you can
|
||||
use ``yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz``. If your
|
||||
application also works without e2e crypto enabled, add ``--optional`` to mark it
|
||||
use `yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz`. If your
|
||||
application also works without e2e crypto enabled, add `--optional` to mark it
|
||||
as an optional dependency.
|
||||
|
||||
# Contributing
|
||||
|
||||
Contributing
|
||||
============
|
||||
*This section is for people who want to modify the SDK. If you just
|
||||
want to use this SDK, skip this section.*
|
||||
_This section is for people who want to modify the SDK. If you just
|
||||
want to use this SDK, skip this section._
|
||||
|
||||
First, you need to pull in the right build tools:
|
||||
|
||||
```
|
||||
$ yarn install
|
||||
```
|
||||
|
||||
Building
|
||||
--------
|
||||
## Building
|
||||
|
||||
To build a browser version from scratch when developing::
|
||||
|
||||
```
|
||||
$ yarn build
|
||||
```
|
||||
|
||||
To run tests (Jasmine)::
|
||||
To run tests (Jest):
|
||||
|
||||
```
|
||||
$ yarn test
|
||||
```
|
||||
|
||||
> **Note**
|
||||
> The `sync-browserify.spec.ts` requires a browser build (`yarn build`) in order to pass
|
||||
|
||||
To run linting:
|
||||
|
||||
```
|
||||
$ yarn lint
|
||||
```
|
||||
|
||||
+27
-27
@@ -20,19 +20,19 @@ blurrier.
|
||||
|
||||
When we are low on disk space overall or near the group limit / origin quota:
|
||||
|
||||
* Chrome
|
||||
* Log database may fail to start with AbortError
|
||||
* IndexedDB fails to start for crypto: AbortError in connect from
|
||||
indexeddb-store-worker
|
||||
* When near the quota, QuotaExceededError is used more consistently
|
||||
* Firefox
|
||||
* The first error will be QuotaExceededError
|
||||
* Future write attempts will fail with various errors when space is low,
|
||||
including nonsense like "InvalidStateError: A mutation operation was
|
||||
attempted on a database that did not allow mutations."
|
||||
* Once you start getting errors, the DB is effectively wedged in read-only
|
||||
mode
|
||||
* Can revive access if you reopen the DB
|
||||
- Chrome
|
||||
- Log database may fail to start with AbortError
|
||||
- IndexedDB fails to start for crypto: AbortError in connect from
|
||||
indexeddb-store-worker
|
||||
- When near the quota, QuotaExceededError is used more consistently
|
||||
- Firefox
|
||||
- The first error will be QuotaExceededError
|
||||
- Future write attempts will fail with various errors when space is low,
|
||||
including nonsense like "InvalidStateError: A mutation operation was
|
||||
attempted on a database that did not allow mutations."
|
||||
- Once you start getting errors, the DB is effectively wedged in read-only
|
||||
mode
|
||||
- Can revive access if you reopen the DB
|
||||
|
||||
## Cache Eviction
|
||||
|
||||
@@ -41,9 +41,9 @@ limited by a single quota, in practice, browsers appear to handle `localStorage`
|
||||
separately from the others, so it has a separate quota limit and isn't evicted
|
||||
when low on space.
|
||||
|
||||
* Chrome, Firefox
|
||||
* IndexedDB for origin deleted
|
||||
* Local Storage remains in place
|
||||
- Chrome, Firefox
|
||||
- IndexedDB for origin deleted
|
||||
- Local Storage remains in place
|
||||
|
||||
## Persistent Storage
|
||||
|
||||
@@ -51,20 +51,20 @@ Storage Standard offers a `navigator.storage.persist` API that can be used to
|
||||
request persistent storage that won't be deleted by the browser because of low
|
||||
space.
|
||||
|
||||
* Chrome
|
||||
* Chrome 75 seems to grant this without any prompt based on [interaction
|
||||
criteria](https://developers.google.com/web/updates/2016/06/persistent-storage)
|
||||
* Firefox
|
||||
* Firefox 67 shows a prompt to grant
|
||||
* Reverting persistent seems to require revoking permission _and_ clearing
|
||||
site data
|
||||
- Chrome
|
||||
- Chrome 75 seems to grant this without any prompt based on [interaction
|
||||
criteria](https://developers.google.com/web/updates/2016/06/persistent-storage)
|
||||
- Firefox
|
||||
- Firefox 67 shows a prompt to grant
|
||||
- Reverting persistent seems to require revoking permission _and_ clearing
|
||||
site data
|
||||
|
||||
## Storage Estimation
|
||||
|
||||
Storage Standard offers a `navigator.storage.estimate` API to get some clue of
|
||||
how much space remains.
|
||||
|
||||
* Chrome, Firefox
|
||||
* Can run this at any time to request an estimate of space remaining
|
||||
* Firefox
|
||||
* Returns `0` for `usage` if a site is persisted
|
||||
- Chrome, Firefox
|
||||
- Can run this at any time to request an estimate of space remaining
|
||||
- Firefox
|
||||
- Returns `0` for `usage` if a site is persisted
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
To try it out, **you must build the SDK first** and then host this folder:
|
||||
|
||||
```
|
||||
$ npm run build
|
||||
$ yarn install
|
||||
$ yarn build
|
||||
$ cd examples/browser
|
||||
$ python -m SimpleHTTPServer 8003
|
||||
$ python -m http.server 8003
|
||||
```
|
||||
|
||||
Then visit ``http://localhost:8003``.
|
||||
Then visit `http://localhost:8003`.
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
console.log("Loading browser sdk");
|
||||
|
||||
var client = matrixcs.createClient("https://matrix.org");
|
||||
client.publicRooms(function (err, data) {
|
||||
if (err) {
|
||||
console.error("err %s", JSON.stringify(err));
|
||||
return;
|
||||
}
|
||||
var client = matrixcs.createClient({ baseUrl: "https://matrix.org" });
|
||||
client.publicRooms().then(function (data) {
|
||||
console.log("data %s [...]", JSON.stringify(data).substring(0, 100));
|
||||
console.log("Congratulations! The SDK is working on the browser!");
|
||||
var result = document.getElementById("result");
|
||||
|
||||
+15
-16
@@ -1,18 +1,17 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Test</title>
|
||||
<meta charset="utf-8"/>
|
||||
<link rel="icon" href="data:,">
|
||||
<script src="lib/matrix.js"></script>
|
||||
<script src="browserTest.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
Sanity Testing (check the console) : This example is here to make sure that
|
||||
the SDK works inside a browser. It simply does a GET /publicRooms on
|
||||
matrix.org
|
||||
<br/>
|
||||
You should see a message confirming that the SDK works below:
|
||||
<br/>
|
||||
<div id="result"></div>
|
||||
</body>
|
||||
<head>
|
||||
<title>Test</title>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="data:," />
|
||||
<script src="lib/matrix.js"></script>
|
||||
<script src="browserTest.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
Sanity Testing (check the console) : This example is here to make sure that the SDK works inside a browser. It
|
||||
simply does a GET /publicRooms on matrix.org
|
||||
<br />
|
||||
You should see a message confirming that the SDK works below:
|
||||
<br />
|
||||
<div id="result"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,59 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Test Crypto in Browser</title>
|
||||
<script src="lib/olm.js"></script>
|
||||
<script src="lib/matrix.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Testing export/import of Olm devices in the browser</h1>
|
||||
<ul>
|
||||
<li>
|
||||
Make sure you built the current version of the Matrix JS SDK
|
||||
(<code>yarn build</code>)
|
||||
</li>
|
||||
<li>
|
||||
copy <code>olm.js</code> and <code>olm.wasm</code>
|
||||
from a recent release of Olm (was tested with version 3.1.4)
|
||||
in directory <code>lib/</code>
|
||||
</li>
|
||||
<li>start a local Matrix homeserver (on port 8008, or change the port in the code)</li>
|
||||
<li>Serve this HTML file (e.g. <code>python3 -m http.server</code>) and go to it through your browser</li>
|
||||
<li>
|
||||
in the JS console, do:
|
||||
<pre>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<title>Test Crypto in Browser</title>
|
||||
<script src="lib/olm.js"></script>
|
||||
<script src="lib/matrix.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Testing export/import of Olm devices in the browser</h1>
|
||||
<ul>
|
||||
<li>Make sure you built the current version of the Matrix JS SDK (<code>yarn build</code>)</li>
|
||||
<li>
|
||||
copy <code>olm.js</code> and <code>olm.wasm</code> from a recent release of Olm (was tested with version
|
||||
3.1.4) in directory <code>lib/</code>
|
||||
</li>
|
||||
<li>start a local Matrix homeserver (on port 8008, or change the port in the code)</li>
|
||||
<li>Serve this HTML file (e.g. <code>python3 -m http.server</code>) and go to it through your browser</li>
|
||||
<li>
|
||||
in the JS console, do:
|
||||
<pre>
|
||||
aliceMatrixClient = await newMatrixClient("alice-"+randomHex());
|
||||
await aliceMatrixClient.exportDevice();
|
||||
await aliceMatrixClient.getAccessToken();
|
||||
</pre>
|
||||
</li>
|
||||
<li>
|
||||
copy the result of <code>exportDevice</code> and <code>getAccessToken</code> somewhere
|
||||
(<strong>not</strong> in a JS variable as it will be destroyed when you refresh the page)
|
||||
</li>
|
||||
<li><strong>refresh the page (F5)</strong> to make sure the client is destroyed</li>
|
||||
<li>
|
||||
Do the following, replacing <code>ALICE_ID</code>
|
||||
with the user ID of Alice (you can find it in the exported data)
|
||||
<pre>
|
||||
</pre
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
copy the result of <code>exportDevice</code> and <code>getAccessToken</code> somewhere (<strong
|
||||
>not</strong
|
||||
>
|
||||
in a JS variable as it will be destroyed when you refresh the page)
|
||||
</li>
|
||||
<li><strong>refresh the page (F5)</strong> to make sure the client is destroyed</li>
|
||||
<li>
|
||||
Do the following, replacing <code>ALICE_ID</code>
|
||||
with the user ID of Alice (you can find it in the exported data)
|
||||
<pre>
|
||||
bobMatrixClient = await newMatrixClient("bob-"+randomHex());
|
||||
roomId = await bobMatrixClient.createEncryptedRoom([ALICE_ID]);
|
||||
await bobMatrixClient.sendTextMessage('Hi Alice!', roomId);
|
||||
</pre>
|
||||
</li>
|
||||
<li>Again, <strong>refresh the page (F5)</strong>. You may want to clear your console as well.</li>
|
||||
<li>
|
||||
Now do the following, using the exported data and the access token you saved previously:
|
||||
<pre>
|
||||
</pre
|
||||
>
|
||||
</li>
|
||||
<li>Again, <strong>refresh the page (F5)</strong>. You may want to clear your console as well.</li>
|
||||
<li>
|
||||
Now do the following, using the exported data and the access token you saved previously:
|
||||
<pre>
|
||||
aliceMatrixClient = await importMatrixClient(EXPORTED_DATA, ACCESS_TOKEN);
|
||||
</pre>
|
||||
</li>
|
||||
<li>You should see the message sent by Bob printed in the console.</li>
|
||||
</ul>
|
||||
</pre
|
||||
>
|
||||
</li>
|
||||
<li>You should see the message sent by Bob printed in the console.</li>
|
||||
</ul>
|
||||
|
||||
<script src="olm-device-export-import.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
<script src="olm-device-export-import.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,34 +1,26 @@
|
||||
if (!Olm) {
|
||||
console.error(
|
||||
"global.Olm does not seem to be present."
|
||||
+ " Did you forget to add olm in the lib/ directory?"
|
||||
);
|
||||
console.error("global.Olm does not seem to be present." + " Did you forget to add olm in the lib/ directory?");
|
||||
}
|
||||
|
||||
const BASE_URL = 'http://localhost:8008';
|
||||
const ROOM_CRYPTO_CONFIG = { algorithm: 'm.megolm.v1.aes-sha2' };
|
||||
const PASSWORD = 'password';
|
||||
const BASE_URL = "http://localhost:8008";
|
||||
const ROOM_CRYPTO_CONFIG = { algorithm: "m.megolm.v1.aes-sha2" };
|
||||
const PASSWORD = "password";
|
||||
|
||||
// useful to create new usernames
|
||||
window.randomHex = () => Math.floor(Math.random() * (10**6)).toString(16);
|
||||
window.randomHex = () => Math.floor(Math.random() * 10 ** 6).toString(16);
|
||||
|
||||
window.newMatrixClient = async function (username) {
|
||||
const registrationClient = matrixcs.createClient(BASE_URL);
|
||||
|
||||
const userRegisterResult = await registrationClient.register(
|
||||
username,
|
||||
PASSWORD,
|
||||
null,
|
||||
{ type: 'm.login.dummy' }
|
||||
);
|
||||
|
||||
const userRegisterResult = await registrationClient.register(username, PASSWORD, null, { type: "m.login.dummy" });
|
||||
|
||||
const matrixClient = matrixcs.createClient({
|
||||
baseUrl: BASE_URL,
|
||||
userId: userRegisterResult.user_id,
|
||||
accessToken: userRegisterResult.access_token,
|
||||
deviceId: userRegisterResult.device_id,
|
||||
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
|
||||
cryptoStore: new matrixcs.MemoryCryptoStore(),
|
||||
baseUrl: BASE_URL,
|
||||
userId: userRegisterResult.user_id,
|
||||
accessToken: userRegisterResult.access_token,
|
||||
deviceId: userRegisterResult.device_id,
|
||||
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
|
||||
cryptoStore: new matrixcs.MemoryCryptoStore(),
|
||||
});
|
||||
|
||||
extendMatrixClient(matrixClient);
|
||||
@@ -36,15 +28,15 @@ window.newMatrixClient = async function (username) {
|
||||
await matrixClient.initCrypto();
|
||||
await matrixClient.startClient();
|
||||
return matrixClient;
|
||||
}
|
||||
};
|
||||
|
||||
window.importMatrixClient = async function (exportedDevice, accessToken) {
|
||||
const matrixClient = matrixcs.createClient({
|
||||
baseUrl: BASE_URL,
|
||||
deviceToImport: exportedDevice,
|
||||
accessToken,
|
||||
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
|
||||
cryptoStore: new matrixcs.MemoryCryptoStore(),
|
||||
baseUrl: BASE_URL,
|
||||
deviceToImport: exportedDevice,
|
||||
accessToken,
|
||||
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
|
||||
cryptoStore: new matrixcs.MemoryCryptoStore(),
|
||||
});
|
||||
|
||||
extendMatrixClient(matrixClient);
|
||||
@@ -52,71 +44,62 @@ window.importMatrixClient = async function (exportedDevice, accessToken) {
|
||||
await matrixClient.initCrypto();
|
||||
await matrixClient.startClient();
|
||||
return matrixClient;
|
||||
}
|
||||
};
|
||||
|
||||
function extendMatrixClient(matrixClient) {
|
||||
// automatic join
|
||||
matrixClient.on('RoomMember.membership', async (event, member) => {
|
||||
if (member.membership === 'invite' && member.userId === matrixClient.getUserId()) {
|
||||
matrixClient.on("RoomMember.membership", async (event, member) => {
|
||||
if (member.membership === "invite" && member.userId === matrixClient.getUserId()) {
|
||||
await matrixClient.joinRoom(member.roomId);
|
||||
// setting up of room encryption seems to be triggered automatically
|
||||
// but if we don't wait for it the first messages we send are unencrypted
|
||||
await matrixClient.setRoomEncryption(member.roomId, { algorithm: 'm.megolm.v1.aes-sha2' })
|
||||
await matrixClient.setRoomEncryption(member.roomId, { algorithm: "m.megolm.v1.aes-sha2" });
|
||||
}
|
||||
});
|
||||
|
||||
matrixClient.onDecryptedMessage = message => {
|
||||
console.log('Got encrypted message: ', message);
|
||||
}
|
||||
matrixClient.onDecryptedMessage = (message) => {
|
||||
console.log("Got encrypted message: ", message);
|
||||
};
|
||||
|
||||
matrixClient.on('Event.decrypted', (event) => {
|
||||
if (event.getType() === 'm.room.message'){
|
||||
matrixClient.on("Event.decrypted", (event) => {
|
||||
if (event.getType() === "m.room.message") {
|
||||
matrixClient.onDecryptedMessage(event.getContent().body);
|
||||
} else {
|
||||
console.log('decrypted an event of type', event.getType());
|
||||
console.log("decrypted an event of type", event.getType());
|
||||
console.log(event);
|
||||
}
|
||||
});
|
||||
|
||||
matrixClient.createEncryptedRoom = async function(usersToInvite) {
|
||||
const {
|
||||
room_id: roomId,
|
||||
} = await this.createRoom({
|
||||
visibility: 'private',
|
||||
invite: usersToInvite,
|
||||
|
||||
matrixClient.createEncryptedRoom = async function (usersToInvite) {
|
||||
const { room_id: roomId } = await this.createRoom({
|
||||
visibility: "private",
|
||||
invite: usersToInvite,
|
||||
});
|
||||
|
||||
// matrixClient.setRoomEncryption() only updates local state
|
||||
// but does not send anything to the server
|
||||
// (see https://github.com/matrix-org/matrix-js-sdk/issues/905)
|
||||
// so we do it ourselves with 'sendStateEvent'
|
||||
await this.sendStateEvent(
|
||||
roomId, 'm.room.encryption', ROOM_CRYPTO_CONFIG,
|
||||
);
|
||||
await this.setRoomEncryption(
|
||||
roomId, ROOM_CRYPTO_CONFIG,
|
||||
);
|
||||
await this.sendStateEvent(roomId, "m.room.encryption", ROOM_CRYPTO_CONFIG);
|
||||
await this.setRoomEncryption(roomId, ROOM_CRYPTO_CONFIG);
|
||||
|
||||
// Marking all devices as verified
|
||||
let room = this.getRoom(roomId);
|
||||
let members = (await room.getEncryptionTargetMembers()).map(x => x["userId"])
|
||||
let members = (await room.getEncryptionTargetMembers()).map((x) => x["userId"]);
|
||||
let memberkeys = await this.downloadKeys(members);
|
||||
for (const userId in memberkeys) {
|
||||
for (const deviceId in memberkeys[userId]) {
|
||||
await this.setDeviceVerified(userId, deviceId);
|
||||
}
|
||||
for (const deviceId in memberkeys[userId]) {
|
||||
await this.setDeviceVerified(userId, deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
return roomId;
|
||||
}
|
||||
};
|
||||
|
||||
matrixClient.sendTextMessage = async function(message, roomId) {
|
||||
return matrixClient.sendMessage(
|
||||
roomId,
|
||||
{
|
||||
body: message,
|
||||
msgtype: 'm.text',
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
matrixClient.sendTextMessage = async function (message, roomId) {
|
||||
return matrixClient.sendMessage(roomId, {
|
||||
body: message,
|
||||
msgtype: "m.text",
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
This is a functional terminal app which allows you to see the room list for a user, join rooms, send messages and view room membership lists.
|
||||
|
||||
|
||||
To try it out, you will need to edit `app.js` to configure it for your `homeserver`, `access_token` and `user_id`. Then run:
|
||||
|
||||
```
|
||||
@@ -24,7 +23,7 @@ Room list index commands:
|
||||
Room commands:
|
||||
'/exit' Return to the room list index.
|
||||
'/members' Show the room member list.
|
||||
|
||||
|
||||
$ /enter 2
|
||||
|
||||
[2015-06-12 15:14:54] Megan2 <<< herro
|
||||
|
||||
+140
-152
@@ -5,7 +5,7 @@ var clc = require("cli-color");
|
||||
var matrixClient = sdk.createClient({
|
||||
baseUrl: "http://localhost:8008",
|
||||
accessToken: myAccessToken,
|
||||
userId: myUserId
|
||||
userId: myUserId,
|
||||
});
|
||||
|
||||
// Data structures
|
||||
@@ -14,15 +14,15 @@ var viewingRoom = null;
|
||||
var numMessagesToShow = 20;
|
||||
|
||||
// Reading from stdin
|
||||
var CLEAR_CONSOLE = '\x1B[2J';
|
||||
var CLEAR_CONSOLE = "\x1B[2J";
|
||||
var readline = require("readline");
|
||||
var rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
completer: completer
|
||||
completer: completer,
|
||||
});
|
||||
rl.setPrompt("$ ");
|
||||
rl.on('line', function(line) {
|
||||
rl.on("line", function (line) {
|
||||
if (line.trim().length === 0) {
|
||||
rl.prompt();
|
||||
return;
|
||||
@@ -37,14 +37,11 @@ rl.on('line', function(line) {
|
||||
if (line === "/exit") {
|
||||
viewingRoom = null;
|
||||
printRoomList();
|
||||
}
|
||||
else if (line === "/members") {
|
||||
} else if (line === "/members") {
|
||||
printMemberList(viewingRoom);
|
||||
}
|
||||
else if (line === "/roominfo") {
|
||||
} else if (line === "/roominfo") {
|
||||
printRoomInfo(viewingRoom);
|
||||
}
|
||||
else if (line === "/resend") {
|
||||
} else if (line === "/resend") {
|
||||
// get the oldest not sent event.
|
||||
var notSentEvent;
|
||||
for (var i = 0; i < viewingRoom.timeline.length; i++) {
|
||||
@@ -54,76 +51,84 @@ rl.on('line', function(line) {
|
||||
}
|
||||
}
|
||||
if (notSentEvent) {
|
||||
matrixClient.resendEvent(notSentEvent, viewingRoom).then(function() {
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
}, function(err) {
|
||||
printMessages();
|
||||
print("/resend Error: %s", err);
|
||||
rl.prompt();
|
||||
});
|
||||
matrixClient.resendEvent(notSentEvent, viewingRoom).then(
|
||||
function () {
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
},
|
||||
function (err) {
|
||||
printMessages();
|
||||
print("/resend Error: %s", err);
|
||||
rl.prompt();
|
||||
},
|
||||
);
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
}
|
||||
}
|
||||
else if (line.indexOf("/more ") === 0) {
|
||||
} else if (line.indexOf("/more ") === 0) {
|
||||
var amount = parseInt(line.split(" ")[1]) || 20;
|
||||
matrixClient.scrollback(viewingRoom, amount).then(function(room) {
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
}, function(err) {
|
||||
print("/more Error: %s", err);
|
||||
});
|
||||
}
|
||||
else if (line.indexOf("/invite ") === 0) {
|
||||
matrixClient.scrollback(viewingRoom, amount).then(
|
||||
function (room) {
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
},
|
||||
function (err) {
|
||||
print("/more Error: %s", err);
|
||||
},
|
||||
);
|
||||
} else if (line.indexOf("/invite ") === 0) {
|
||||
var userId = line.split(" ")[1].trim();
|
||||
matrixClient.invite(viewingRoom.roomId, userId).then(function() {
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
}, function(err) {
|
||||
print("/invite Error: %s", err);
|
||||
});
|
||||
}
|
||||
else if (line.indexOf("/file ") === 0) {
|
||||
matrixClient.invite(viewingRoom.roomId, userId).then(
|
||||
function () {
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
},
|
||||
function (err) {
|
||||
print("/invite Error: %s", err);
|
||||
},
|
||||
);
|
||||
} else if (line.indexOf("/file ") === 0) {
|
||||
var filename = line.split(" ")[1].trim();
|
||||
var stream = fs.createReadStream(filename);
|
||||
matrixClient.uploadContent({
|
||||
stream: stream,
|
||||
name: filename
|
||||
}).then(function(url) {
|
||||
var content = {
|
||||
msgtype: "m.file",
|
||||
body: filename,
|
||||
url: JSON.parse(url).content_uri
|
||||
};
|
||||
matrixClient.sendMessage(viewingRoom.roomId, content);
|
||||
});
|
||||
}
|
||||
else {
|
||||
matrixClient.sendTextMessage(viewingRoom.roomId, line).finally(function() {
|
||||
matrixClient
|
||||
.uploadContent({
|
||||
stream: stream,
|
||||
name: filename,
|
||||
})
|
||||
.then(function (url) {
|
||||
var content = {
|
||||
msgtype: "m.file",
|
||||
body: filename,
|
||||
url: JSON.parse(url).content_uri,
|
||||
};
|
||||
matrixClient.sendMessage(viewingRoom.roomId, content);
|
||||
});
|
||||
} else {
|
||||
matrixClient.sendTextMessage(viewingRoom.roomId, line).finally(function () {
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
});
|
||||
// print local echo immediately
|
||||
printMessages();
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (line.indexOf("/join ") === 0) {
|
||||
var roomIndex = line.split(" ")[1];
|
||||
viewingRoom = roomList[roomIndex];
|
||||
if (viewingRoom.getMember(myUserId).membership === "invite") {
|
||||
// join the room first
|
||||
matrixClient.joinRoom(viewingRoom.roomId).then(function(room) {
|
||||
setRoomList();
|
||||
viewingRoom = room;
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
}, function(err) {
|
||||
print("/join Error: %s", err);
|
||||
});
|
||||
}
|
||||
else {
|
||||
matrixClient.joinRoom(viewingRoom.roomId).then(
|
||||
function (room) {
|
||||
setRoomList();
|
||||
viewingRoom = room;
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
},
|
||||
function (err) {
|
||||
print("/join Error: %s", err);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
printMessages();
|
||||
}
|
||||
}
|
||||
@@ -133,18 +138,18 @@ rl.on('line', function(line) {
|
||||
// ==== END User input
|
||||
|
||||
// show the room list after syncing.
|
||||
matrixClient.on("sync", function(state, prevState, data) {
|
||||
matrixClient.on("sync", function (state, prevState, data) {
|
||||
switch (state) {
|
||||
case "PREPARED":
|
||||
setRoomList();
|
||||
printRoomList();
|
||||
printHelp();
|
||||
rl.prompt();
|
||||
break;
|
||||
}
|
||||
setRoomList();
|
||||
printRoomList();
|
||||
printHelp();
|
||||
rl.prompt();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
matrixClient.on("Room", function() {
|
||||
matrixClient.on("Room", function () {
|
||||
setRoomList();
|
||||
if (!viewingRoom) {
|
||||
printRoomList();
|
||||
@@ -153,7 +158,7 @@ matrixClient.on("Room", function() {
|
||||
});
|
||||
|
||||
// print incoming messages.
|
||||
matrixClient.on("Room.timeline", function(event, room, toStartOfTimeline) {
|
||||
matrixClient.on("Room.timeline", function (event, room, toStartOfTimeline) {
|
||||
if (toStartOfTimeline) {
|
||||
return; // don't print paginated results
|
||||
}
|
||||
@@ -165,20 +170,19 @@ matrixClient.on("Room.timeline", function(event, room, toStartOfTimeline) {
|
||||
|
||||
function setRoomList() {
|
||||
roomList = matrixClient.getRooms();
|
||||
roomList.sort(function(a,b) {
|
||||
roomList.sort(function (a, b) {
|
||||
// < 0 = a comes first (lower index) - we want high indexes = newer
|
||||
var aMsg = a.timeline[a.timeline.length-1];
|
||||
var aMsg = a.timeline[a.timeline.length - 1];
|
||||
if (!aMsg) {
|
||||
return -1;
|
||||
}
|
||||
var bMsg = b.timeline[b.timeline.length-1];
|
||||
var bMsg = b.timeline[b.timeline.length - 1];
|
||||
if (!bMsg) {
|
||||
return 1;
|
||||
}
|
||||
if (aMsg.getTs() > bMsg.getTs()) {
|
||||
return 1;
|
||||
}
|
||||
else if (aMsg.getTs() < bMsg.getTs()) {
|
||||
} else if (aMsg.getTs() < bMsg.getTs()) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
@@ -189,16 +193,15 @@ function printRoomList() {
|
||||
print(CLEAR_CONSOLE);
|
||||
print("Room List:");
|
||||
var fmts = {
|
||||
"invite": clc.cyanBright,
|
||||
"leave": clc.blackBright
|
||||
invite: clc.cyanBright,
|
||||
leave: clc.blackBright,
|
||||
};
|
||||
for (var i = 0; i < roomList.length; i++) {
|
||||
var msg = roomList[i].timeline[roomList[i].timeline.length-1];
|
||||
var msg = roomList[i].timeline[roomList[i].timeline.length - 1];
|
||||
var dateStr = "---";
|
||||
var fmt;
|
||||
if (msg) {
|
||||
dateStr = new Date(msg.getTs()).toISOString().replace(
|
||||
/T/, ' ').replace(/\..+/, '');
|
||||
dateStr = new Date(msg.getTs()).toISOString().replace(/T/, " ").replace(/\..+/, "");
|
||||
}
|
||||
var myMembership = roomList[i].getMyMembership();
|
||||
if (myMembership) {
|
||||
@@ -207,9 +210,10 @@ function printRoomList() {
|
||||
var roomName = fixWidth(roomList[i].name, 25);
|
||||
print(
|
||||
"[%s] %s (%s members) %s",
|
||||
i, fmt ? fmt(roomName) : roomName,
|
||||
i,
|
||||
fmt ? fmt(roomName) : roomName,
|
||||
roomList[i].getJoinedMembers().length,
|
||||
dateStr
|
||||
dateStr,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -230,12 +234,12 @@ function printHelp() {
|
||||
}
|
||||
|
||||
function completer(line) {
|
||||
var completions = [
|
||||
"/help", "/join ", "/exit", "/members", "/more ", "/resend", "/invite"
|
||||
];
|
||||
var hits = completions.filter(function(c) { return c.indexOf(line) == 0 });
|
||||
var completions = ["/help", "/join ", "/exit", "/members", "/more ", "/resend", "/invite"];
|
||||
var hits = completions.filter(function (c) {
|
||||
return c.indexOf(line) == 0;
|
||||
});
|
||||
// show all completions if none found
|
||||
return [hits.length ? hits : completions, line]
|
||||
return [hits.length ? hits : completions, line];
|
||||
}
|
||||
|
||||
function printMessages() {
|
||||
@@ -252,14 +256,14 @@ function printMessages() {
|
||||
|
||||
function printMemberList(room) {
|
||||
var fmts = {
|
||||
"join": clc.green,
|
||||
"ban": clc.red,
|
||||
"invite": clc.blue,
|
||||
"leave": clc.blackBright
|
||||
join: clc.green,
|
||||
ban: clc.red,
|
||||
invite: clc.blue,
|
||||
leave: clc.blackBright,
|
||||
};
|
||||
var members = room.currentState.getMembers();
|
||||
// sorted based on name.
|
||||
members.sort(function(a, b) {
|
||||
members.sort(function (a, b) {
|
||||
if (a.name > b.name) {
|
||||
return -1;
|
||||
}
|
||||
@@ -268,21 +272,24 @@ function printMemberList(room) {
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
print("Membership list for room \"%s\"", room.name);
|
||||
print('Membership list for room "%s"', room.name);
|
||||
print(new Array(room.name.length + 28).join("-"));
|
||||
room.currentState.getMembers().forEach(function(member) {
|
||||
room.currentState.getMembers().forEach(function (member) {
|
||||
if (!member.membership) {
|
||||
return;
|
||||
}
|
||||
var fmt = fmts[member.membership] || function(a){return a;};
|
||||
var membershipWithPadding = (
|
||||
member.membership + new Array(10 - member.membership.length).join(" ")
|
||||
);
|
||||
var fmt =
|
||||
fmts[member.membership] ||
|
||||
function (a) {
|
||||
return a;
|
||||
};
|
||||
var membershipWithPadding = member.membership + new Array(10 - member.membership.length).join(" ");
|
||||
print(
|
||||
"%s"+fmt(" :: ")+"%s"+fmt(" (")+"%s"+fmt(")"),
|
||||
membershipWithPadding, member.name,
|
||||
(member.userId === myUserId ? "Me" : member.userId),
|
||||
fmt
|
||||
"%s" + fmt(" :: ") + "%s" + fmt(" (") + "%s" + fmt(")"),
|
||||
membershipWithPadding,
|
||||
member.name,
|
||||
member.userId === myUserId ? "Me" : member.userId,
|
||||
fmt,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -292,38 +299,31 @@ function printRoomInfo(room) {
|
||||
var eTypeHeader = " Event Type(state_key) ";
|
||||
var sendHeader = " Sender ";
|
||||
// pad content to 100
|
||||
var restCount = (
|
||||
100 - "Content".length - " | ".length - " | ".length -
|
||||
eTypeHeader.length - sendHeader.length
|
||||
);
|
||||
var padSide = new Array(Math.floor(restCount/2)).join(" ");
|
||||
var restCount = 100 - "Content".length - " | ".length - " | ".length - eTypeHeader.length - sendHeader.length;
|
||||
var padSide = new Array(Math.floor(restCount / 2)).join(" ");
|
||||
var contentHeader = padSide + "Content" + padSide;
|
||||
print(eTypeHeader+sendHeader+contentHeader);
|
||||
print(eTypeHeader + sendHeader + contentHeader);
|
||||
print(new Array(100).join("-"));
|
||||
eventMap.keys().forEach(function(eventType) {
|
||||
if (eventType === "m.room.member") { return; } // use /members instead.
|
||||
eventMap.keys().forEach(function (eventType) {
|
||||
if (eventType === "m.room.member") {
|
||||
return;
|
||||
} // use /members instead.
|
||||
var eventEventMap = eventMap.get(eventType);
|
||||
eventEventMap.keys().forEach(function(stateKey) {
|
||||
var typeAndKey = eventType + (
|
||||
stateKey.length > 0 ? "("+stateKey+")" : ""
|
||||
);
|
||||
eventEventMap.keys().forEach(function (stateKey) {
|
||||
var typeAndKey = eventType + (stateKey.length > 0 ? "(" + stateKey + ")" : "");
|
||||
var typeStr = fixWidth(typeAndKey, eTypeHeader.length);
|
||||
var event = eventEventMap.get(stateKey);
|
||||
var sendStr = fixWidth(event.getSender(), sendHeader.length);
|
||||
var contentStr = fixWidth(
|
||||
JSON.stringify(event.getContent()), contentHeader.length
|
||||
);
|
||||
print(typeStr+" | "+sendStr+" | "+contentStr);
|
||||
var contentStr = fixWidth(JSON.stringify(event.getContent()), contentHeader.length);
|
||||
print(typeStr + " | " + sendStr + " | " + contentStr);
|
||||
});
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function printLine(event) {
|
||||
var fmt;
|
||||
var name = event.sender ? event.sender.name : event.getSender();
|
||||
var time = new Date(
|
||||
event.getTs()
|
||||
).toISOString().replace(/T/, ' ').replace(/\..+/, '');
|
||||
var time = new Date(event.getTs()).toISOString().replace(/T/, " ").replace(/\..+/, "");
|
||||
var separator = "<<<";
|
||||
if (event.getSender() === myUserId) {
|
||||
name = "Me";
|
||||
@@ -331,8 +331,7 @@ function printLine(event) {
|
||||
if (event.status === sdk.EventStatus.SENDING) {
|
||||
separator = "...";
|
||||
fmt = clc.xterm(8);
|
||||
}
|
||||
else if (event.status === sdk.EventStatus.NOT_SENT) {
|
||||
} else if (event.status === sdk.EventStatus.NOT_SENT) {
|
||||
separator = " x ";
|
||||
fmt = clc.redBright;
|
||||
}
|
||||
@@ -341,69 +340,58 @@ function printLine(event) {
|
||||
|
||||
var maxNameWidth = 15;
|
||||
if (name.length > maxNameWidth) {
|
||||
name = name.slice(0, maxNameWidth-1) + "\u2026";
|
||||
name = name.slice(0, maxNameWidth - 1) + "\u2026";
|
||||
}
|
||||
|
||||
if (event.getType() === "m.room.message") {
|
||||
body = event.getContent().body;
|
||||
}
|
||||
else if (event.isState()) {
|
||||
} else if (event.isState()) {
|
||||
var stateName = event.getType();
|
||||
if (event.getStateKey().length > 0) {
|
||||
stateName += " ("+event.getStateKey()+")";
|
||||
stateName += " (" + event.getStateKey() + ")";
|
||||
}
|
||||
body = (
|
||||
"[State: "+stateName+" updated to: "+JSON.stringify(event.getContent())+"]"
|
||||
);
|
||||
body = "[State: " + stateName + " updated to: " + JSON.stringify(event.getContent()) + "]";
|
||||
separator = "---";
|
||||
fmt = clc.xterm(249).italic;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// random message event
|
||||
body = (
|
||||
"[Message: "+event.getType()+" Content: "+JSON.stringify(event.getContent())+"]"
|
||||
);
|
||||
body = "[Message: " + event.getType() + " Content: " + JSON.stringify(event.getContent()) + "]";
|
||||
separator = "---";
|
||||
fmt = clc.xterm(249).italic;
|
||||
}
|
||||
if (fmt) {
|
||||
print(
|
||||
"[%s] %s %s %s", time, name, separator, body, fmt
|
||||
);
|
||||
}
|
||||
else {
|
||||
print("[%s] %s %s %s", time, name, separator, body, fmt);
|
||||
} else {
|
||||
print("[%s] %s %s %s", time, name, separator, body);
|
||||
}
|
||||
}
|
||||
|
||||
function print(str, formatter) {
|
||||
if (typeof arguments[arguments.length-1] === "function") {
|
||||
if (typeof arguments[arguments.length - 1] === "function") {
|
||||
// last arg is the formatter so get rid of it and use it on each
|
||||
// param passed in but not the template string.
|
||||
var newArgs = [];
|
||||
var i = 0;
|
||||
for (i=0; i<arguments.length-1; i++) {
|
||||
for (i = 0; i < arguments.length - 1; i++) {
|
||||
newArgs.push(arguments[i]);
|
||||
}
|
||||
var fmt = arguments[arguments.length-1];
|
||||
for (i=0; i<newArgs.length; i++) {
|
||||
var fmt = arguments[arguments.length - 1];
|
||||
for (i = 0; i < newArgs.length; i++) {
|
||||
newArgs[i] = fmt(newArgs[i]);
|
||||
}
|
||||
console.log.apply(console.log, newArgs);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
console.log.apply(console.log, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
function fixWidth(str, len) {
|
||||
if (str.length > len) {
|
||||
return str.substring(0, len-2) + "\u2026";
|
||||
}
|
||||
else if (str.length < len) {
|
||||
return str.substring(0, len - 2) + "\u2026";
|
||||
} else if (str.length < len) {
|
||||
return str + new Array(len - str.length).join(" ");
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
matrixClient.startClient(numMessagesToShow); // messages for each room.
|
||||
matrixClient.startClient(numMessagesToShow); // messages for each room.
|
||||
|
||||
+12
-12
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "example-app",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"preinstall": "npm install ../.."
|
||||
},
|
||||
"author": "",
|
||||
"license": "Apache 2.0",
|
||||
"dependencies": {
|
||||
"cli-color": "^1.0.0"
|
||||
}
|
||||
"name": "example-app",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"preinstall": "npm install ../.."
|
||||
},
|
||||
"author": "",
|
||||
"license": "Apache 2.0",
|
||||
"dependencies": {
|
||||
"cli-color": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,4 +6,4 @@ To try it out, **you must build the SDK first** and then host this folder:
|
||||
$ python -m SimpleHTTPServer 8003
|
||||
```
|
||||
|
||||
Then visit ``http://localhost:8003``.
|
||||
Then visit `http://localhost:8003`.
|
||||
|
||||
@@ -9,7 +9,7 @@ const client = matrixcs.createClient({
|
||||
baseUrl: BASE_URL,
|
||||
accessToken: TOKEN,
|
||||
userId: USER_ID,
|
||||
deviceId: DEVICE_ID
|
||||
deviceId: DEVICE_ID,
|
||||
});
|
||||
let call;
|
||||
|
||||
@@ -21,18 +21,16 @@ function disableButtons(place, answer, hangup) {
|
||||
|
||||
function addListeners(call) {
|
||||
let lastError = "";
|
||||
call.on("hangup", function() {
|
||||
call.on("hangup", function () {
|
||||
disableButtons(false, true, true);
|
||||
document.getElementById("result").innerHTML = (
|
||||
"<p>Call ended. Last error: "+lastError+"</p>"
|
||||
);
|
||||
document.getElementById("result").innerHTML = "<p>Call ended. Last error: " + lastError + "</p>";
|
||||
});
|
||||
call.on("error", function(err) {
|
||||
call.on("error", function (err) {
|
||||
lastError = err.message;
|
||||
call.hangup();
|
||||
disableButtons(false, true, true);
|
||||
});
|
||||
call.on("feeds_changed", function(feeds) {
|
||||
call.on("feeds_changed", function (feeds) {
|
||||
const localFeed = feeds.find((feed) => feed.isLocal());
|
||||
const remoteFeed = feeds.find((feed) => !feed.isLocal());
|
||||
|
||||
@@ -51,33 +49,38 @@ function addListeners(call) {
|
||||
});
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
window.onload = function () {
|
||||
document.getElementById("result").innerHTML = "<p>Please wait. Syncing...</p>";
|
||||
document.getElementById("config").innerHTML = "<p>" +
|
||||
"Homeserver: <code>"+BASE_URL+"</code><br/>"+
|
||||
"Room: <code>"+ROOM_ID+"</code><br/>"+
|
||||
"User: <code>"+USER_ID+"</code><br/>"+
|
||||
document.getElementById("config").innerHTML =
|
||||
"<p>" +
|
||||
"Homeserver: <code>" +
|
||||
BASE_URL +
|
||||
"</code><br/>" +
|
||||
"Room: <code>" +
|
||||
ROOM_ID +
|
||||
"</code><br/>" +
|
||||
"User: <code>" +
|
||||
USER_ID +
|
||||
"</code><br/>" +
|
||||
"</p>";
|
||||
disableButtons(true, true, true);
|
||||
};
|
||||
|
||||
client.on("sync", function(state, prevState, data) {
|
||||
client.on("sync", function (state, prevState, data) {
|
||||
switch (state) {
|
||||
case "PREPARED":
|
||||
syncComplete();
|
||||
break;
|
||||
}
|
||||
syncComplete();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
function syncComplete() {
|
||||
document.getElementById("result").innerHTML = "<p>Ready for calls.</p>";
|
||||
disableButtons(false, true, true);
|
||||
|
||||
document.getElementById("call").onclick = function() {
|
||||
document.getElementById("call").onclick = function () {
|
||||
console.log("Placing call...");
|
||||
call = matrixcs.createNewMatrixCall(
|
||||
client, ROOM_ID
|
||||
);
|
||||
call = matrixcs.createNewMatrixCall(client, ROOM_ID);
|
||||
console.log("Call => %s", call);
|
||||
addListeners(call);
|
||||
call.placeVideoCall();
|
||||
@@ -85,14 +88,14 @@ function syncComplete() {
|
||||
disableButtons(true, true, false);
|
||||
};
|
||||
|
||||
document.getElementById("hangup").onclick = function() {
|
||||
document.getElementById("hangup").onclick = function () {
|
||||
console.log("Hanging up call...");
|
||||
console.log("Call => %s", call);
|
||||
call.hangup();
|
||||
document.getElementById("result").innerHTML = "<p>Hungup call.</p>";
|
||||
};
|
||||
|
||||
document.getElementById("answer").onclick = function() {
|
||||
document.getElementById("answer").onclick = function () {
|
||||
console.log("Answering call...");
|
||||
console.log("Call => %s", call);
|
||||
call.answer();
|
||||
@@ -100,7 +103,7 @@ function syncComplete() {
|
||||
document.getElementById("result").innerHTML = "<p>Answered call.</p>";
|
||||
};
|
||||
|
||||
client.on("Call.incoming", function(c) {
|
||||
client.on("Call.incoming", function (c) {
|
||||
console.log("Call ringing");
|
||||
disableButtons(true, false, false);
|
||||
document.getElementById("result").innerHTML = "<p>Incoming call...</p>";
|
||||
|
||||
+19
-21
@@ -1,25 +1,23 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>VoIP Test</title>
|
||||
<script src="lib/matrix.js"></script>
|
||||
<script src="browserTest.js"></script>
|
||||
</head>
|
||||
|
||||
<head>
|
||||
<title>VoIP Test</title>
|
||||
<script src="lib/matrix.js"></script>
|
||||
<script src="browserTest.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
You can place and receive calls with this example. Make sure to edit the
|
||||
constants in <code>browserTest.js</code> first.
|
||||
<div id="config"></div>
|
||||
<div id="result"></div>
|
||||
<button id="call">Place Call</button>
|
||||
<button id="answer">Answer Call</button>
|
||||
<button id="hangup">Hangup Call</button>
|
||||
<div id="videoBackground" class="video-background">
|
||||
<video class="video-element" id="local"></video>
|
||||
<video class="video-element" id="remote"></video>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<body>
|
||||
You can place and receive calls with this example. Make sure to edit the constants in
|
||||
<code>browserTest.js</code> first.
|
||||
<div id="config"></div>
|
||||
<div id="result"></div>
|
||||
<button id="call">Place Call</button>
|
||||
<button id="answer">Answer Call</button>
|
||||
<button id="hangup">Hangup Call</button>
|
||||
<div id="videoBackground" class="video-background">
|
||||
<video class="video-element" id="local"></video>
|
||||
<video class="video-element" id="remote"></video>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style>
|
||||
@@ -31,4 +29,4 @@
|
||||
.video-element {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
+144
-137
@@ -1,141 +1,148 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "21.2.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||
"dist": "echo 'This is for the release script so it can make assets (browser bundle).' && yarn build",
|
||||
"clean": "rimraf lib dist",
|
||||
"build": "yarn build:dev && yarn build:compile-browser && yarn build:minify-browser",
|
||||
"build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
|
||||
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
|
||||
"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": "typedoc",
|
||||
"lint": "yarn lint:types && yarn lint:js",
|
||||
"lint:js": "eslint --max-warnings 0 src spec",
|
||||
"lint:js-fix": "eslint --fix src spec",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"coverage": "yarn test --coverage"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/matrix-org/matrix-js-sdk"
|
||||
},
|
||||
"keywords": [
|
||||
"matrix-org"
|
||||
],
|
||||
"main": "./lib/index.js",
|
||||
"browser": "./lib/browser-index.js",
|
||||
"matrix_src_main": "./src/index.ts",
|
||||
"matrix_src_browser": "./src/browser-index.js",
|
||||
"matrix_lib_main": "./lib/index.js",
|
||||
"matrix_lib_typings": "./lib/index.d.ts",
|
||||
"author": "matrix.org",
|
||||
"license": "Apache-2.0",
|
||||
"files": [
|
||||
"dist",
|
||||
"lib",
|
||||
"src",
|
||||
"git-revision.txt",
|
||||
"CHANGELOG.md",
|
||||
"CONTRIBUTING.rst",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"package.json",
|
||||
"release.sh"
|
||||
],
|
||||
"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",
|
||||
"matrix-widget-api": "^1.0.0",
|
||||
"p-retry": "4",
|
||||
"qs": "^6.9.6",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"unhomoglyph": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.12.10",
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/eslint-parser": "^7.12.10",
|
||||
"@babel/eslint-plugin": "^7.12.10",
|
||||
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-runtime": "^7.12.10",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/register": "^7.12.10",
|
||||
"@casualbot/jest-sonar-reporter": "^2.2.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",
|
||||
"@types/domexception": "^4.0.0",
|
||||
"@types/jest": "^29.0.0",
|
||||
"@types/node": "16",
|
||||
"@typescript-eslint/eslint-plugin": "^5.6.0",
|
||||
"@typescript-eslint/parser": "^5.6.0",
|
||||
"allchange": "^1.0.6",
|
||||
"babel-jest": "^29.0.0",
|
||||
"babelify": "^10.0.0",
|
||||
"better-docs": "^2.4.0-beta.9",
|
||||
"browserify": "^17.0.0",
|
||||
"docdash": "^1.2.0",
|
||||
"domexception": "^4.0.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.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": "^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": {
|
||||
"testEnvironment": "node",
|
||||
"testMatch": [
|
||||
"<rootDir>/spec/**/*.spec.{js,ts}"
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "23.2.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||
"dist": "echo 'This is for the release script so it can make assets (browser bundle).' && yarn build",
|
||||
"clean": "rimraf lib dist",
|
||||
"build": "yarn build:dev && yarn build:compile-browser && yarn build:minify-browser",
|
||||
"build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
|
||||
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
|
||||
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
|
||||
"build:compile-browser": "mkdir dist && browserify -d src/browser-index.ts -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": "typedoc",
|
||||
"lint": "yarn lint:types && yarn lint:js",
|
||||
"lint:js": "eslint --max-warnings 0 src spec && prettier --check .",
|
||||
"lint:js-fix": "prettier --loglevel=warn --write . && eslint --fix src spec",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"coverage": "yarn test --coverage"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/matrix-org/matrix-js-sdk"
|
||||
},
|
||||
"keywords": [
|
||||
"matrix-org"
|
||||
],
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/spec/setupTests.ts"
|
||||
"main": "./lib/index.js",
|
||||
"browser": "./src/browser-index.ts",
|
||||
"matrix_src_main": "./src/index.ts",
|
||||
"matrix_src_browser": "./src/browser-index.ts",
|
||||
"matrix_lib_main": "./lib/index.js",
|
||||
"matrix_lib_browser": "./lib/browser-index.js",
|
||||
"matrix_lib_typings": "./lib/index.d.ts",
|
||||
"author": "matrix.org",
|
||||
"license": "Apache-2.0",
|
||||
"files": [
|
||||
"dist",
|
||||
"lib",
|
||||
"src",
|
||||
"git-revision.txt",
|
||||
"CHANGELOG.md",
|
||||
"CONTRIBUTING.rst",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"package.json",
|
||||
"release.sh"
|
||||
],
|
||||
"collectCoverageFrom": [
|
||||
"<rootDir>/src/**/*.{js,ts}"
|
||||
],
|
||||
"coverageReporters": [
|
||||
"text-summary",
|
||||
"lcov"
|
||||
],
|
||||
"testResultsProcessor": "@casualbot/jest-sonar-reporter"
|
||||
},
|
||||
"@casualbot/jest-sonar-reporter": {
|
||||
"outputDirectory": "coverage",
|
||||
"outputName": "jest-sonar-report.xml",
|
||||
"relativePaths": true
|
||||
},
|
||||
"typings": "./lib/index.d.ts"
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.2",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^5.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
"loglevel": "^1.7.1",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-widget-api": "^1.0.0",
|
||||
"p-retry": "4",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"unhomoglyph": "^1.0.6",
|
||||
"uuid": "9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.12.10",
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/eslint-parser": "^7.12.10",
|
||||
"@babel/eslint-plugin": "^7.12.10",
|
||||
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-runtime": "^7.12.10",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/register": "^7.12.10",
|
||||
"@casualbot/jest-sonar-reporter": "^2.2.5",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/domexception": "^4.0.0",
|
||||
"@types/jest": "^29.0.0",
|
||||
"@types/node": "18",
|
||||
"@types/sdp-transform": "^2.4.5",
|
||||
"@types/uuid": "7",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"allchange": "^1.0.6",
|
||||
"babel-jest": "^29.0.0",
|
||||
"babelify": "^10.0.0",
|
||||
"better-docs": "^2.4.0-beta.9",
|
||||
"browserify": "^17.0.0",
|
||||
"docdash": "^2.0.0",
|
||||
"domexception": "^4.0.0",
|
||||
"eslint": "8.31.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsdoc": "^39.6.4",
|
||||
"eslint-plugin-matrix-org": "^0.9.0",
|
||||
"eslint-plugin-tsdoc": "^0.2.17",
|
||||
"eslint-plugin-unicorn": "^45.0.0",
|
||||
"exorcist": "^2.0.0",
|
||||
"fake-indexeddb": "^4.0.0",
|
||||
"jest": "^29.0.0",
|
||||
"jest-environment-jsdom": "^29.0.0",
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jest-mock": "^29.0.0",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"prettier": "2.8.2",
|
||||
"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": {
|
||||
"testEnvironment": "node",
|
||||
"testMatch": [
|
||||
"<rootDir>/spec/**/*.spec.{js,ts}"
|
||||
],
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/spec/setupTests.ts"
|
||||
],
|
||||
"collectCoverageFrom": [
|
||||
"<rootDir>/src/**/*.{js,ts}"
|
||||
],
|
||||
"coverageReporters": [
|
||||
"text-summary",
|
||||
"lcov"
|
||||
],
|
||||
"testResultsProcessor": "@casualbot/jest-sonar-reporter"
|
||||
},
|
||||
"@casualbot/jest-sonar-reporter": {
|
||||
"outputDirectory": "coverage",
|
||||
"outputName": "jest-sonar-report.xml",
|
||||
"relativePaths": true
|
||||
},
|
||||
"typings": "./lib/index.d.ts"
|
||||
}
|
||||
|
||||
+2
-2
@@ -21,9 +21,9 @@ if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then
|
||||
# to the TypeScript source.
|
||||
src_value=$(jq -r ".matrix_src_$i" package.json)
|
||||
if [ "$src_value" != "null" ]; then
|
||||
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json
|
||||
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
|
||||
else
|
||||
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json
|
||||
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
+1
-1
@@ -184,7 +184,7 @@ for i in main typings
|
||||
do
|
||||
lib_value=$(jq -r ".matrix_lib_$i" package.json)
|
||||
if [ "$lib_value" != "null" ]; then
|
||||
jq ".$i = .matrix_lib_$i" package.json > package.json.new && mv package.json.new package.json
|
||||
jq ".$i = .matrix_lib_$i" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fsProm = require('fs/promises');
|
||||
const fsProm = require("fs/promises");
|
||||
|
||||
const PKGJSON = 'package.json';
|
||||
const PKGJSON = "package.json";
|
||||
|
||||
async function main() {
|
||||
const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, 'utf8'));
|
||||
for (const field of ['main', 'typings']) {
|
||||
if (pkgJson["matrix_lib_"+field] !== undefined) {
|
||||
pkgJson[field] = pkgJson["matrix_lib_"+field];
|
||||
const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, "utf8"));
|
||||
for (const field of ["main", "typings"]) {
|
||||
if (pkgJson["matrix_lib_" + field] !== undefined) {
|
||||
pkgJson[field] = pkgJson["matrix_lib_" + field];
|
||||
}
|
||||
}
|
||||
await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2));
|
||||
|
||||
+70
-52
@@ -17,20 +17,20 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import './olm-loader';
|
||||
import "./olm-loader";
|
||||
|
||||
import MockHttpBackend from 'matrix-mock-request';
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import { LocalStorageCryptoStore } from '../src/crypto/store/localStorage-crypto-store';
|
||||
import { logger } from '../src/logger';
|
||||
import { LocalStorageCryptoStore } from "../src/crypto/store/localStorage-crypto-store";
|
||||
import { logger } from "../src/logger";
|
||||
import { syncPromise } from "./test-utils/test-utils";
|
||||
import { createClient } from "../src/matrix";
|
||||
import { createClient, IStartClientOpts } from "../src/matrix";
|
||||
import { ICreateClientOpts, IDownloadKeyResult, MatrixClient, PendingEventOrdering } from "../src/client";
|
||||
import { MockStorageApi } from "./MockStorageApi";
|
||||
import { encodeUri } from "../src/utils";
|
||||
import { IDeviceKeys, IOneTimeKey } from "../src/crypto/dehydration";
|
||||
import { IKeyBackupSession } from "../src/crypto/keybackup";
|
||||
import { IKeysUploadResponse, IUploadKeysRequest } from '../src/client';
|
||||
import { IKeysUploadResponse, IUploadKeysRequest } from "../src/client";
|
||||
|
||||
/**
|
||||
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
|
||||
@@ -73,15 +73,18 @@ export class TestClient {
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return 'TestClient[' + this.userId + ']';
|
||||
return "TestClient[" + this.userId + "]";
|
||||
}
|
||||
|
||||
/**
|
||||
* start the client, and wait for it to initialise.
|
||||
*/
|
||||
public start(): Promise<void> {
|
||||
logger.log(this + ': starting');
|
||||
this.httpBackend.when("GET", "/versions").respond(200, {});
|
||||
public start(opts: IStartClientOpts = {}): Promise<void> {
|
||||
logger.log(this + ": starting");
|
||||
this.httpBackend.when("GET", "/versions").respond(200, {
|
||||
// we have tests that rely on support for lazy-loading members
|
||||
versions: ["r0.5.0"],
|
||||
});
|
||||
this.httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
this.expectDeviceKeyUpload();
|
||||
@@ -93,19 +96,18 @@ export class TestClient {
|
||||
this.client.startClient({
|
||||
// set this so that we can get hold of failed events
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
|
||||
...opts,
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
this.httpBackend.flushAllExpected(),
|
||||
syncPromise(this.client),
|
||||
]).then(() => {
|
||||
logger.log(this + ': started');
|
||||
return Promise.all([this.httpBackend.flushAllExpected(), syncPromise(this.client)]).then(() => {
|
||||
logger.log(this + ": started");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* stop the client
|
||||
* @return {Promise} Resolves once the mock http backend has finished all pending flushes
|
||||
* @returns Promise which resolves once the mock http backend has finished all pending flushes
|
||||
*/
|
||||
public async stop(): Promise<void> {
|
||||
this.client.stopClient();
|
||||
@@ -113,20 +115,30 @@ export class TestClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up expectations that the client will upload device keys.
|
||||
* Set up expectations that the client will upload device keys (and possibly one-time keys)
|
||||
*/
|
||||
public expectDeviceKeyUpload() {
|
||||
this.httpBackend.when("POST", "/keys/upload")
|
||||
this.httpBackend
|
||||
.when("POST", "/keys/upload")
|
||||
.respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content) => {
|
||||
expect(content.one_time_keys).toBe(undefined);
|
||||
expect(content.device_keys).toBeTruthy();
|
||||
|
||||
logger.log(this + ': received device keys');
|
||||
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);
|
||||
|
||||
this.deviceKeys = content.device_keys;
|
||||
return { one_time_key_counts: { signed_curve25519: 0 } };
|
||||
|
||||
// the first batch of one-time keys may be uploaded at the same time.
|
||||
if (content.one_time_keys) {
|
||||
logger.log(`${this}: received ${Object.keys(content.one_time_keys).length} one-time keys`);
|
||||
this.oneTimeKeys = content.one_time_keys;
|
||||
}
|
||||
return {
|
||||
one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys!).length,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -135,7 +147,7 @@ export class TestClient {
|
||||
* set up an expectation that the keys will be uploaded, and wait for
|
||||
* that to happen.
|
||||
*
|
||||
* @returns {Promise} for the one-time keys
|
||||
* @returns Promise for the one-time keys
|
||||
*/
|
||||
public awaitOneTimeKeyUpload(): Promise<Record<string, IOneTimeKey>> {
|
||||
if (Object.keys(this.oneTimeKeys!).length != 0) {
|
||||
@@ -143,30 +155,35 @@ export class TestClient {
|
||||
return Promise.resolve(this.oneTimeKeys!);
|
||||
}
|
||||
|
||||
this.httpBackend.when("POST", "/keys/upload")
|
||||
this.httpBackend
|
||||
.when("POST", "/keys/upload")
|
||||
.respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content: IUploadKeysRequest) => {
|
||||
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,
|
||||
} };
|
||||
return {
|
||||
one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys!).length,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
this.httpBackend.when("POST", "/keys/upload")
|
||||
this.httpBackend
|
||||
.when("POST", "/keys/upload")
|
||||
.respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content: IUploadKeysRequest) => {
|
||||
expect(content.device_keys).toBe(undefined);
|
||||
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);
|
||||
logger.log("%s: received %i one-time keys", this, 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,
|
||||
} };
|
||||
return {
|
||||
one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys!).length,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// this can take ages
|
||||
return this.httpBackend.flush('/keys/upload', 2, 1000).then((flushed) => {
|
||||
return this.httpBackend.flush("/keys/upload", 2, 1000).then((flushed) => {
|
||||
expect(flushed).toEqual(2);
|
||||
return this.oneTimeKeys!;
|
||||
});
|
||||
@@ -177,45 +194,49 @@ export class TestClient {
|
||||
*
|
||||
* We check that the query contains each of the users in `response`.
|
||||
*
|
||||
* @param {Object} response response to the query.
|
||||
* @param response - response to the query.
|
||||
*/
|
||||
public expectKeyQuery(response: IDownloadKeyResult) {
|
||||
this.httpBackend.when('POST', '/keys/query').respond<IDownloadKeyResult>(
|
||||
200, (_path, content) => {
|
||||
Object.keys(response.device_keys).forEach((userId) => {
|
||||
expect(content.device_keys![userId]).toEqual([]);
|
||||
});
|
||||
return response;
|
||||
this.httpBackend.when("POST", "/keys/query").respond<IDownloadKeyResult>(200, (_path, content) => {
|
||||
Object.keys(response.device_keys).forEach((userId) => {
|
||||
expect((content.device_keys! as Record<string, any>)[userId]).toEqual([]);
|
||||
});
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up expectations that the client will query key backups for a particular session
|
||||
*/
|
||||
public expectKeyBackupQuery(roomId: string, sessionId: string, status: number, response: IKeyBackupSession) {
|
||||
this.httpBackend.when('GET', encodeUri("/room_keys/keys/$roomId/$sessionId", {
|
||||
$roomId: roomId,
|
||||
$sessionId: sessionId,
|
||||
})).respond(status, response);
|
||||
this.httpBackend
|
||||
.when(
|
||||
"GET",
|
||||
encodeUri("/room_keys/keys/$roomId/$sessionId", {
|
||||
$roomId: roomId,
|
||||
$sessionId: sessionId,
|
||||
}),
|
||||
)
|
||||
.respond(status, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* get the uploaded curve25519 device key
|
||||
*
|
||||
* @return {string} base64 device key
|
||||
* @returns base64 device key
|
||||
*/
|
||||
public getDeviceKey(): string {
|
||||
const keyId = 'curve25519:' + this.deviceId;
|
||||
const keyId = "curve25519:" + this.deviceId;
|
||||
return this.deviceKeys!.keys[keyId];
|
||||
}
|
||||
|
||||
/**
|
||||
* get the uploaded ed25519 device key
|
||||
*
|
||||
* @return {string} base64 device key
|
||||
* @returns base64 device key
|
||||
*/
|
||||
public getSigningKey(): string {
|
||||
const keyId = 'ed25519:' + this.deviceId;
|
||||
const keyId = "ed25519:" + this.deviceId;
|
||||
return this.deviceKeys!.keys[keyId];
|
||||
}
|
||||
|
||||
@@ -224,10 +245,7 @@ export class TestClient {
|
||||
*/
|
||||
public flushSync(): Promise<void> {
|
||||
logger.log(`${this}: flushSync`);
|
||||
return Promise.all([
|
||||
this.httpBackend.flush('/sync', 1),
|
||||
syncPromise(this.client),
|
||||
]).then(() => {
|
||||
return Promise.all([this.httpBackend.flush("/sync", 1), syncPromise(this.client)]).then(() => {
|
||||
logger.log(`${this}: flushSync completed`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,19 +15,7 @@ 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
import type { default as BrowserMatrix } from "../../src/browser-index";
|
||||
|
||||
// stub for browser-matrix browserify tests
|
||||
// @ts-ignore
|
||||
@@ -43,4 +31,4 @@ afterAll(() => {
|
||||
global.matrixcs = {
|
||||
...global.matrixcs,
|
||||
timeoutSignal: () => new AbortController().signal,
|
||||
};
|
||||
} as typeof BrowserMatrix;
|
||||
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
|
||||
import "./setupTests";// uses browser-matrix instead of the src
|
||||
import "./setupTests"; // uses browser-matrix instead of the src
|
||||
import type { MatrixClient } from "../../src";
|
||||
|
||||
const USER_ID = "@user:test.server";
|
||||
@@ -24,7 +24,7 @@ const DEVICE_ID = "device_id";
|
||||
const ACCESS_TOKEN = "access_token";
|
||||
const ROOM_ID = "!room_id:server.test";
|
||||
|
||||
describe("Browserify Test", function() {
|
||||
describe("Browserify Test", function () {
|
||||
let client: MatrixClient;
|
||||
let httpBackend: HttpBackend;
|
||||
|
||||
@@ -65,22 +65,21 @@ describe("Browserify Test", function() {
|
||||
const syncData = {
|
||||
next_batch: "batch1",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[ROOM_ID] = {
|
||||
timeline: {
|
||||
events: [
|
||||
event,
|
||||
],
|
||||
limited: false,
|
||||
join: {
|
||||
[ROOM_ID]: {
|
||||
timeline: {
|
||||
events: [event],
|
||||
limited: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
const syncPromise = new Promise(r => client.once(global.matrixcs.ClientEvent.Sync, r));
|
||||
const syncPromise = new Promise((r) => client.once(global.matrixcs.ClientEvent.Sync, r));
|
||||
const unexpectedErrorFn = jest.fn();
|
||||
client.once(global.matrixcs.ClientEvent.SyncUnexpectedError, unexpectedErrorFn);
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+264
-259
@@ -16,9 +16,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { TestClient } from '../TestClient';
|
||||
import * as testUtils from '../test-utils/test-utils';
|
||||
import { logger } from '../../src/logger';
|
||||
import { TestClient } from "../TestClient";
|
||||
import * as testUtils from "../test-utils/test-utils";
|
||||
import { logger } from "../../src/logger";
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
|
||||
@@ -26,26 +26,24 @@ const ROOM_ID = "!room:id";
|
||||
* get a /sync response which contains a single e2e room (ROOM_ID), with the
|
||||
* members given
|
||||
*
|
||||
* @param {string[]} roomMembers
|
||||
*
|
||||
* @return {object} sync response
|
||||
* @returns sync response
|
||||
*/
|
||||
function getSyncResponse(roomMembers) {
|
||||
function getSyncResponse(roomMembers: string[]) {
|
||||
const stateEvents = [
|
||||
testUtils.mkEvent({
|
||||
type: 'm.room.encryption',
|
||||
skey: '',
|
||||
type: "m.room.encryption",
|
||||
skey: "",
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
Array.prototype.push.apply(
|
||||
stateEvents,
|
||||
roomMembers.map(
|
||||
(m) => testUtils.mkMembership({
|
||||
mship: 'join',
|
||||
roomMembers.map((m) =>
|
||||
testUtils.mkMembership({
|
||||
mship: "join",
|
||||
sender: m,
|
||||
}),
|
||||
),
|
||||
@@ -67,24 +65,22 @@ function getSyncResponse(roomMembers) {
|
||||
return syncResponse;
|
||||
}
|
||||
|
||||
describe("DeviceList management:", function() {
|
||||
describe("DeviceList management:", function () {
|
||||
if (!global.Olm) {
|
||||
logger.warn('not running deviceList tests: Olm not present');
|
||||
logger.warn("not running deviceList tests: Olm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
let sessionStoreBackend;
|
||||
let aliceTestClient;
|
||||
let aliceTestClient: TestClient;
|
||||
let sessionStoreBackend: Storage;
|
||||
|
||||
async function createTestClient() {
|
||||
const testClient = new TestClient(
|
||||
"@alice:localhost", "xzcvb", "akjgkrgjs", sessionStoreBackend,
|
||||
);
|
||||
const testClient = new TestClient("@alice:localhost", "xzcvb", "akjgkrgjs", sessionStoreBackend);
|
||||
await testClient.client.initCrypto();
|
||||
return testClient;
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
beforeEach(async function () {
|
||||
// we create our own sessionStoreBackend so that we can use it for
|
||||
// another TestClient.
|
||||
sessionStoreBackend = new testUtils.MockStorageApi();
|
||||
@@ -92,305 +88,314 @@ describe("DeviceList management:", function() {
|
||||
aliceTestClient = await createTestClient();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
afterEach(function () {
|
||||
return aliceTestClient.stop();
|
||||
});
|
||||
|
||||
it("Alice shouldn't do a second /query for non-e2e-capable devices", function() {
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
|
||||
return aliceTestClient.start().then(function() {
|
||||
const syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
logger.log("Forcing alice to download our device keys");
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
},
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.downloadKeys(['@bob:xyz']),
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 1),
|
||||
]);
|
||||
}).then(function() {
|
||||
logger.log("Telling alice to send a megolm message");
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/',
|
||||
).respond(200, {
|
||||
event_id: '$event_id',
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
|
||||
// the crypto stuff can take a while, so give the requests a whole second.
|
||||
aliceTestClient.httpBackend.flushAllExpected({
|
||||
timeout: 1000,
|
||||
}),
|
||||
]);
|
||||
it("Alice shouldn't do a second /query for non-e2e-capable devices", function () {
|
||||
aliceTestClient.expectKeyQuery({
|
||||
device_keys: { "@alice:localhost": {} },
|
||||
failures: {},
|
||||
});
|
||||
return aliceTestClient
|
||||
.start()
|
||||
.then(function () {
|
||||
const syncResponse = getSyncResponse(["@bob:xyz"]);
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
|
||||
return aliceTestClient.flushSync();
|
||||
})
|
||||
.then(function () {
|
||||
logger.log("Forcing alice to download our device keys");
|
||||
|
||||
aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, {
|
||||
device_keys: {
|
||||
"@bob:xyz": {},
|
||||
},
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.downloadKeys(["@bob:xyz"]),
|
||||
aliceTestClient.httpBackend.flush("/keys/query", 1),
|
||||
]);
|
||||
})
|
||||
.then(function () {
|
||||
logger.log("Telling alice to send a megolm message");
|
||||
|
||||
aliceTestClient.httpBackend.when("PUT", "/send/").respond(200, {
|
||||
event_id: "$event_id",
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, "test"),
|
||||
|
||||
// the crypto stuff can take a while, so give the requests a whole second.
|
||||
aliceTestClient.httpBackend.flushAllExpected({
|
||||
timeout: 1000,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it.skip("We should not get confused by out-of-order device query responses", () => {
|
||||
// https://github.com/vector-im/element-web/issues/3126
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
|
||||
return aliceTestClient.start().then(() => {
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse(['@bob:xyz', '@chris:abc']));
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(() => {
|
||||
// to make sure the initial device queries are flushed out, we
|
||||
// attempt to send a message.
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
'@chris:abc': {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
aliceTestClient.httpBackend.when('PUT', '/send/').respond(
|
||||
200, { event_id: '$event1' });
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 1).then(
|
||||
() => aliceTestClient.httpBackend.flush('/send/', 1),
|
||||
),
|
||||
aliceTestClient.client.crypto.deviceList.saveIfDirty(),
|
||||
]);
|
||||
}).then(() => {
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
expect(data.syncToken).toEqual(1);
|
||||
});
|
||||
|
||||
// invalidate bob's and chris's device lists in separate syncs
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
|
||||
next_batch: '2',
|
||||
device_lists: {
|
||||
changed: ['@bob:xyz'],
|
||||
},
|
||||
});
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
|
||||
next_batch: '3',
|
||||
device_lists: {
|
||||
changed: ['@chris:abc'],
|
||||
},
|
||||
});
|
||||
// flush both syncs
|
||||
return aliceTestClient.flushSync().then(() => {
|
||||
return aliceTestClient.flushSync();
|
||||
});
|
||||
}).then(() => {
|
||||
// check that we don't yet have a request for chris's devices.
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query', {
|
||||
device_keys: {
|
||||
'@chris:abc': {},
|
||||
},
|
||||
token: '3',
|
||||
}).respond(200, {
|
||||
device_keys: { '@chris:abc': {} },
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(0);
|
||||
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
if (bobStat != 1 && bobStat != 2) {
|
||||
throw new Error('Unexpected status for bob: wanted 1 or 2, got ' +
|
||||
bobStat);
|
||||
}
|
||||
const chrisStat = data.trackingStatus['@chris:abc'];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error(
|
||||
'Unexpected status for chris: wanted 1 or 2, got ' + chrisStat,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// now add an expectation for a query for bob's devices, and let
|
||||
// it complete.
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query', {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
},
|
||||
token: '2',
|
||||
}).respond(200, {
|
||||
device_keys: { '@bob:xyz': {} },
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(1);
|
||||
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(['@bob:xyz']);
|
||||
}).then(() => {
|
||||
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
expect(bobStat).toEqual(3);
|
||||
const chrisStat = data.trackingStatus['@chris:abc'];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error(
|
||||
'Unexpected status for chris: wanted 1 or 2, got ' + bobStat,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// now let the query for chris's devices complete.
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(1);
|
||||
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(['@chris:abc']);
|
||||
}).then(() => {
|
||||
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
const chrisStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
expect(bobStat).toEqual(3);
|
||||
expect(chrisStat).toEqual(3);
|
||||
expect(data.syncToken).toEqual(3);
|
||||
});
|
||||
aliceTestClient.expectKeyQuery({
|
||||
device_keys: { "@alice:localhost": {} },
|
||||
failures: {},
|
||||
});
|
||||
return aliceTestClient
|
||||
.start()
|
||||
.then(() => {
|
||||
aliceTestClient.httpBackend
|
||||
.when("GET", "/sync")
|
||||
.respond(200, getSyncResponse(["@bob:xyz", "@chris:abc"]));
|
||||
return aliceTestClient.flushSync();
|
||||
})
|
||||
.then(() => {
|
||||
// to make sure the initial device queries are flushed out, we
|
||||
// attempt to send a message.
|
||||
|
||||
aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, {
|
||||
device_keys: {
|
||||
"@bob:xyz": {},
|
||||
"@chris:abc": {},
|
||||
},
|
||||
});
|
||||
|
||||
aliceTestClient.httpBackend.when("PUT", "/send/").respond(200, { event_id: "$event1" });
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, "test"),
|
||||
aliceTestClient.httpBackend
|
||||
.flush("/keys/query", 1)
|
||||
.then(() => aliceTestClient.httpBackend.flush("/send/", 1)),
|
||||
aliceTestClient.client.crypto!.deviceList.saveIfDirty(),
|
||||
]);
|
||||
})
|
||||
.then(() => {
|
||||
// @ts-ignore accessing a protected field
|
||||
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
|
||||
expect(data!.syncToken).toEqual(1);
|
||||
});
|
||||
|
||||
// invalidate bob's and chris's device lists in separate syncs
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "2",
|
||||
device_lists: {
|
||||
changed: ["@bob:xyz"],
|
||||
},
|
||||
});
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "3",
|
||||
device_lists: {
|
||||
changed: ["@chris:abc"],
|
||||
},
|
||||
});
|
||||
// flush both syncs
|
||||
return aliceTestClient.flushSync().then(() => {
|
||||
return aliceTestClient.flushSync();
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
// check that we don't yet have a request for chris's devices.
|
||||
aliceTestClient.httpBackend
|
||||
.when("POST", "/keys/query", {
|
||||
device_keys: {
|
||||
"@chris:abc": {},
|
||||
},
|
||||
token: "3",
|
||||
})
|
||||
.respond(200, {
|
||||
device_keys: { "@chris:abc": {} },
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush("/keys/query", 1);
|
||||
})
|
||||
.then((flushed) => {
|
||||
expect(flushed).toEqual(0);
|
||||
return aliceTestClient.client.crypto!.deviceList.saveIfDirty();
|
||||
})
|
||||
.then(() => {
|
||||
// @ts-ignore accessing a protected field
|
||||
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data!.trackingStatus["@bob:xyz"];
|
||||
if (bobStat != 1 && bobStat != 2) {
|
||||
throw new Error("Unexpected status for bob: wanted 1 or 2, got " + bobStat);
|
||||
}
|
||||
const chrisStat = data!.trackingStatus["@chris:abc"];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error("Unexpected status for chris: wanted 1 or 2, got " + chrisStat);
|
||||
}
|
||||
});
|
||||
|
||||
// now add an expectation for a query for bob's devices, and let
|
||||
// it complete.
|
||||
aliceTestClient.httpBackend
|
||||
.when("POST", "/keys/query", {
|
||||
device_keys: {
|
||||
"@bob:xyz": {},
|
||||
},
|
||||
token: "2",
|
||||
})
|
||||
.respond(200, {
|
||||
device_keys: { "@bob:xyz": {} },
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush("/keys/query", 1);
|
||||
})
|
||||
.then((flushed) => {
|
||||
expect(flushed).toEqual(1);
|
||||
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(["@bob:xyz"]);
|
||||
})
|
||||
.then(() => {
|
||||
return aliceTestClient.client.crypto!.deviceList.saveIfDirty();
|
||||
})
|
||||
.then(() => {
|
||||
// @ts-ignore accessing a protected field
|
||||
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data!.trackingStatus["@bob:xyz"];
|
||||
expect(bobStat).toEqual(3);
|
||||
const chrisStat = data!.trackingStatus["@chris:abc"];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error("Unexpected status for chris: wanted 1 or 2, got " + bobStat);
|
||||
}
|
||||
});
|
||||
|
||||
// now let the query for chris's devices complete.
|
||||
return aliceTestClient.httpBackend.flush("/keys/query", 1);
|
||||
})
|
||||
.then((flushed) => {
|
||||
expect(flushed).toEqual(1);
|
||||
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(["@chris:abc"]);
|
||||
})
|
||||
.then(() => {
|
||||
return aliceTestClient.client.crypto!.deviceList.saveIfDirty();
|
||||
})
|
||||
.then(() => {
|
||||
// @ts-ignore accessing a protected field
|
||||
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data!.trackingStatus["@bob:xyz"];
|
||||
const chrisStat = data!.trackingStatus["@bob:xyz"];
|
||||
|
||||
expect(bobStat).toEqual(3);
|
||||
expect(chrisStat).toEqual(3);
|
||||
expect(data!.syncToken).toEqual(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// https://github.com/vector-im/element-web/issues/4983
|
||||
describe("Alice should know she has stale device lists", () => {
|
||||
beforeEach(async function() {
|
||||
beforeEach(async function () {
|
||||
await aliceTestClient.start();
|
||||
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse(['@bob:xyz']));
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, getSyncResponse(["@bob:xyz"]));
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
},
|
||||
aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, {
|
||||
device_keys: {
|
||||
"@bob:xyz": {},
|
||||
},
|
||||
);
|
||||
await aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
});
|
||||
await aliceTestClient.httpBackend.flush("/keys/query", 1);
|
||||
await aliceTestClient.client.crypto!.deviceList.saveIfDirty();
|
||||
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
// @ts-ignore accessing a protected field
|
||||
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data!.trackingStatus["@bob:xyz"];
|
||||
|
||||
// Alice should be tracking bob's device list
|
||||
expect(bobStat).toBeGreaterThan(
|
||||
0,
|
||||
);
|
||||
expect(bobStat).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("when Bob leaves", async function() {
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, {
|
||||
next_batch: 2,
|
||||
device_lists: {
|
||||
left: ['@bob:xyz'],
|
||||
},
|
||||
rooms: {
|
||||
join: {
|
||||
[ROOM_ID]: {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: 'leave',
|
||||
sender: '@bob:xyz',
|
||||
}),
|
||||
],
|
||||
},
|
||||
it("when Bob leaves", async function () {
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: 2,
|
||||
device_lists: {
|
||||
left: ["@bob:xyz"],
|
||||
},
|
||||
rooms: {
|
||||
join: {
|
||||
[ROOM_ID]: {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: "leave",
|
||||
sender: "@bob:xyz",
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
await aliceTestClient.flushSync();
|
||||
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
await aliceTestClient.client.crypto!.deviceList.saveIfDirty();
|
||||
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
// @ts-ignore accessing a protected field
|
||||
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data!.trackingStatus["@bob:xyz"];
|
||||
|
||||
// Alice should have marked bob's device list as untracked
|
||||
expect(bobStat).toEqual(
|
||||
0,
|
||||
);
|
||||
expect(bobStat).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("when Alice leaves", async function() {
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, {
|
||||
next_batch: 2,
|
||||
device_lists: {
|
||||
left: ['@bob:xyz'],
|
||||
},
|
||||
rooms: {
|
||||
leave: {
|
||||
[ROOM_ID]: {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: 'leave',
|
||||
sender: '@bob:xyz',
|
||||
}),
|
||||
],
|
||||
},
|
||||
it("when Alice leaves", async function () {
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: 2,
|
||||
device_lists: {
|
||||
left: ["@bob:xyz"],
|
||||
},
|
||||
rooms: {
|
||||
leave: {
|
||||
[ROOM_ID]: {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: "leave",
|
||||
sender: "@bob:xyz",
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
await aliceTestClient.flushSync();
|
||||
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
await aliceTestClient.client.crypto!.deviceList.saveIfDirty();
|
||||
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
// @ts-ignore accessing a protected field
|
||||
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data!.trackingStatus["@bob:xyz"];
|
||||
|
||||
// Alice should have marked bob's device list as untracked
|
||||
expect(bobStat).toEqual(
|
||||
0,
|
||||
);
|
||||
expect(bobStat).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("when Bob leaves whilst Alice is offline", async function() {
|
||||
it("when Bob leaves whilst Alice is offline", async function () {
|
||||
aliceTestClient.stop();
|
||||
|
||||
const anotherTestClient = await createTestClient();
|
||||
|
||||
try {
|
||||
await anotherTestClient.start();
|
||||
anotherTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse([]));
|
||||
anotherTestClient.httpBackend.when("GET", "/sync").respond(200, getSyncResponse([]));
|
||||
await anotherTestClient.flushSync();
|
||||
await anotherTestClient.client?.crypto?.deviceList?.saveIfDirty();
|
||||
|
||||
// @ts-ignore accessing private property
|
||||
anotherTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data!.trackingStatus['@bob:xyz'];
|
||||
const bobStat = data!.trackingStatus["@bob:xyz"];
|
||||
|
||||
// Alice should have marked bob's device list as untracked
|
||||
expect(bobStat).toEqual(
|
||||
0,
|
||||
);
|
||||
expect(bobStat).toEqual(0);
|
||||
});
|
||||
} finally {
|
||||
anotherTestClient.stop();
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient events", function() {
|
||||
describe("MatrixClient events", function () {
|
||||
const selfUserId = "@alice:localhost";
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
let client: MatrixClient | undefined;
|
||||
@@ -46,23 +46,25 @@ describe("MatrixClient events", function() {
|
||||
return [client!, httpBackend];
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
[client!, httpBackend] = setupTests();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
afterEach(function () {
|
||||
httpBackend?.verifyNoOutstandingExpectation();
|
||||
client?.stopClient();
|
||||
return httpBackend?.stop();
|
||||
});
|
||||
|
||||
describe("emissions", function() {
|
||||
describe("emissions", function () {
|
||||
const SYNC_DATA = {
|
||||
next_batch: "s_5_3",
|
||||
presence: {
|
||||
events: [
|
||||
utils.mkPresence({
|
||||
user: "@foo:bar", name: "Foo Bar", presence: "online",
|
||||
user: "@foo:bar",
|
||||
name: "Foo Bar",
|
||||
presence: "online",
|
||||
}),
|
||||
],
|
||||
},
|
||||
@@ -72,7 +74,9 @@ describe("MatrixClient events", function() {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: "!erufh:bar", user: "@foo:bar", msg: "hmmm",
|
||||
room: "!erufh:bar",
|
||||
user: "@foo:bar",
|
||||
msg: "hmmm",
|
||||
}),
|
||||
],
|
||||
prev_batch: "s",
|
||||
@@ -80,10 +84,13 @@ describe("MatrixClient events", function() {
|
||||
state: {
|
||||
events: [
|
||||
utils.mkMembership({
|
||||
room: "!erufh:bar", mship: "join", user: "@foo:bar",
|
||||
room: "!erufh:bar",
|
||||
mship: "join",
|
||||
user: "@foo:bar",
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: "!erufh:bar",
|
||||
type: "m.room.create",
|
||||
room: "!erufh:bar",
|
||||
user: "@foo:bar",
|
||||
content: {
|
||||
creator: "@foo:bar",
|
||||
@@ -103,18 +110,23 @@ describe("MatrixClient events", function() {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: "!erufh:bar", user: "@foo:bar",
|
||||
room: "!erufh:bar",
|
||||
user: "@foo:bar",
|
||||
msg: "ello ello",
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: "!erufh:bar", user: "@foo:bar", msg: ":D",
|
||||
room: "!erufh:bar",
|
||||
user: "@foo:bar",
|
||||
msg: ":D",
|
||||
}),
|
||||
],
|
||||
},
|
||||
ephemeral: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.typing", room: "!erufh:bar", content: {
|
||||
type: "m.typing",
|
||||
room: "!erufh:bar",
|
||||
content: {
|
||||
user_ids: ["@foo:bar"],
|
||||
},
|
||||
}),
|
||||
@@ -125,50 +137,49 @@ describe("MatrixClient events", function() {
|
||||
},
|
||||
};
|
||||
|
||||
it("should emit events from both the first and subsequent /sync calls",
|
||||
function() {
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
it("should emit events from both the first and subsequent /sync calls", function () {
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
let expectedEvents: Partial<IEvent>[] = [];
|
||||
expectedEvents = expectedEvents.concat(
|
||||
SYNC_DATA.presence.events,
|
||||
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
|
||||
SYNC_DATA.rooms.join["!erufh:bar"].state.events,
|
||||
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
|
||||
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events,
|
||||
);
|
||||
let expectedEvents: Partial<IEvent>[] = [];
|
||||
expectedEvents = expectedEvents.concat(
|
||||
SYNC_DATA.presence.events,
|
||||
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
|
||||
SYNC_DATA.rooms.join["!erufh:bar"].state.events,
|
||||
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
|
||||
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events,
|
||||
);
|
||||
|
||||
client!.on(ClientEvent.Event, function(event) {
|
||||
let found = false;
|
||||
for (let i = 0; i < expectedEvents.length; i++) {
|
||||
if (expectedEvents[i].event_id === event.getId()) {
|
||||
expectedEvents.splice(i, 1);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
client!.on(ClientEvent.Event, function (event) {
|
||||
let found = false;
|
||||
for (let i = 0; i < expectedEvents.length; i++) {
|
||||
if (expectedEvents[i].event_id === event.getId()) {
|
||||
expectedEvents.splice(i, 1);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
expect(found).toBe(true);
|
||||
});
|
||||
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([
|
||||
// wait for two SYNCING events
|
||||
utils.syncPromise(client!).then(() => {
|
||||
return utils.syncPromise(client!);
|
||||
}),
|
||||
httpBackend!.flushAllExpected(),
|
||||
]).then(() => {
|
||||
expect(expectedEvents.length).toEqual(0);
|
||||
});
|
||||
}
|
||||
expect(found).toBe(true);
|
||||
});
|
||||
|
||||
it("should emit User events", function(done) {
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([
|
||||
// wait for two SYNCING events
|
||||
utils.syncPromise(client!).then(() => {
|
||||
return utils.syncPromise(client!);
|
||||
}),
|
||||
httpBackend!.flushAllExpected(),
|
||||
]).then(() => {
|
||||
expect(expectedEvents.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit User events", function (done) {
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
let fired = false;
|
||||
client!.on(UserEvent.Presence, function(event, user) {
|
||||
client!.on(UserEvent.Presence, function (event, user) {
|
||||
fired = true;
|
||||
expect(user).toBeTruthy();
|
||||
expect(event).toBeTruthy();
|
||||
@@ -177,59 +188,52 @@ describe("MatrixClient events", function() {
|
||||
}
|
||||
|
||||
expect(event.event).toEqual(SYNC_DATA.presence.events[0]);
|
||||
expect(user.presence).toEqual(
|
||||
SYNC_DATA.presence.events[0]?.content?.presence,
|
||||
);
|
||||
expect(user.presence).toEqual(SYNC_DATA.presence.events[0]?.content?.presence);
|
||||
});
|
||||
client!.startClient();
|
||||
|
||||
httpBackend!.flushAllExpected().then(function() {
|
||||
httpBackend!.flushAllExpected().then(function () {
|
||||
expect(fired).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit Room events", function() {
|
||||
it("should emit Room events", function () {
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
let roomInvokeCount = 0;
|
||||
let roomNameInvokeCount = 0;
|
||||
let timelineFireCount = 0;
|
||||
client!.on(ClientEvent.Room, function(room) {
|
||||
client!.on(ClientEvent.Room, function (room) {
|
||||
roomInvokeCount++;
|
||||
expect(room.roomId).toEqual("!erufh:bar");
|
||||
});
|
||||
client!.on(RoomEvent.Timeline, function(event, room) {
|
||||
client!.on(RoomEvent.Timeline, function (event, room) {
|
||||
timelineFireCount++;
|
||||
expect(room?.roomId).toEqual("!erufh:bar");
|
||||
});
|
||||
client!.on(RoomEvent.Name, function(room) {
|
||||
client!.on(RoomEvent.Name, function (room) {
|
||||
roomNameInvokeCount++;
|
||||
});
|
||||
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([
|
||||
httpBackend!.flushAllExpected(),
|
||||
utils.syncPromise(client!, 2),
|
||||
]).then(function() {
|
||||
return Promise.all([httpBackend!.flushAllExpected(), utils.syncPromise(client!, 2)]).then(function () {
|
||||
expect(roomInvokeCount).toEqual(1);
|
||||
expect(roomNameInvokeCount).toEqual(1);
|
||||
expect(timelineFireCount).toEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit RoomState events", function() {
|
||||
it("should emit RoomState events", function () {
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
const roomStateEventTypes = [
|
||||
"m.room.member", "m.room.create",
|
||||
];
|
||||
const roomStateEventTypes = ["m.room.member", "m.room.create"];
|
||||
let eventsInvokeCount = 0;
|
||||
let membersInvokeCount = 0;
|
||||
let newMemberInvokeCount = 0;
|
||||
client!.on(RoomStateEvent.Events, function(event, state) {
|
||||
client!.on(RoomStateEvent.Events, function (event, state) {
|
||||
eventsInvokeCount++;
|
||||
const index = roomStateEventTypes.indexOf(event.getType());
|
||||
expect(index).not.toEqual(-1);
|
||||
@@ -237,13 +241,13 @@ describe("MatrixClient events", function() {
|
||||
roomStateEventTypes.splice(index, 1);
|
||||
}
|
||||
});
|
||||
client!.on(RoomStateEvent.Members, function(event, state, member) {
|
||||
client!.on(RoomStateEvent.Members, function (event, state, member) {
|
||||
membersInvokeCount++;
|
||||
expect(member.roomId).toEqual("!erufh:bar");
|
||||
expect(member.userId).toEqual("@foo:bar");
|
||||
expect(member.membership).toEqual("join");
|
||||
});
|
||||
client!.on(RoomStateEvent.NewMember, function(event, state, member) {
|
||||
client!.on(RoomStateEvent.NewMember, function (event, state, member) {
|
||||
newMemberInvokeCount++;
|
||||
expect(member.roomId).toEqual("!erufh:bar");
|
||||
expect(member.userId).toEqual("@foo:bar");
|
||||
@@ -252,17 +256,14 @@ describe("MatrixClient events", function() {
|
||||
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([
|
||||
httpBackend!.flushAllExpected(),
|
||||
utils.syncPromise(client!, 2),
|
||||
]).then(function() {
|
||||
return Promise.all([httpBackend!.flushAllExpected(), utils.syncPromise(client!, 2)]).then(function () {
|
||||
expect(membersInvokeCount).toEqual(1);
|
||||
expect(newMemberInvokeCount).toEqual(1);
|
||||
expect(eventsInvokeCount).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit RoomMember events", function() {
|
||||
it("should emit RoomMember events", function () {
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
@@ -270,27 +271,24 @@ describe("MatrixClient events", function() {
|
||||
let powerLevelInvokeCount = 0;
|
||||
let nameInvokeCount = 0;
|
||||
let membershipInvokeCount = 0;
|
||||
client!.on(RoomMemberEvent.Name, function(event, member) {
|
||||
client!.on(RoomMemberEvent.Name, function (event, member) {
|
||||
nameInvokeCount++;
|
||||
});
|
||||
client!.on(RoomMemberEvent.Typing, function(event, member) {
|
||||
client!.on(RoomMemberEvent.Typing, function (event, member) {
|
||||
typingInvokeCount++;
|
||||
expect(member.typing).toBe(true);
|
||||
});
|
||||
client!.on(RoomMemberEvent.PowerLevel, function(event, member) {
|
||||
client!.on(RoomMemberEvent.PowerLevel, function (event, member) {
|
||||
powerLevelInvokeCount++;
|
||||
});
|
||||
client!.on(RoomMemberEvent.Membership, function(event, member) {
|
||||
client!.on(RoomMemberEvent.Membership, function (event, member) {
|
||||
membershipInvokeCount++;
|
||||
expect(member.membership).toEqual("join");
|
||||
});
|
||||
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([
|
||||
httpBackend!.flushAllExpected(),
|
||||
utils.syncPromise(client!, 2),
|
||||
]).then(function() {
|
||||
return Promise.all([httpBackend!.flushAllExpected(), utils.syncPromise(client!, 2)]).then(function () {
|
||||
expect(typingInvokeCount).toEqual(1);
|
||||
expect(powerLevelInvokeCount).toEqual(0);
|
||||
expect(nameInvokeCount).toEqual(0);
|
||||
@@ -298,36 +296,36 @@ describe("MatrixClient events", function() {
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function() {
|
||||
const error = { errcode: 'M_UNKNOWN_TOKEN' };
|
||||
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function () {
|
||||
const error = { errcode: "M_UNKNOWN_TOKEN" };
|
||||
httpBackend!.when("GET", "/sync").respond(401, error);
|
||||
|
||||
let sessionLoggedOutCount = 0;
|
||||
client!.on(HttpApiEvent.SessionLoggedOut, function(errObj) {
|
||||
client!.on(HttpApiEvent.SessionLoggedOut, function (errObj) {
|
||||
sessionLoggedOutCount++;
|
||||
expect(errObj.data).toEqual(error);
|
||||
});
|
||||
|
||||
client!.startClient();
|
||||
|
||||
return httpBackend!.flushAllExpected().then(function() {
|
||||
return httpBackend!.flushAllExpected().then(function () {
|
||||
expect(sessionLoggedOutCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit Session.logged_out on M_UNKNOWN_TOKEN (soft logout)", function() {
|
||||
const error = { errcode: 'M_UNKNOWN_TOKEN', soft_logout: true };
|
||||
it("should emit Session.logged_out on M_UNKNOWN_TOKEN (soft logout)", function () {
|
||||
const error = { errcode: "M_UNKNOWN_TOKEN", soft_logout: true };
|
||||
httpBackend!.when("GET", "/sync").respond(401, error);
|
||||
|
||||
let sessionLoggedOutCount = 0;
|
||||
client!.on(HttpApiEvent.SessionLoggedOut, function(errObj) {
|
||||
client!.on(HttpApiEvent.SessionLoggedOut, function (errObj) {
|
||||
sessionLoggedOutCount++;
|
||||
expect(errObj.data).toEqual(error);
|
||||
});
|
||||
|
||||
client!.startClient();
|
||||
|
||||
return httpBackend!.flushAllExpected().then(function() {
|
||||
return httpBackend!.flushAllExpected().then(function () {
|
||||
expect(sessionLoggedOutCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,13 @@
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { MatrixClient } from "../../src/matrix";
|
||||
import { ClientEvent, MatrixClient } from "../../src/matrix";
|
||||
import { MatrixScheduler } from "../../src/scheduler";
|
||||
import { MemoryStore } from "../../src/store/memory";
|
||||
import { MatrixError } from "../../src/http-api";
|
||||
import { IStore } from "../../src/store";
|
||||
|
||||
describe("MatrixClient opts", function() {
|
||||
describe("MatrixClient opts", function () {
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
let httpBackend = new HttpBackend();
|
||||
const userId = "@alice:localhost";
|
||||
@@ -19,11 +19,14 @@ describe("MatrixClient opts", function() {
|
||||
presence: {},
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": { // roomId
|
||||
"!foo:bar": {
|
||||
// roomId
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userB, msg: "hello",
|
||||
room: roomId,
|
||||
user: userB,
|
||||
msg: "hello",
|
||||
}),
|
||||
],
|
||||
prev_batch: "f_1_1",
|
||||
@@ -31,19 +34,29 @@ describe("MatrixClient opts", function() {
|
||||
state: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: userB,
|
||||
type: "m.room.name",
|
||||
room: roomId,
|
||||
user: userB,
|
||||
content: {
|
||||
name: "Old room name",
|
||||
},
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "join", user: userB, name: "Bob",
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
user: userB,
|
||||
name: "Bob",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "join", user: userId, name: "Alice",
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
user: userId,
|
||||
name: "Alice",
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomId, user: userId,
|
||||
type: "m.room.create",
|
||||
room: roomId,
|
||||
user: userId,
|
||||
content: {
|
||||
creator: userId,
|
||||
},
|
||||
@@ -55,18 +68,18 @@ describe("MatrixClient opts", function() {
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
httpBackend = new HttpBackend();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
afterEach(function () {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
return httpBackend.stop();
|
||||
});
|
||||
|
||||
describe("without opts.store", function() {
|
||||
let client;
|
||||
beforeEach(function() {
|
||||
describe("without opts.store", function () {
|
||||
let client: MatrixClient;
|
||||
beforeEach(function () {
|
||||
client = new MatrixClient({
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
store: undefined,
|
||||
@@ -77,34 +90,34 @@ describe("MatrixClient opts", function() {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
afterEach(function () {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it("should be able to send messages", function(done) {
|
||||
it("should be able to send messages", function (done) {
|
||||
const eventId = "$flibble:wibble";
|
||||
httpBackend.when("PUT", "/txn1").respond(200, {
|
||||
event_id: eventId,
|
||||
});
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) {
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function (res) {
|
||||
expect(res.event_id).toEqual(eventId);
|
||||
done();
|
||||
});
|
||||
httpBackend.flush("/txn1", 1);
|
||||
});
|
||||
|
||||
it("should be able to sync / get new events", async function() {
|
||||
const expectedEventTypes = [ // from /initialSync
|
||||
"m.room.message", "m.room.name", "m.room.member", "m.room.member",
|
||||
it("should be able to sync / get new events", async function () {
|
||||
const expectedEventTypes = [
|
||||
// from /initialSync
|
||||
"m.room.message",
|
||||
"m.room.name",
|
||||
"m.room.member",
|
||||
"m.room.member",
|
||||
"m.room.create",
|
||||
];
|
||||
client.on("event", function(event) {
|
||||
expect(expectedEventTypes.indexOf(event.getType())).not.toEqual(
|
||||
-1,
|
||||
);
|
||||
expectedEventTypes.splice(
|
||||
expectedEventTypes.indexOf(event.getType()), 1,
|
||||
);
|
||||
client.on(ClientEvent.Event, function (event) {
|
||||
expect(expectedEventTypes.indexOf(event.getType())).not.toEqual(-1);
|
||||
expectedEventTypes.splice(expectedEventTypes.indexOf(event.getType()), 1);
|
||||
});
|
||||
httpBackend.when("GET", "/versions").respond(200, {});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
@@ -114,19 +127,14 @@ describe("MatrixClient opts", function() {
|
||||
await httpBackend.flush("/versions", 1);
|
||||
await httpBackend.flush("/pushrules", 1);
|
||||
await httpBackend.flush("/filter", 1);
|
||||
await Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]);
|
||||
expect(expectedEventTypes.length).toEqual(
|
||||
0,
|
||||
);
|
||||
await Promise.all([httpBackend.flush("/sync", 1), utils.syncPromise(client)]);
|
||||
expect(expectedEventTypes.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("without opts.scheduler", function() {
|
||||
let client;
|
||||
beforeEach(function() {
|
||||
describe("without opts.scheduler", function () {
|
||||
let client: MatrixClient;
|
||||
beforeEach(function () {
|
||||
client = new MatrixClient({
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
store: new MemoryStore() as IStore,
|
||||
@@ -137,25 +145,31 @@ describe("MatrixClient opts", function() {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
afterEach(function () {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it("shouldn't retry sending events", function(done) {
|
||||
httpBackend.when("PUT", "/txn1").respond(500, new MatrixError({
|
||||
errcode: "M_SOMETHING",
|
||||
error: "Ruh roh",
|
||||
}));
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) {
|
||||
expect(false).toBe(true);
|
||||
}, function(err) {
|
||||
expect(err.errcode).toEqual("M_SOMETHING");
|
||||
done();
|
||||
});
|
||||
it("shouldn't retry sending events", function (done) {
|
||||
httpBackend.when("PUT", "/txn1").respond(
|
||||
500,
|
||||
new MatrixError({
|
||||
errcode: "M_SOMETHING",
|
||||
error: "Ruh roh",
|
||||
}),
|
||||
);
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1").then(
|
||||
function (res) {
|
||||
expect(false).toBe(true);
|
||||
},
|
||||
function (err) {
|
||||
expect(err.errcode).toEqual("M_SOMETHING");
|
||||
done();
|
||||
},
|
||||
);
|
||||
httpBackend.flush("/txn1", 1);
|
||||
});
|
||||
|
||||
it("shouldn't queue events", function(done) {
|
||||
it("shouldn't queue events", function (done) {
|
||||
httpBackend.when("PUT", "/txn1").respond(200, {
|
||||
event_id: "AAA",
|
||||
});
|
||||
@@ -164,26 +178,26 @@ describe("MatrixClient opts", function() {
|
||||
});
|
||||
let sentA = false;
|
||||
let sentB = false;
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) {
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function (res) {
|
||||
sentA = true;
|
||||
expect(sentB).toBe(true);
|
||||
});
|
||||
client.sendTextMessage("!foo:bar", "b body", "txn2").then(function(res) {
|
||||
client.sendTextMessage("!foo:bar", "b body", "txn2").then(function (res) {
|
||||
sentB = true;
|
||||
expect(sentA).toBe(false);
|
||||
});
|
||||
httpBackend.flush("/txn2", 1).then(function() {
|
||||
httpBackend.flush("/txn1", 1).then(function() {
|
||||
httpBackend.flush("/txn2", 1).then(function () {
|
||||
httpBackend.flush("/txn1", 1).then(function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should be able to send messages", function(done) {
|
||||
it("should be able to send messages", function (done) {
|
||||
httpBackend.when("PUT", "/txn1").respond(200, {
|
||||
event_id: "foo",
|
||||
});
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) {
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function (res) {
|
||||
expect(res.event_id).toEqual("foo");
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -29,13 +29,7 @@ describe("MatrixClient relations", () => {
|
||||
|
||||
const setupTests = (): [MatrixClient, HttpBackend] => {
|
||||
const scheduler = new MatrixScheduler();
|
||||
const testClient = new TestClient(
|
||||
userId,
|
||||
"DEVICE",
|
||||
accessToken,
|
||||
undefined,
|
||||
{ scheduler },
|
||||
);
|
||||
const testClient = new TestClient(userId, "DEVICE", accessToken, undefined, { scheduler });
|
||||
const httpBackend = testClient.httpBackend;
|
||||
const client = testClient.client;
|
||||
|
||||
@@ -52,76 +46,71 @@ describe("MatrixClient relations", () => {
|
||||
});
|
||||
|
||||
it("should read related events with the default options", async () => {
|
||||
const response = client!.relations(roomId, '$event-0', null, null);
|
||||
const response = client!.relations(roomId, "$event-0", null, null);
|
||||
|
||||
httpBackend!.when("GET", "/rooms/!room%3Ahere/event/%24event-0").respond(200, null);
|
||||
httpBackend!
|
||||
.when("GET", "/rooms/!room%3Ahere/relations/%24event-0?dir=b")
|
||||
.respond(200, { chunk: [], next_batch: 'NEXT' });
|
||||
.when("GET", "/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0?dir=b")
|
||||
.respond(200, { chunk: [], next_batch: "NEXT" });
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
|
||||
expect(await response).toEqual({ events: [], nextBatch: "NEXT", originalEvent: null, prevBatch: null });
|
||||
});
|
||||
|
||||
it("should read related events with relation type", async () => {
|
||||
const response = client!.relations(roomId, '$event-0', 'm.reference', null);
|
||||
const response = client!.relations(roomId, "$event-0", "m.reference", null);
|
||||
|
||||
httpBackend!.when("GET", "/rooms/!room%3Ahere/event/%24event-0").respond(200, null);
|
||||
httpBackend!
|
||||
.when("GET", "/rooms/!room%3Ahere/relations/%24event-0/m.reference?dir=b")
|
||||
.respond(200, { chunk: [], next_batch: 'NEXT' });
|
||||
.when("GET", "/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0/m.reference?dir=b")
|
||||
.respond(200, { chunk: [], next_batch: "NEXT" });
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
|
||||
expect(await response).toEqual({ events: [], nextBatch: "NEXT", originalEvent: null, prevBatch: null });
|
||||
});
|
||||
|
||||
it("should read related events with relation type and event type", async () => {
|
||||
const response = client!.relations(roomId, '$event-0', 'm.reference', 'm.room.message');
|
||||
const response = client!.relations(roomId, "$event-0", "m.reference", "m.room.message");
|
||||
|
||||
httpBackend!.when("GET", "/rooms/!room%3Ahere/event/%24event-0").respond(200, null);
|
||||
httpBackend!
|
||||
.when(
|
||||
"GET",
|
||||
"/rooms/!room%3Ahere/relations/%24event-0/m.reference/m.room.message?dir=b",
|
||||
)
|
||||
.respond(200, { chunk: [], next_batch: 'NEXT' });
|
||||
.when("GET", "/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0/m.reference/m.room.message?dir=b")
|
||||
.respond(200, { chunk: [], next_batch: "NEXT" });
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
|
||||
expect(await response).toEqual({ events: [], nextBatch: "NEXT", originalEvent: null, prevBatch: null });
|
||||
});
|
||||
|
||||
it("should read related events with custom options", async () => {
|
||||
const response = client!.relations(roomId, '$event-0', null, null, {
|
||||
const response = client!.relations(roomId, "$event-0", null, null, {
|
||||
dir: Direction.Forward,
|
||||
from: 'FROM',
|
||||
from: "FROM",
|
||||
limit: 10,
|
||||
to: 'TO',
|
||||
to: "TO",
|
||||
});
|
||||
|
||||
httpBackend!.when("GET", "/rooms/!room%3Ahere/event/%24event-0").respond(200, null);
|
||||
httpBackend!
|
||||
.when(
|
||||
"GET",
|
||||
"/rooms/!room%3Ahere/relations/%24event-0?dir=f&from=FROM&limit=10&to=TO",
|
||||
)
|
||||
.respond(200, { chunk: [], next_batch: 'NEXT' });
|
||||
.when("GET", "/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0?dir=f&from=FROM&limit=10&to=TO")
|
||||
.respond(200, { chunk: [], next_batch: "NEXT" });
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
|
||||
expect(await response).toEqual({ events: [], nextBatch: "NEXT", originalEvent: null, prevBatch: null });
|
||||
});
|
||||
|
||||
it('should use default direction in the fetchRelations endpoint', async () => {
|
||||
const response = client!.fetchRelations(roomId, '$event-0', null, null);
|
||||
it("should use default direction in the fetchRelations endpoint", async () => {
|
||||
const response = client!.fetchRelations(roomId, "$event-0", null, null);
|
||||
|
||||
httpBackend!
|
||||
.when(
|
||||
"GET",
|
||||
"/rooms/!room%3Ahere/relations/%24event-0?dir=b",
|
||||
)
|
||||
.respond(200, { chunk: [], next_batch: 'NEXT' });
|
||||
.when("GET", "/rooms/!room%3Ahere/relations/%24event-0?dir=b")
|
||||
.respond(200, { chunk: [], next_batch: "NEXT" });
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await response).toEqual({ "chunk": [], "next_batch": "NEXT" });
|
||||
expect(await response).toEqual({ chunk: [], next_batch: "NEXT" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ import { EventStatus, RoomEvent, MatrixClient, MatrixScheduler } from "../../src
|
||||
import { Room } from "../../src/models/room";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient retrying", function() {
|
||||
describe("MatrixClient retrying", function () {
|
||||
const userId = "@alice:localhost";
|
||||
const accessToken = "aseukfgwef";
|
||||
const roomId = "!room:here";
|
||||
@@ -30,13 +30,7 @@ describe("MatrixClient retrying", function() {
|
||||
|
||||
const setupTests = (): [MatrixClient, HttpBackend, Room] => {
|
||||
const scheduler = new MatrixScheduler();
|
||||
const testClient = new TestClient(
|
||||
userId,
|
||||
"DEVICE",
|
||||
accessToken,
|
||||
undefined,
|
||||
{ scheduler },
|
||||
);
|
||||
const testClient = new TestClient(userId, "DEVICE", accessToken, undefined, { scheduler });
|
||||
const httpBackend = testClient.httpBackend;
|
||||
const client = testClient.client;
|
||||
const room = new Room(roomId, client, userId);
|
||||
@@ -45,49 +39,46 @@ describe("MatrixClient retrying", function() {
|
||||
return [client, httpBackend, room];
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
[client, httpBackend, room] = setupTests();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
afterEach(function () {
|
||||
httpBackend!.verifyNoOutstandingExpectation();
|
||||
return httpBackend!.stop();
|
||||
});
|
||||
|
||||
xit("should retry according to MatrixScheduler.retryFn", function() {
|
||||
xit("should retry according to MatrixScheduler.retryFn", function () {});
|
||||
|
||||
});
|
||||
xit("should queue according to MatrixScheduler.queueFn", function () {});
|
||||
|
||||
xit("should queue according to MatrixScheduler.queueFn", function() {
|
||||
xit("should mark events as EventStatus.NOT_SENT when giving up", function () {});
|
||||
|
||||
});
|
||||
xit("should mark events as EventStatus.QUEUED when queued", function () {});
|
||||
|
||||
xit("should mark events as EventStatus.NOT_SENT when giving up", function() {
|
||||
|
||||
});
|
||||
|
||||
xit("should mark events as EventStatus.QUEUED when queued", function() {
|
||||
|
||||
});
|
||||
|
||||
it("should mark events as EventStatus.CANCELLED when cancelled", function() {
|
||||
it("should mark events as EventStatus.CANCELLED when cancelled", function () {
|
||||
// send a couple of events; the second will be queued
|
||||
const p1 = client!.sendMessage(roomId, {
|
||||
"msgtype": "m.text",
|
||||
"body": "m1",
|
||||
}).then(function() {
|
||||
// we expect the first message to fail
|
||||
throw new Error('Message 1 unexpectedly sent successfully');
|
||||
}, () => {
|
||||
// this is expected
|
||||
});
|
||||
const p1 = client!
|
||||
.sendMessage(roomId, {
|
||||
msgtype: "m.text",
|
||||
body: "m1",
|
||||
})
|
||||
.then(
|
||||
function () {
|
||||
// we expect the first message to fail
|
||||
throw new Error("Message 1 unexpectedly sent successfully");
|
||||
},
|
||||
() => {
|
||||
// this is expected
|
||||
},
|
||||
);
|
||||
|
||||
// XXX: it turns out that the promise returned by this message
|
||||
// never gets resolved.
|
||||
// https://github.com/matrix-org/matrix-js-sdk/issues/496
|
||||
client!.sendMessage(roomId, {
|
||||
"msgtype": "m.text",
|
||||
"body": "m2",
|
||||
msgtype: "m.text",
|
||||
body: "m2",
|
||||
});
|
||||
|
||||
// both events should be in the timeline at this point
|
||||
@@ -100,20 +91,23 @@ describe("MatrixClient retrying", function() {
|
||||
expect(ev2.status).toEqual(EventStatus.SENDING);
|
||||
|
||||
// the first message should get sent, and the second should get queued
|
||||
httpBackend!.when("PUT", "/send/m.room.message/").check(function() {
|
||||
// ev2 should now have been queued
|
||||
expect(ev2.status).toEqual(EventStatus.QUEUED);
|
||||
httpBackend!
|
||||
.when("PUT", "/send/m.room.message/")
|
||||
.check(function () {
|
||||
// ev2 should now have been queued
|
||||
expect(ev2.status).toEqual(EventStatus.QUEUED);
|
||||
|
||||
// now we can cancel the second and check everything looks sane
|
||||
client!.cancelPendingEvent(ev2);
|
||||
expect(ev2.status).toEqual(EventStatus.CANCELLED);
|
||||
expect(tl.length).toEqual(1);
|
||||
// now we can cancel the second and check everything looks sane
|
||||
client!.cancelPendingEvent(ev2);
|
||||
expect(ev2.status).toEqual(EventStatus.CANCELLED);
|
||||
expect(tl.length).toEqual(1);
|
||||
|
||||
// shouldn't be able to cancel the first message yet
|
||||
expect(function() {
|
||||
client!.cancelPendingEvent(ev1);
|
||||
}).toThrow();
|
||||
}).respond(400); // fail the first message
|
||||
// shouldn't be able to cancel the first message yet
|
||||
expect(function () {
|
||||
client!.cancelPendingEvent(ev1);
|
||||
}).toThrow();
|
||||
})
|
||||
.respond(400); // fail the first message
|
||||
|
||||
// wait for the localecho of ev1 to be updated
|
||||
const p3 = new Promise<void>((resolve, reject) => {
|
||||
@@ -122,7 +116,7 @@ describe("MatrixClient retrying", function() {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}).then(function() {
|
||||
}).then(function () {
|
||||
expect(ev1.status).toEqual(EventStatus.NOT_SENT);
|
||||
expect(tl.length).toEqual(1);
|
||||
|
||||
@@ -132,19 +126,11 @@ describe("MatrixClient retrying", function() {
|
||||
expect(tl.length).toEqual(0);
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
p1,
|
||||
p3,
|
||||
httpBackend!.flushAllExpected(),
|
||||
]);
|
||||
return Promise.all([p1, p3, httpBackend!.flushAllExpected()]);
|
||||
});
|
||||
|
||||
describe("resending", function() {
|
||||
xit("should be able to resend a NOT_SENT event", function() {
|
||||
|
||||
});
|
||||
xit("should be able to resend a sent event", function() {
|
||||
|
||||
});
|
||||
describe("resending", function () {
|
||||
xit("should be able to resend a NOT_SENT event", function () {});
|
||||
xit("should be able to resend a sent event", function () {});
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -23,22 +23,23 @@ import { TestClient } from "../TestClient";
|
||||
import { IEvent } from "../../src";
|
||||
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
|
||||
const ROOM_ID = '!ROOM:ID';
|
||||
const ROOM_ID = "!ROOM:ID";
|
||||
|
||||
const SESSION_ID = 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc';
|
||||
const SESSION_ID = "o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc";
|
||||
|
||||
const ENCRYPTED_EVENT: Partial<IEvent> = {
|
||||
type: 'm.room.encrypted',
|
||||
type: "m.room.encrypted",
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
sender_key: 'SENDER_CURVE25519',
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
sender_key: "SENDER_CURVE25519",
|
||||
session_id: SESSION_ID,
|
||||
ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N'
|
||||
+ 'CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl'
|
||||
+ 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs',
|
||||
ciphertext:
|
||||
"AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N" +
|
||||
"CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl" +
|
||||
"mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs",
|
||||
},
|
||||
room_id: '!ROOM:ID',
|
||||
event_id: '$event1',
|
||||
room_id: "!ROOM:ID",
|
||||
event_id: "$event1",
|
||||
origin_server_ts: 1507753886000,
|
||||
};
|
||||
|
||||
@@ -47,19 +48,20 @@ const CURVE25519_KEY_BACKUP_DATA: IKeyBackupSession = {
|
||||
forwarded_count: 0,
|
||||
is_verified: false,
|
||||
session_data: {
|
||||
ciphertext: '2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw'
|
||||
+ '6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ'
|
||||
+ 'Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9'
|
||||
+ 'SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy'
|
||||
+ 'Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF'
|
||||
+ 'ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV'
|
||||
+ '4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv'
|
||||
+ 'C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe'
|
||||
+ 'Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf'
|
||||
+ 'QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy'
|
||||
+ 'iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg',
|
||||
mac: '5lxYBHQU80M',
|
||||
ephemeral: '/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14',
|
||||
ciphertext:
|
||||
"2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw" +
|
||||
"6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ" +
|
||||
"Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9" +
|
||||
"SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy" +
|
||||
"Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF" +
|
||||
"ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV" +
|
||||
"4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv" +
|
||||
"C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe" +
|
||||
"Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf" +
|
||||
"QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy" +
|
||||
"iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg",
|
||||
mac: "5lxYBHQU80M",
|
||||
ephemeral: "/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -82,16 +84,14 @@ function createOlmSession(olmAccount: Olm.Account, recipientTestClient: TestClie
|
||||
const otk = keys[otkId];
|
||||
|
||||
const session = new global.Olm.Session();
|
||||
session.create_outbound(
|
||||
olmAccount, recipientTestClient.getDeviceKey(), otk.key,
|
||||
);
|
||||
session.create_outbound(olmAccount, recipientTestClient.getDeviceKey(), otk.key);
|
||||
return session;
|
||||
});
|
||||
}
|
||||
|
||||
describe("megolm key backups", function() {
|
||||
describe("megolm key backups", function () {
|
||||
if (!global.Olm) {
|
||||
logger.warn('not running megolm tests: Olm not present');
|
||||
logger.warn("not running megolm tests: Olm not present");
|
||||
return;
|
||||
}
|
||||
const Olm = global.Olm;
|
||||
@@ -99,72 +99,72 @@ describe("megolm key backups", function() {
|
||||
let aliceTestClient: TestClient;
|
||||
|
||||
const setupTestClient = (): [Account, TestClient] => {
|
||||
const aliceTestClient = new TestClient(
|
||||
"@alice:localhost", "xzcvb", "akjgkrgjs",
|
||||
);
|
||||
const aliceTestClient = new TestClient("@alice:localhost", "xzcvb", "akjgkrgjs");
|
||||
const testOlmAccount = new Olm.Account();
|
||||
testOlmAccount!.create();
|
||||
|
||||
return [testOlmAccount, aliceTestClient];
|
||||
};
|
||||
|
||||
beforeAll(function() {
|
||||
beforeAll(function () {
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
beforeEach(async function() {
|
||||
beforeEach(async function () {
|
||||
[testOlmAccount, aliceTestClient] = setupTestClient();
|
||||
await aliceTestClient!.client.initCrypto();
|
||||
aliceTestClient!.client.crypto!.backupManager.backupInfo = CURVE25519_BACKUP_INFO;
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
afterEach(function () {
|
||||
return aliceTestClient!.stop();
|
||||
});
|
||||
|
||||
it("Alice checks key backups when receiving a message she can't decrypt", function() {
|
||||
it("Alice checks key backups when receiving a message she can't decrypt", function () {
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncResponse.rooms.join[ROOM_ID] = {
|
||||
timeline: {
|
||||
events: [ENCRYPTED_EVENT],
|
||||
join: {
|
||||
[ROOM_ID]: {
|
||||
timeline: {
|
||||
events: [ENCRYPTED_EVENT],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return aliceTestClient!.start().then(() => {
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
}).then(() => {
|
||||
const privkey = decodeRecoveryKey(RECOVERY_KEY);
|
||||
return aliceTestClient!.client!.crypto!.storeSessionBackupPrivateKey(privkey);
|
||||
}).then(() => {
|
||||
aliceTestClient!.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
aliceTestClient!.expectKeyBackupQuery(
|
||||
ROOM_ID,
|
||||
SESSION_ID,
|
||||
200,
|
||||
CURVE25519_KEY_BACKUP_DATA,
|
||||
);
|
||||
return aliceTestClient!.httpBackend.flushAllExpected();
|
||||
}).then(function(): Promise<MatrixEvent> {
|
||||
const room = aliceTestClient!.client.getRoom(ROOM_ID)!;
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
return aliceTestClient!
|
||||
.start()
|
||||
.then(() => {
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
})
|
||||
.then(() => {
|
||||
const privkey = decodeRecoveryKey(RECOVERY_KEY);
|
||||
return aliceTestClient!.client!.crypto!.storeSessionBackupPrivateKey(privkey);
|
||||
})
|
||||
.then(() => {
|
||||
aliceTestClient!.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
aliceTestClient!.expectKeyBackupQuery(ROOM_ID, SESSION_ID, 200, CURVE25519_KEY_BACKUP_DATA);
|
||||
return aliceTestClient!.httpBackend.flushAllExpected();
|
||||
})
|
||||
.then(function (): Promise<MatrixEvent> {
|
||||
const room = aliceTestClient!.client.getRoom(ROOM_ID)!;
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
|
||||
if (event.getContent()) {
|
||||
return Promise.resolve(event);
|
||||
}
|
||||
if (event.getContent()) {
|
||||
return Promise.resolve(event);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
event.once(MatrixEventEvent.Decrypted, (ev) => {
|
||||
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
||||
resolve(ev);
|
||||
return new Promise((resolve, reject) => {
|
||||
event.once(MatrixEventEvent.Decrypted, (ev) => {
|
||||
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
||||
resolve(ev);
|
||||
});
|
||||
});
|
||||
})
|
||||
.then((event) => {
|
||||
expect(event.getContent()).toEqual("testytest");
|
||||
});
|
||||
}).then((event) => {
|
||||
expect(event.getContent()).toEqual('testytest');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,18 +22,20 @@ limitations under the License.
|
||||
*
|
||||
* Note that megolm (group) conversation is not tested here.
|
||||
*
|
||||
* See also `megolm.spec.js`.
|
||||
* See also `crypto.spec.js`.
|
||||
*/
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import '../olm-loader';
|
||||
import "../olm-loader";
|
||||
|
||||
import { logger } from '../../src/logger';
|
||||
import type { Session } from "@matrix-org/olm";
|
||||
import { logger } from "../../src/logger";
|
||||
import * as testUtils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { CRYPTO_ENABLED, IUploadKeysRequest } from "../../src/client";
|
||||
import { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, IUploadKeysRequest } from "../../src/client";
|
||||
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix";
|
||||
import { DeviceInfo } from '../../src/crypto/deviceinfo';
|
||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||
import { IDeviceKeys, IOneTimeKey } from "../../src/crypto/dehydration";
|
||||
|
||||
let aliTestClient: TestClient;
|
||||
const roomId = "!room:localhost";
|
||||
@@ -47,39 +49,31 @@ const bobAccessToken = "fewgfkuesa";
|
||||
let aliMessages: IContent[];
|
||||
let bobMessages: IContent[];
|
||||
|
||||
// IMessage isn't exported by src/crypto/algorithms/olm.ts
|
||||
interface OlmPayload {
|
||||
type: number;
|
||||
body: string;
|
||||
}
|
||||
type OlmPayload = ReturnType<Session["encrypt"]>;
|
||||
|
||||
async function bobUploadsDeviceKeys(): Promise<void> {
|
||||
bobTestClient.expectDeviceKeyUpload();
|
||||
await Promise.all([
|
||||
bobTestClient.client.uploadKeys(),
|
||||
bobTestClient.httpBackend.flushAllExpected(),
|
||||
]);
|
||||
await bobTestClient.httpBackend.flushAllExpected();
|
||||
expect(Object.keys(bobTestClient.deviceKeys!).length).not.toEqual(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that querier will query uploader's keys; then flush the http request.
|
||||
*
|
||||
* @return {promise} resolves once the http request has completed.
|
||||
* @returns resolves once the http request has completed.
|
||||
*/
|
||||
function expectQueryKeys(querier: TestClient, uploader: TestClient): Promise<number> {
|
||||
// can't query keys before bob has uploaded them
|
||||
expect(uploader.deviceKeys).toBeTruthy();
|
||||
|
||||
const uploaderKeys = {};
|
||||
uploaderKeys[uploader.deviceId!] = uploader.deviceKeys;
|
||||
querier.httpBackend.when("POST", "/keys/query")
|
||||
.respond(200, function(_path, content: IUploadKeysRequest) {
|
||||
expect(content.device_keys![uploader.userId!]).toEqual([]);
|
||||
const result = {};
|
||||
result[uploader.userId!] = uploaderKeys;
|
||||
return { device_keys: result };
|
||||
});
|
||||
const uploaderKeys: Record<string, IDeviceKeys> = {};
|
||||
uploaderKeys[uploader.deviceId!] = uploader.deviceKeys!;
|
||||
querier.httpBackend.when("POST", "/keys/query").respond(200, function (_path, content: IQueryKeysRequest) {
|
||||
expect(content.device_keys![uploader.userId!]).toEqual([]);
|
||||
const result: Record<string, Record<string, IDeviceKeys>> = {};
|
||||
result[uploader.userId!] = uploaderKeys;
|
||||
return { device_keys: result };
|
||||
});
|
||||
return querier.httpBackend.flush("/keys/query", 1);
|
||||
}
|
||||
const expectAliQueryKeys = () => expectQueryKeys(aliTestClient, bobTestClient);
|
||||
@@ -88,16 +82,14 @@ const expectBobQueryKeys = () => expectQueryKeys(bobTestClient, aliTestClient);
|
||||
/**
|
||||
* Set an expectation that ali will claim one of bob's keys; then flush the http request.
|
||||
*
|
||||
* @return {promise} resolves once the http request has completed.
|
||||
* @returns resolves once the http request has completed.
|
||||
*/
|
||||
async function expectAliClaimKeys(): Promise<void> {
|
||||
const keys = await bobTestClient.awaitOneTimeKeyUpload();
|
||||
aliTestClient.httpBackend.when(
|
||||
"POST", "/keys/claim",
|
||||
).respond(200, function(_path, content: IUploadKeysRequest) {
|
||||
aliTestClient.httpBackend.when("POST", "/keys/claim").respond(200, function (_path, content: IClaimKeysRequest) {
|
||||
const claimType = content.one_time_keys![bobUserId][bobDeviceId];
|
||||
expect(claimType).toEqual("signed_curve25519");
|
||||
let keyId = '';
|
||||
let keyId = "";
|
||||
for (keyId in keys) {
|
||||
if (bobTestClient.oneTimeKeys!.hasOwnProperty(keyId)) {
|
||||
if (keyId.indexOf(claimType + ":") === 0) {
|
||||
@@ -105,7 +97,7 @@ async function expectAliClaimKeys(): Promise<void> {
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = {};
|
||||
const result: Record<string, Record<string, Record<string, IOneTimeKey>>> = {};
|
||||
result[bobUserId] = {};
|
||||
result[bobUserId][bobDeviceId] = {};
|
||||
result[bobUserId][bobDeviceId][keyId] = keys[keyId];
|
||||
@@ -138,8 +130,7 @@ async function aliDownloadsKeys(): Promise<void> {
|
||||
aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const devices = data!.devices[bobUserId]!;
|
||||
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys!.keys);
|
||||
expect(devices[bobDeviceId].verified).
|
||||
toBe(DeviceInfo.DeviceVerification.UNVERIFIED);
|
||||
expect(devices[bobDeviceId].verified).toBe(DeviceInfo.DeviceVerification.UNVERIFIED);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,15 +147,13 @@ const bobEnablesEncryption = () => clientEnablesEncryption(bobTestClient.client)
|
||||
* Ali sends a message, first claiming e2e keys. Set the expectations and
|
||||
* check the results.
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
* @returns which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
async function aliSendsFirstMessage(): Promise<OlmPayload> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [_, ciphertext] = await Promise.all([
|
||||
sendMessage(aliTestClient.client),
|
||||
expectAliQueryKeys()
|
||||
.then(expectAliClaimKeys)
|
||||
.then(expectAliSendMessageRequest),
|
||||
expectAliQueryKeys().then(expectAliClaimKeys).then(expectAliSendMessageRequest),
|
||||
]);
|
||||
return ciphertext;
|
||||
}
|
||||
@@ -173,14 +162,11 @@ async function aliSendsFirstMessage(): Promise<OlmPayload> {
|
||||
* Ali sends a message without first claiming e2e keys. Set the expectations
|
||||
* and check the results.
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
* @returns which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
async function aliSendsMessage(): Promise<OlmPayload> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [_, ciphertext] = await Promise.all([
|
||||
sendMessage(aliTestClient.client),
|
||||
expectAliSendMessageRequest(),
|
||||
]);
|
||||
const [_, ciphertext] = await Promise.all([sendMessage(aliTestClient.client), expectAliSendMessageRequest()]);
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
@@ -188,14 +174,13 @@ async function aliSendsMessage(): Promise<OlmPayload> {
|
||||
* Bob sends a message, first querying (but not claiming) e2e keys. Set the
|
||||
* expectations and check the results.
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Ali's device.
|
||||
* @returns which resolves to the ciphertext for Ali's device.
|
||||
*/
|
||||
async function bobSendsReplyMessage(): Promise<OlmPayload> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [_, ciphertext] = await Promise.all([
|
||||
sendMessage(bobTestClient.client),
|
||||
expectBobQueryKeys()
|
||||
.then(expectBobSendMessageRequest),
|
||||
expectBobQueryKeys().then(expectBobSendMessageRequest),
|
||||
]);
|
||||
return ciphertext;
|
||||
}
|
||||
@@ -203,7 +188,7 @@ async function bobSendsReplyMessage(): Promise<OlmPayload> {
|
||||
/**
|
||||
* Set an expectation that Ali will send a message, and flush the request
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
* @returns which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
async function expectAliSendMessageRequest(): Promise<OlmPayload> {
|
||||
const content = await expectSendMessageRequest(aliTestClient.httpBackend);
|
||||
@@ -217,7 +202,7 @@ async function expectAliSendMessageRequest(): Promise<OlmPayload> {
|
||||
/**
|
||||
* Set an expectation that Bob will send a message, and flush the request
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
* @returns which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
async function expectBobSendMessageRequest(): Promise<OlmPayload> {
|
||||
const content = await expectSendMessageRequest(bobTestClient.httpBackend);
|
||||
@@ -231,15 +216,13 @@ async function expectBobSendMessageRequest(): Promise<OlmPayload> {
|
||||
}
|
||||
|
||||
function sendMessage(client: MatrixClient): Promise<ISendEventResponse> {
|
||||
return client.sendMessage(
|
||||
roomId, { msgtype: "m.text", body: "Hello, World" },
|
||||
);
|
||||
return client.sendMessage(roomId, { msgtype: "m.text", body: "Hello, World" });
|
||||
}
|
||||
|
||||
async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]): Promise<IContent> {
|
||||
const path = "/send/m.room.encrypted/";
|
||||
const prom = new Promise<IContent>((resolve) => {
|
||||
httpBackend.when("PUT", path).respond(200, function(_path, content) {
|
||||
httpBackend.when("PUT", path).respond(200, function (_path, content) {
|
||||
resolve(content);
|
||||
return {
|
||||
event_id: "asdfgh",
|
||||
@@ -254,16 +237,12 @@ async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]):
|
||||
|
||||
function aliRecvMessage(): Promise<void> {
|
||||
const message = bobMessages.shift()!;
|
||||
return recvMessage(
|
||||
aliTestClient.httpBackend, aliTestClient.client, bobUserId, message,
|
||||
);
|
||||
return recvMessage(aliTestClient.httpBackend, aliTestClient.client, bobUserId, message);
|
||||
}
|
||||
|
||||
function bobRecvMessage(): Promise<void> {
|
||||
const message = aliMessages.shift()!;
|
||||
return recvMessage(
|
||||
bobTestClient.httpBackend, bobTestClient.client, aliUserId, message,
|
||||
);
|
||||
return recvMessage(bobTestClient.httpBackend, bobTestClient.client, aliUserId, message);
|
||||
}
|
||||
|
||||
async function recvMessage(
|
||||
@@ -276,32 +255,30 @@ async function recvMessage(
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
[roomId]: {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkEvent({
|
||||
type: "m.room.encrypted",
|
||||
room: roomId,
|
||||
content: message,
|
||||
sender: sender,
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkEvent({
|
||||
type: "m.room.encrypted",
|
||||
room: roomId,
|
||||
content: message,
|
||||
sender: sender,
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
const eventPromise = new Promise<MatrixEvent>((resolve) => {
|
||||
const onEvent = function(event: MatrixEvent) {
|
||||
const onEvent = function (event: MatrixEvent) {
|
||||
// ignore the m.room.member events
|
||||
if (event.getType() == "m.room.member") {
|
||||
return;
|
||||
}
|
||||
logger.log(client.credentials.userId + " received event",
|
||||
event);
|
||||
logger.log(client.credentials.userId + " received event", event);
|
||||
|
||||
client.removeListener(ClientEvent.Event, onEvent);
|
||||
resolve(event);
|
||||
@@ -327,32 +304,32 @@ async function recvMessage(
|
||||
* Send an initial sync response to the client (which just includes the member
|
||||
* list for our test room).
|
||||
*
|
||||
* @param {TestClient} testClient
|
||||
* @returns {Promise} which resolves when the sync has been flushed.
|
||||
* @returns which resolves when the sync has been flushed.
|
||||
*/
|
||||
function firstSync(testClient: TestClient): Promise<void> {
|
||||
// send a sync response including our test room.
|
||||
const syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: { },
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
state: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: "join",
|
||||
user: aliUserId,
|
||||
}),
|
||||
testUtils.mkMembership({
|
||||
mship: "join",
|
||||
user: bobUserId,
|
||||
}),
|
||||
],
|
||||
},
|
||||
timeline: {
|
||||
events: [],
|
||||
join: {
|
||||
[roomId]: {
|
||||
state: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: "join",
|
||||
user: aliUserId,
|
||||
}),
|
||||
testUtils.mkMembership({
|
||||
mship: "join",
|
||||
user: bobUserId,
|
||||
}),
|
||||
],
|
||||
},
|
||||
timeline: {
|
||||
events: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -385,6 +362,13 @@ describe("MatrixClient crypto", () => {
|
||||
|
||||
it("Bob uploads device keys", bobUploadsDeviceKeys);
|
||||
|
||||
it("handles failures to upload device keys", async () => {
|
||||
// since device keys are uploaded asynchronously, there's not really much to do here other than fail the
|
||||
// upload.
|
||||
bobTestClient.httpBackend.when("POST", "/keys/upload").fail(0, new Error("bleh"));
|
||||
await bobTestClient.httpBackend.flushAllExpected();
|
||||
});
|
||||
|
||||
it("Ali downloads Bobs device keys", async () => {
|
||||
await bobUploadsDeviceKeys();
|
||||
await aliDownloadsKeys();
|
||||
@@ -396,10 +380,7 @@ describe("MatrixClient crypto", () => {
|
||||
const bobDeviceKeys = bobTestClient.deviceKeys!;
|
||||
expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeTruthy();
|
||||
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
|
||||
await Promise.all([
|
||||
aliTestClient.client.downloadKeys([bobUserId]),
|
||||
expectAliQueryKeys(),
|
||||
]);
|
||||
await Promise.all([aliTestClient.client.downloadKeys([bobUserId]), expectAliQueryKeys()]);
|
||||
const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId);
|
||||
// should get an empty list
|
||||
expect(devices).toEqual([]);
|
||||
@@ -409,26 +390,24 @@ describe("MatrixClient crypto", () => {
|
||||
const eveUserId = "@eve:localhost";
|
||||
|
||||
const bobDeviceKeys = {
|
||||
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
|
||||
device_id: 'bvcxz',
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "bvcxz",
|
||||
keys: {
|
||||
'ed25519:bvcxz': 'pYuWKMCVuaDLRTM/eWuB8OlXEb61gZhfLVJ+Y54tl0Q',
|
||||
'curve25519:bvcxz': '7Gni0loo/nzF0nFp9847RbhElGewzwUXHPrljjBGPTQ',
|
||||
"ed25519:bvcxz": "pYuWKMCVuaDLRTM/eWuB8OlXEb61gZhfLVJ+Y54tl0Q",
|
||||
"curve25519:bvcxz": "7Gni0loo/nzF0nFp9847RbhElGewzwUXHPrljjBGPTQ",
|
||||
},
|
||||
user_id: '@eve:localhost',
|
||||
user_id: "@eve:localhost",
|
||||
signatures: {
|
||||
'@eve:localhost': {
|
||||
'ed25519:bvcxz': 'CliUPZ7dyVPBxvhSA1d+X+LYa5b2AYdjcTwG' +
|
||||
'0stXcIxjaJNemQqtdgwKDtBFl3pN2I13SEijRDCf1A8bYiQMDg',
|
||||
"@eve:localhost": {
|
||||
"ed25519:bvcxz":
|
||||
"CliUPZ7dyVPBxvhSA1d+X+LYa5b2AYdjcTwG" + "0stXcIxjaJNemQqtdgwKDtBFl3pN2I13SEijRDCf1A8bYiQMDg",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const bobKeys = {};
|
||||
const bobKeys: Record<string, typeof bobDeviceKeys> = {};
|
||||
bobKeys[bobDeviceId] = bobDeviceKeys;
|
||||
aliTestClient.httpBackend.when(
|
||||
"POST", "/keys/query",
|
||||
).respond(200, { device_keys: { [bobUserId]: bobKeys } });
|
||||
aliTestClient.httpBackend.when("POST", "/keys/query").respond(200, { device_keys: { [bobUserId]: bobKeys } });
|
||||
|
||||
await Promise.all([
|
||||
aliTestClient.client.downloadKeys([bobUserId, eveUserId]),
|
||||
@@ -445,26 +424,24 @@ describe("MatrixClient crypto", () => {
|
||||
|
||||
it("Ali gets keys with an incorrect deviceId", async () => {
|
||||
const bobDeviceKeys = {
|
||||
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
|
||||
device_id: 'bad_device',
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "bad_device",
|
||||
keys: {
|
||||
'ed25519:bad_device': 'e8XlY5V8x2yJcwa5xpSzeC/QVOrU+D5qBgyTK0ko+f0',
|
||||
'curve25519:bad_device': 'YxuuLG/4L5xGeP8XPl5h0d7DzyYVcof7J7do+OXz0xc',
|
||||
"ed25519:bad_device": "e8XlY5V8x2yJcwa5xpSzeC/QVOrU+D5qBgyTK0ko+f0",
|
||||
"curve25519:bad_device": "YxuuLG/4L5xGeP8XPl5h0d7DzyYVcof7J7do+OXz0xc",
|
||||
},
|
||||
user_id: '@bob:localhost',
|
||||
user_id: "@bob:localhost",
|
||||
signatures: {
|
||||
'@bob:localhost': {
|
||||
'ed25519:bad_device': 'fEFTq67RaSoIEVBJ8DtmRovbwUBKJ0A' +
|
||||
'me9m9PDzM9azPUwZ38Xvf6vv1A7W1PSafH4z3Y2ORIyEnZgHaNby3CQ',
|
||||
"@bob:localhost": {
|
||||
"ed25519:bad_device":
|
||||
"fEFTq67RaSoIEVBJ8DtmRovbwUBKJ0A" + "me9m9PDzM9azPUwZ38Xvf6vv1A7W1PSafH4z3Y2ORIyEnZgHaNby3CQ",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const bobKeys = {};
|
||||
const bobKeys: Record<string, typeof bobDeviceKeys> = {};
|
||||
bobKeys[bobDeviceId] = bobDeviceKeys;
|
||||
aliTestClient.httpBackend.when(
|
||||
"POST", "/keys/query",
|
||||
).respond(200, { device_keys: { [bobUserId]: bobKeys } });
|
||||
aliTestClient.httpBackend.when("POST", "/keys/query").respond(200, { device_keys: { [bobUserId]: bobKeys } });
|
||||
|
||||
await Promise.all([
|
||||
aliTestClient.client.downloadKeys([bobUserId]),
|
||||
@@ -515,26 +492,25 @@ describe("MatrixClient crypto", () => {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
[roomId]: {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkEvent({
|
||||
type: "m.room.encrypted",
|
||||
room: roomId,
|
||||
content: message,
|
||||
sender: "@bogus:sender",
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkEvent({
|
||||
type: "m.room.encrypted",
|
||||
room: roomId,
|
||||
content: message,
|
||||
sender: "@bogus:sender",
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
bobTestClient.httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
const eventPromise = new Promise<MatrixEvent>((resolve) => {
|
||||
const onEvent = function(event: MatrixEvent) {
|
||||
const onEvent = function (event: MatrixEvent) {
|
||||
logger.log(bobUserId + " received event", event);
|
||||
resolve(event);
|
||||
};
|
||||
@@ -558,11 +534,10 @@ describe("MatrixClient crypto", () => {
|
||||
await aliDownloadsKeys();
|
||||
aliTestClient.client.setDeviceBlocked(bobUserId, bobDeviceId, true);
|
||||
const p1 = sendMessage(aliTestClient.client);
|
||||
const p2 = expectSendMessageRequest(aliTestClient.httpBackend)
|
||||
.then(function(sentContent) {
|
||||
// no unblocked devices, so the ciphertext should be empty
|
||||
expect(sentContent.ciphertext).toEqual({});
|
||||
});
|
||||
const p2 = expectSendMessageRequest(aliTestClient.httpBackend).then(function (sentContent) {
|
||||
// no unblocked devices, so the ciphertext should be empty
|
||||
expect(sentContent.ciphertext).toEqual({});
|
||||
});
|
||||
await Promise.all([p1, p2]);
|
||||
});
|
||||
|
||||
@@ -588,9 +563,7 @@ describe("MatrixClient crypto", () => {
|
||||
await firstSync(bobTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
bobTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, {},
|
||||
);
|
||||
bobTestClient.httpBackend.when("POST", "/keys/query").respond(200, {});
|
||||
await bobRecvMessage();
|
||||
await bobEnablesEncryption();
|
||||
const ciphertext = await bobSendsReplyMessage();
|
||||
@@ -605,28 +578,28 @@ describe("MatrixClient crypto", () => {
|
||||
await aliTestClient.start();
|
||||
await firstSync(aliTestClient);
|
||||
const syncData = {
|
||||
next_batch: '2',
|
||||
next_batch: "2",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
state: {
|
||||
events: [
|
||||
testUtils.mkEvent({
|
||||
type: 'm.room.encryption',
|
||||
skey: '',
|
||||
content: {
|
||||
algorithm: 'm.olm.v1.curve25519-aes-sha2',
|
||||
join: {
|
||||
[roomId]: {
|
||||
state: {
|
||||
events: [
|
||||
testUtils.mkEvent({
|
||||
type: "m.room.encryption",
|
||||
skey: "",
|
||||
content: {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
aliTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, syncData);
|
||||
await aliTestClient.httpBackend.flush('/sync', 1);
|
||||
aliTestClient.httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
await aliTestClient.httpBackend.flush("/sync", 1);
|
||||
aliTestClient.expectKeyQuery({
|
||||
device_keys: {
|
||||
[bobUserId]: {},
|
||||
@@ -649,7 +622,7 @@ describe("MatrixClient crypto", () => {
|
||||
// enqueue expectations:
|
||||
// * Sync with empty one_time_keys => upload keys
|
||||
|
||||
logger.log(aliTestClient + ': starting');
|
||||
logger.log(aliTestClient + ": starting");
|
||||
httpBackend.when("GET", "/versions").respond(200, {});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
@@ -659,24 +632,61 @@ describe("MatrixClient crypto", () => {
|
||||
// it will upload one-time keys.
|
||||
httpBackend.when("GET", "/sync").respond(200, syncDataEmpty);
|
||||
|
||||
await Promise.all([
|
||||
aliTestClient.client.startClient({}),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
logger.log(aliTestClient + ': started');
|
||||
httpBackend.when("POST", "/keys/upload")
|
||||
.respond(200, (_path, content: IUploadKeysRequest) => {
|
||||
expect(content.one_time_keys).toBeTruthy();
|
||||
expect(content.one_time_keys).not.toEqual({});
|
||||
expect(Object.keys(content.one_time_keys!).length).toBeGreaterThanOrEqual(1);
|
||||
// cancel futher calls by telling the client
|
||||
// we have more than we need
|
||||
return {
|
||||
one_time_key_counts: {
|
||||
signed_curve25519: 70,
|
||||
},
|
||||
};
|
||||
});
|
||||
await Promise.all([aliTestClient.client.startClient({}), httpBackend.flushAllExpected()]);
|
||||
logger.log(aliTestClient + ": started");
|
||||
httpBackend.when("POST", "/keys/upload").respond(200, (_path, content: IUploadKeysRequest) => {
|
||||
expect(content.one_time_keys).toBeTruthy();
|
||||
expect(content.one_time_keys).not.toEqual({});
|
||||
expect(Object.keys(content.one_time_keys!).length).toBeGreaterThanOrEqual(1);
|
||||
// cancel futher calls by telling the client
|
||||
// we have more than we need
|
||||
return {
|
||||
one_time_key_counts: {
|
||||
signed_curve25519: 70,
|
||||
},
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
});
|
||||
|
||||
it("Checks for outgoing room key requests for a given event's session", async () => {
|
||||
const eventA0 = new MatrixEvent({
|
||||
sender: "@bob:example.com",
|
||||
room_id: "!someroom",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
session_id: "sessionid",
|
||||
sender_key: "senderkey",
|
||||
},
|
||||
});
|
||||
const eventA1 = new MatrixEvent({
|
||||
sender: "@bob:example.com",
|
||||
room_id: "!someroom",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
session_id: "sessionid",
|
||||
sender_key: "senderkey",
|
||||
},
|
||||
});
|
||||
const eventB = new MatrixEvent({
|
||||
sender: "@bob:example.com",
|
||||
room_id: "!someroom",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
session_id: "othersessionid",
|
||||
sender_key: "senderkey",
|
||||
},
|
||||
});
|
||||
const nonEncryptedEvent = new MatrixEvent({
|
||||
sender: "@bob:example.com",
|
||||
room_id: "!someroom",
|
||||
content: {},
|
||||
});
|
||||
|
||||
aliTestClient.client.crypto?.onSyncCompleted({});
|
||||
await aliTestClient.client.cancelAndResendEventRoomKeyRequest(eventA0);
|
||||
expect(await aliTestClient.client.getOutgoingRoomKeyRequest(eventA1)).not.toBeNull();
|
||||
expect(await aliTestClient.client.getOutgoingRoomKeyRequest(eventB)).toBeNull();
|
||||
expect(await aliTestClient.client.getOutgoingRoomKeyRequest(nonEncryptedEvent)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
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 "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
|
||||
import { createClient } from "../../src";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = new IDBFactory();
|
||||
});
|
||||
|
||||
describe("MatrixClient.initRustCrypto", () => {
|
||||
it("should raise if userId or deviceId is unknown", async () => {
|
||||
const unknownUserClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
deviceId: "aliceDevice",
|
||||
});
|
||||
await expect(() => unknownUserClient.initRustCrypto()).rejects.toThrow("unknown userId");
|
||||
|
||||
const unknownDeviceClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:test",
|
||||
});
|
||||
await expect(() => unknownDeviceClient.initRustCrypto()).rejects.toThrow("unknown deviceId");
|
||||
});
|
||||
|
||||
it("should create the indexed dbs", async () => {
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
});
|
||||
|
||||
// No databases.
|
||||
expect(await indexedDB.databases()).toHaveLength(0);
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
// should have two dbs now
|
||||
const databaseNames = (await indexedDB.databases()).map((db) => db.name);
|
||||
expect(databaseNames).toEqual(
|
||||
expect.arrayContaining(["matrix-js-sdk::matrix-sdk-crypto", "matrix-js-sdk::matrix-sdk-crypto-meta"]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should ignore a second call", async () => {
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
await matrixClient.initRustCrypto();
|
||||
});
|
||||
});
|
||||
|
||||
describe("MatrixClient.clearStores", () => {
|
||||
it("should clear the indexeddbs", async () => {
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
expect(await indexedDB.databases()).toHaveLength(2);
|
||||
await matrixClient.stopClient();
|
||||
|
||||
await matrixClient.clearStores();
|
||||
expect(await indexedDB.databases()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -22,11 +22,23 @@ import { SlidingSync, SlidingSyncEvent, MSC3575RoomData, SlidingSyncState, Exten
|
||||
import { TestClient } from "../TestClient";
|
||||
import { IRoomEvent, IStateEvent } from "../../src/sync-accumulator";
|
||||
import {
|
||||
MatrixClient, MatrixEvent, NotificationCountType, JoinRule, MatrixError,
|
||||
EventType, IPushRules, PushRuleKind, TweakName, ClientEvent, RoomMemberEvent,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
NotificationCountType,
|
||||
JoinRule,
|
||||
MatrixError,
|
||||
EventType,
|
||||
IPushRules,
|
||||
PushRuleKind,
|
||||
TweakName,
|
||||
ClientEvent,
|
||||
RoomMemberEvent,
|
||||
RoomEvent,
|
||||
Room,
|
||||
IRoomTimelineData,
|
||||
} from "../../src";
|
||||
import { SlidingSyncSdk } from "../../src/sliding-sync-sdk";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { SyncApiOptions, SyncState } from "../../src/sync";
|
||||
import { IStoredClientOpts } from "../../src/client";
|
||||
import { logger } from "../../src/logger";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
@@ -40,10 +52,9 @@ describe("SlidingSyncSdk", () => {
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
|
||||
const mockifySlidingSync = (s: SlidingSync): SlidingSync => {
|
||||
s.getList = jest.fn();
|
||||
s.getListParams = jest.fn();
|
||||
s.getListData = jest.fn();
|
||||
s.getRoomSubscriptions = jest.fn();
|
||||
s.listLength = jest.fn();
|
||||
s.modifyRoomSubscriptionInfo = jest.fn();
|
||||
s.modifyRoomSubscriptions = jest.fn();
|
||||
s.registerExtension = jest.fn();
|
||||
@@ -67,7 +78,7 @@ describe("SlidingSyncSdk", () => {
|
||||
event_id: "$" + eventIdCounter,
|
||||
};
|
||||
};
|
||||
const mkOwnStateEvent = (evType: string, content: object, stateKey = ''): IStateEvent => {
|
||||
const mkOwnStateEvent = (evType: string, content: object, stateKey = ""): IStateEvent => {
|
||||
eventIdCounter++;
|
||||
return {
|
||||
type: evType,
|
||||
@@ -97,19 +108,20 @@ describe("SlidingSyncSdk", () => {
|
||||
};
|
||||
|
||||
// assign client/httpBackend globals
|
||||
const setupClient = async (testOpts?: Partial<IStoredClientOpts&{withCrypto: boolean}>) => {
|
||||
const setupClient = async (testOpts?: Partial<IStoredClientOpts & { withCrypto: boolean }>) => {
|
||||
testOpts = testOpts || {};
|
||||
const syncOpts: SyncApiOptions = {};
|
||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
mockSlidingSync = mockifySlidingSync(new SlidingSync("", [], {}, client, 0));
|
||||
mockSlidingSync = mockifySlidingSync(new SlidingSync("", new Map(), {}, client, 0));
|
||||
if (testOpts.withCrypto) {
|
||||
httpBackend!.when("GET", "/room_keys/version").respond(404, {});
|
||||
await client!.initCrypto();
|
||||
testOpts.crypto = client!.crypto;
|
||||
syncOpts.cryptoCallbacks = syncOpts.crypto = client!.crypto;
|
||||
}
|
||||
httpBackend!.when("GET", "/_matrix/client/r0/pushrules").respond(200, {});
|
||||
sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts);
|
||||
sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts, syncOpts);
|
||||
};
|
||||
|
||||
// tear down client/httpBackend globals
|
||||
@@ -119,13 +131,13 @@ describe("SlidingSyncSdk", () => {
|
||||
};
|
||||
|
||||
// find an extension on a SlidingSyncSdk instance
|
||||
const findExtension = (name: string): Extension => {
|
||||
const findExtension = (name: string): Extension<any, any> => {
|
||||
expect(mockSlidingSync!.registerExtension).toHaveBeenCalled();
|
||||
const mockFn = mockSlidingSync!.registerExtension as jest.Mock;
|
||||
// find the extension
|
||||
for (let i = 0; i < mockFn.mock.calls.length; i++) {
|
||||
const calledExtension = mockFn.mock.calls[i][0] as Extension;
|
||||
if (calledExtension && calledExtension.name() === name) {
|
||||
const calledExtension = mockFn.mock.calls[i][0] as Extension<any, any>;
|
||||
if (calledExtension?.name() === name) {
|
||||
return calledExtension;
|
||||
}
|
||||
}
|
||||
@@ -170,6 +182,7 @@ describe("SlidingSyncSdk", () => {
|
||||
const roomE = "!e_with_invite:localhost";
|
||||
const roomF = "!f_calc_room_name:localhost";
|
||||
const roomG = "!g_join_invite_counts:localhost";
|
||||
const roomH = "!g_num_live:localhost";
|
||||
const data: Record<string, MSC3575RoomData> = {
|
||||
[roomA]: {
|
||||
name: "A",
|
||||
@@ -194,7 +207,6 @@ describe("SlidingSyncSdk", () => {
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello B" }),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "world B" }),
|
||||
|
||||
],
|
||||
initial: true,
|
||||
},
|
||||
@@ -275,13 +287,27 @@ describe("SlidingSyncSdk", () => {
|
||||
invited_count: 2,
|
||||
initial: true,
|
||||
},
|
||||
[roomH]: {
|
||||
name: "H",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "live event" }),
|
||||
],
|
||||
initial: true,
|
||||
num_live: 1,
|
||||
},
|
||||
};
|
||||
|
||||
it("can be created with required_state and timeline", () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]);
|
||||
const gotRoom = client!.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.name).toEqual(data[roomA].name);
|
||||
expect(gotRoom.getMyMembership()).toEqual("join");
|
||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline);
|
||||
@@ -291,7 +317,9 @@ describe("SlidingSyncSdk", () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]);
|
||||
const gotRoom = client!.getRoom(roomB);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.name).toEqual(data[roomB].name);
|
||||
expect(gotRoom.getMyMembership()).toEqual("join");
|
||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline);
|
||||
@@ -301,36 +329,73 @@ describe("SlidingSyncSdk", () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]);
|
||||
const gotRoom = client!.getRoom(roomC);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight),
|
||||
).toEqual(data[roomC].highlight_count);
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(
|
||||
data[roomC].highlight_count,
|
||||
);
|
||||
});
|
||||
|
||||
it("can be created with a notification_count", () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]);
|
||||
const gotRoom = client!.getRoom(roomD);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
|
||||
).toEqual(data[roomD].notification_count);
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(
|
||||
data[roomD].notification_count,
|
||||
);
|
||||
});
|
||||
|
||||
it("can be created with an invited/joined_count", () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomG, data[roomG]);
|
||||
const gotRoom = client!.getRoom(roomG);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getInvitedMemberCount()).toEqual(data[roomG].invited_count);
|
||||
expect(gotRoom.getJoinedMemberCount()).toEqual(data[roomG].joined_count);
|
||||
});
|
||||
|
||||
it("can be created with live events", () => {
|
||||
let seenLiveEvent = false;
|
||||
const listener = (
|
||||
ev: MatrixEvent,
|
||||
room?: Room,
|
||||
toStartOfTimeline?: boolean,
|
||||
deleted?: boolean,
|
||||
timelineData?: IRoomTimelineData,
|
||||
) => {
|
||||
if (timelineData?.liveEvent) {
|
||||
assertTimelineEvents([ev], data[roomH].timeline.slice(-1));
|
||||
seenLiveEvent = true;
|
||||
}
|
||||
};
|
||||
client!.on(RoomEvent.Timeline, listener);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomH, data[roomH]);
|
||||
client!.off(RoomEvent.Timeline, listener);
|
||||
const gotRoom = client!.getRoom(roomH);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.name).toEqual(data[roomH].name);
|
||||
expect(gotRoom.getMyMembership()).toEqual("join");
|
||||
// check the entire timeline is correct
|
||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents(), data[roomH].timeline);
|
||||
expect(seenLiveEvent).toBe(true);
|
||||
});
|
||||
|
||||
it("can be created with invite_state", () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]);
|
||||
const gotRoom = client!.getRoom(roomE);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getMyMembership()).toEqual("invite");
|
||||
expect(gotRoom.currentState.getJoinRule()).toEqual(JoinRule.Invite);
|
||||
});
|
||||
@@ -339,10 +404,10 @@ describe("SlidingSyncSdk", () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]);
|
||||
const gotRoom = client!.getRoom(roomF);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
gotRoom.name,
|
||||
).toEqual(data[roomF].name);
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.name).toEqual(data[roomF].name);
|
||||
});
|
||||
|
||||
describe("updating", () => {
|
||||
@@ -355,7 +420,9 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
const gotRoom = client!.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
const newTimeline = data[roomA].timeline;
|
||||
newTimeline.push(newEvent);
|
||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-3), newTimeline);
|
||||
@@ -364,18 +431,20 @@ describe("SlidingSyncSdk", () => {
|
||||
it("can update with a new required_state event", async () => {
|
||||
let gotRoom = client!.getRoom(roomB);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Invite); // default
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, {
|
||||
required_state: [
|
||||
mkOwnStateEvent("m.room.join_rules", { join_rule: "restricted" }, ""),
|
||||
],
|
||||
required_state: [mkOwnStateEvent("m.room.join_rules", { join_rule: "restricted" }, "")],
|
||||
timeline: [],
|
||||
name: data[roomB].name,
|
||||
});
|
||||
gotRoom = client!.getRoom(roomB);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Restricted);
|
||||
});
|
||||
|
||||
@@ -388,10 +457,10 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
const gotRoom = client!.getRoom(roomC);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight),
|
||||
).toEqual(1);
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(1);
|
||||
});
|
||||
|
||||
it("can update with a new notification_count", async () => {
|
||||
@@ -403,10 +472,10 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
const gotRoom = client!.getRoom(roomD);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
|
||||
).toEqual(1);
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(1);
|
||||
});
|
||||
|
||||
it("can update with a new joined_count", () => {
|
||||
@@ -418,7 +487,9 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
const gotRoom = client!.getRoom(roomG);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
expect(gotRoom.getJoinedMemberCount()).toEqual(1);
|
||||
});
|
||||
|
||||
@@ -442,11 +513,20 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
const gotRoom = client!.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
if (gotRoom == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log("want:", oldTimeline.map((e) => (e.type + " : " + (e.content || {}).body)));
|
||||
logger.log("got:", gotRoom.getLiveTimeline().getEvents().map(
|
||||
(e) => (e.getType() + " : " + e.getContent().body)),
|
||||
logger.log(
|
||||
"want:",
|
||||
oldTimeline.map((e) => e.type + " : " + (e.content || {}).body),
|
||||
);
|
||||
logger.log(
|
||||
"got:",
|
||||
gotRoom
|
||||
.getLiveTimeline()
|
||||
.getEvents()
|
||||
.map((e) => e.getType() + " : " + e.getContent().body),
|
||||
);
|
||||
|
||||
// we expect the timeline now to be oldTimeline (so the old events are in fact old)
|
||||
@@ -466,40 +546,54 @@ describe("SlidingSyncSdk", () => {
|
||||
const FAILED_SYNC_ERROR_THRESHOLD = 3; // would be nice to export the const in the actual class...
|
||||
|
||||
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: {} },
|
||||
);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, {
|
||||
pos: "h",
|
||||
lists: {},
|
||||
rooms: {},
|
||||
extensions: {},
|
||||
});
|
||||
expect(sdk!.getSyncState()).toEqual(SyncState.Syncing);
|
||||
|
||||
mockSlidingSync!.emit(
|
||||
SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new Error("generic"),
|
||||
SlidingSyncEvent.Lifecycle,
|
||||
SlidingSyncState.RequestFinished,
|
||||
null,
|
||||
new Error("generic"),
|
||||
);
|
||||
expect(sdk!.getSyncState()).toEqual(SyncState.Reconnecting);
|
||||
|
||||
for (let i = 0; i < FAILED_SYNC_ERROR_THRESHOLD; i++) {
|
||||
mockSlidingSync!.emit(
|
||||
SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new Error("generic"),
|
||||
SlidingSyncEvent.Lifecycle,
|
||||
SlidingSyncState.RequestFinished,
|
||||
null,
|
||||
new Error("generic"),
|
||||
);
|
||||
}
|
||||
expect(sdk!.getSyncState()).toEqual(SyncState.Error);
|
||||
});
|
||||
|
||||
it("emits SyncState.Syncing after a previous SyncState.Error", async () => {
|
||||
mockSlidingSync!.emit(
|
||||
SlidingSyncEvent.Lifecycle,
|
||||
SlidingSyncState.Complete,
|
||||
{ pos: "i", lists: [], rooms: {}, extensions: {} },
|
||||
);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, {
|
||||
pos: "i",
|
||||
lists: {},
|
||||
rooms: {},
|
||||
extensions: {},
|
||||
});
|
||||
expect(sdk!.getSyncState()).toEqual(SyncState.Syncing);
|
||||
});
|
||||
|
||||
it("emits SyncState.Error immediately when receiving M_UNKNOWN_TOKEN and stops syncing", async () => {
|
||||
expect(mockSlidingSync!.stop).not.toBeCalled();
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new MatrixError({
|
||||
errcode: "M_UNKNOWN_TOKEN",
|
||||
message: "Oh no your access token is no longer valid",
|
||||
}));
|
||||
mockSlidingSync!.emit(
|
||||
SlidingSyncEvent.Lifecycle,
|
||||
SlidingSyncState.RequestFinished,
|
||||
null,
|
||||
new MatrixError({
|
||||
errcode: "M_UNKNOWN_TOKEN",
|
||||
message: "Oh no your access token is no longer valid",
|
||||
}),
|
||||
);
|
||||
expect(sdk!.getSyncState()).toEqual(SyncState.Error);
|
||||
expect(mockSlidingSync!.stop).toBeCalled();
|
||||
});
|
||||
@@ -541,7 +635,8 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
describe("ExtensionE2EE", () => {
|
||||
let ext: Extension;
|
||||
let ext: Extension<any, any>;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupClient({
|
||||
withCrypto: true,
|
||||
@@ -551,18 +646,21 @@ describe("SlidingSyncSdk", () => {
|
||||
await hasSynced;
|
||||
ext = findExtension("e2ee");
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// needed else we do some async operations in the background which can cause Jest to whine:
|
||||
// "Cannot log after tests are done. Did you forget to wait for something async in your test?"
|
||||
// Attempted to log "Saving device tracking data null"."
|
||||
client!.crypto!.stop();
|
||||
});
|
||||
|
||||
it("gets enabled on the initial request only", () => {
|
||||
expect(ext.onRequest(true)).toEqual({
|
||||
enabled: true,
|
||||
});
|
||||
expect(ext.onRequest(false)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("can update device lists", () => {
|
||||
ext.onResponse({
|
||||
device_lists: {
|
||||
@@ -572,6 +670,7 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
// TODO: more assertions?
|
||||
});
|
||||
|
||||
it("can update OTK counts", () => {
|
||||
client!.crypto!.updateOneTimeKeyCount = jest.fn();
|
||||
ext.onResponse({
|
||||
@@ -588,6 +687,7 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
expect(client!.crypto!.updateOneTimeKeyCount).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it("can update fallback keys", () => {
|
||||
ext.onResponse({
|
||||
device_unused_fallback_key_types: ["signed_curve25519"],
|
||||
@@ -599,8 +699,10 @@ describe("SlidingSyncSdk", () => {
|
||||
expect(client!.crypto!.getNeedsNewFallback()).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ExtensionAccountData", () => {
|
||||
let ext: Extension;
|
||||
let ext: Extension<any, any>;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupClient();
|
||||
const hasSynced = sdk!.sync();
|
||||
@@ -608,12 +710,14 @@ describe("SlidingSyncSdk", () => {
|
||||
await hasSynced;
|
||||
ext = findExtension("account_data");
|
||||
});
|
||||
|
||||
it("gets enabled on the initial request only", () => {
|
||||
expect(ext.onRequest(true)).toEqual({
|
||||
enabled: true,
|
||||
});
|
||||
expect(ext.onRequest(false)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("processes global account data", async () => {
|
||||
const globalType = "global_test";
|
||||
const globalContent = {
|
||||
@@ -633,6 +737,7 @@ describe("SlidingSyncSdk", () => {
|
||||
expect(globalData).toBeDefined();
|
||||
expect(globalData.getContent()).toEqual(globalContent);
|
||||
});
|
||||
|
||||
it("processes rooms account data", async () => {
|
||||
const roomId = "!room:id";
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
@@ -643,7 +748,6 @@ describe("SlidingSyncSdk", () => {
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
|
||||
|
||||
],
|
||||
initial: true,
|
||||
});
|
||||
@@ -667,6 +771,7 @@ describe("SlidingSyncSdk", () => {
|
||||
expect(event).toBeDefined();
|
||||
expect(event.getContent()).toEqual(roomContent);
|
||||
});
|
||||
|
||||
it("doesn't crash for unknown room account data", async () => {
|
||||
const unknownRoomId = "!unknown:id";
|
||||
const roomType = "tester";
|
||||
@@ -686,22 +791,25 @@ describe("SlidingSyncSdk", () => {
|
||||
expect(room).toBeNull();
|
||||
expect(client!.getAccountData(roomType)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("can update push rules via account data", async () => {
|
||||
const roomId = "!foo:bar";
|
||||
const pushRulesContent: IPushRules = {
|
||||
global: {
|
||||
[PushRuleKind.RoomSpecific]: [{
|
||||
enabled: true,
|
||||
default: true,
|
||||
pattern: "monkey",
|
||||
actions: [
|
||||
{
|
||||
set_tweak: TweakName.Sound,
|
||||
value: "default",
|
||||
},
|
||||
],
|
||||
rule_id: roomId,
|
||||
}],
|
||||
[PushRuleKind.RoomSpecific]: [
|
||||
{
|
||||
enabled: true,
|
||||
default: true,
|
||||
pattern: "monkey",
|
||||
actions: [
|
||||
{
|
||||
set_tweak: TweakName.Sound,
|
||||
value: "default",
|
||||
},
|
||||
],
|
||||
rule_id: roomId,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
let pushRule = client!.getRoomPushRule("global", roomId);
|
||||
@@ -718,8 +826,10 @@ describe("SlidingSyncSdk", () => {
|
||||
expect(pushRule).toEqual(pushRulesContent.global[PushRuleKind.RoomSpecific]![0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ExtensionToDevice", () => {
|
||||
let ext: Extension;
|
||||
let ext: Extension<any, any>;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupClient();
|
||||
const hasSynced = sdk!.sync();
|
||||
@@ -727,12 +837,14 @@ describe("SlidingSyncSdk", () => {
|
||||
await hasSynced;
|
||||
ext = findExtension("to_device");
|
||||
});
|
||||
|
||||
it("gets enabled with a limit on the initial request only", () => {
|
||||
const reqJson: any = ext.onRequest(true);
|
||||
expect(reqJson.enabled).toEqual(true);
|
||||
expect(reqJson.limit).toBeGreaterThan(0);
|
||||
expect(reqJson.since).toBeUndefined();
|
||||
});
|
||||
|
||||
it("updates the since value", async () => {
|
||||
ext.onResponse({
|
||||
next_batch: "12345",
|
||||
@@ -742,12 +854,14 @@ describe("SlidingSyncSdk", () => {
|
||||
since: "12345",
|
||||
});
|
||||
});
|
||||
|
||||
it("can handle missing fields", async () => {
|
||||
ext.onResponse({
|
||||
next_batch: "23456",
|
||||
// no events array
|
||||
});
|
||||
});
|
||||
|
||||
it("emits to-device events on the client", async () => {
|
||||
const toDeviceType = "custom_test";
|
||||
const toDeviceContent = {
|
||||
@@ -770,6 +884,7 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
expect(called).toBe(true);
|
||||
});
|
||||
|
||||
it("can cancel key verification requests", async () => {
|
||||
const seen: Record<string, boolean> = {};
|
||||
client!.on(ClientEvent.ToDeviceEvent, (ev) => {
|
||||
@@ -809,4 +924,189 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("ExtensionTyping", () => {
|
||||
let ext: Extension<any, any>;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupClient();
|
||||
const hasSynced = sdk!.sync();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await hasSynced;
|
||||
ext = findExtension("typing");
|
||||
});
|
||||
|
||||
it("gets enabled on the initial request only", () => {
|
||||
expect(ext.onRequest(true)).toEqual({
|
||||
enabled: true,
|
||||
});
|
||||
expect(ext.onRequest(false)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("processes typing notifications", async () => {
|
||||
const roomId = "!room:id";
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
name: "Room with typing",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
|
||||
],
|
||||
initial: true,
|
||||
});
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeDefined();
|
||||
expect(room.getMember(selfUserId)?.typing).toEqual(false);
|
||||
ext.onResponse({
|
||||
rooms: {
|
||||
[roomId]: {
|
||||
type: EventType.Typing,
|
||||
content: {
|
||||
user_ids: [selfUserId],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(room.getMember(selfUserId)?.typing).toEqual(true);
|
||||
ext.onResponse({
|
||||
rooms: {
|
||||
[roomId]: {
|
||||
type: EventType.Typing,
|
||||
content: {
|
||||
user_ids: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(room.getMember(selfUserId)?.typing).toEqual(false);
|
||||
});
|
||||
|
||||
it("gracefully handles missing rooms and members when typing", async () => {
|
||||
const roomId = "!room:id";
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
name: "Room with typing",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
|
||||
],
|
||||
initial: true,
|
||||
});
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeDefined();
|
||||
expect(room.getMember(selfUserId)?.typing).toEqual(false);
|
||||
ext.onResponse({
|
||||
rooms: {
|
||||
[roomId]: {
|
||||
type: EventType.Typing,
|
||||
content: {
|
||||
user_ids: ["@someone:else"],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(room.getMember(selfUserId)?.typing).toEqual(false);
|
||||
ext.onResponse({
|
||||
rooms: {
|
||||
"!something:else": {
|
||||
type: EventType.Typing,
|
||||
content: {
|
||||
user_ids: [selfUserId],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(room.getMember(selfUserId)?.typing).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ExtensionReceipts", () => {
|
||||
let ext: Extension<any, any>;
|
||||
|
||||
const generateReceiptResponse = (
|
||||
userId: string,
|
||||
roomId: string,
|
||||
eventId: string,
|
||||
recType: string,
|
||||
ts: number,
|
||||
) => {
|
||||
return {
|
||||
rooms: {
|
||||
[roomId]: {
|
||||
type: EventType.Receipt,
|
||||
content: {
|
||||
[eventId]: {
|
||||
[recType]: {
|
||||
[userId]: {
|
||||
ts: ts,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupClient();
|
||||
const hasSynced = sdk!.sync();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await hasSynced;
|
||||
ext = findExtension("receipts");
|
||||
});
|
||||
|
||||
it("gets enabled on the initial request only", () => {
|
||||
expect(ext.onRequest(true)).toEqual({
|
||||
enabled: true,
|
||||
});
|
||||
expect(ext.onRequest(false)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("processes receipts", async () => {
|
||||
const roomId = "!room:id";
|
||||
const alice = "@alice:alice";
|
||||
const lastEvent = mkOwnEvent(EventType.RoomMessage, { body: "hello" });
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
name: "Room with receipts",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
{
|
||||
type: EventType.RoomMember,
|
||||
state_key: alice,
|
||||
content: { membership: "join" },
|
||||
sender: alice,
|
||||
origin_server_ts: Date.now(),
|
||||
event_id: "$alice",
|
||||
},
|
||||
lastEvent,
|
||||
],
|
||||
initial: true,
|
||||
});
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeDefined();
|
||||
expect(room.getReadReceiptForUserId(alice, true)).toBeNull();
|
||||
ext.onResponse(generateReceiptResponse(alice, roomId, lastEvent.event_id, "m.read", 1234567));
|
||||
const receipt = room.getReadReceiptForUserId(alice);
|
||||
expect(receipt).toBeDefined();
|
||||
expect(receipt?.eventId).toEqual(lastEvent.event_id);
|
||||
expect(receipt?.data.ts).toEqual(1234567);
|
||||
expect(receipt?.data.thread_id).toBeFalsy();
|
||||
});
|
||||
|
||||
it("gracefully handles missing rooms when receiving receipts", async () => {
|
||||
const roomId = "!room:id";
|
||||
const alice = "@alice:alice";
|
||||
const eventId = "$something";
|
||||
ext.onResponse(generateReceiptResponse(alice, roomId, eventId, "m.read", 1234567));
|
||||
// we expect it not to crash
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+929
-649
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -15,13 +15,13 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from '../src/logger';
|
||||
import { logger } from "../src/logger";
|
||||
|
||||
// try to load the olm library.
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
global.Olm = require('@matrix-org/olm');
|
||||
logger.log('loaded libolm');
|
||||
global.Olm = require("@matrix-org/olm");
|
||||
logger.log("loaded libolm");
|
||||
} catch (e) {
|
||||
logger.warn("unable to run crypto tests: libolm not available");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
class JestSlowTestReporter {
|
||||
constructor(globalConfig, options) {
|
||||
this._globalConfig = globalConfig;
|
||||
this._options = options;
|
||||
this._slowTests = [];
|
||||
this._slowTestSuites = [];
|
||||
}
|
||||
|
||||
onRunComplete() {
|
||||
const displayResult = (result, isTestSuite) => {
|
||||
if (!isTestSuite) console.log();
|
||||
|
||||
result.sort((a, b) => b.duration - a.duration);
|
||||
const rootPathRegex = new RegExp(`^${process.cwd()}`);
|
||||
const slowestTests = result.slice(0, this._options.numTests || 10);
|
||||
const slowTestTime = this._slowTestTime(slowestTests);
|
||||
const allTestTime = this._allTestTime(result);
|
||||
const percentTime = (slowTestTime / allTestTime) * 100;
|
||||
|
||||
if (isTestSuite) {
|
||||
console.log(
|
||||
`Top ${slowestTests.length} slowest test suites (${slowTestTime / 1000} seconds,` +
|
||||
` ${percentTime.toFixed(1)}% of total time):`,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`Top ${slowestTests.length} slowest tests (${slowTestTime / 1000} seconds,` +
|
||||
` ${percentTime.toFixed(1)}% of total time):`,
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < slowestTests.length; i++) {
|
||||
const duration = slowestTests[i].duration;
|
||||
const filePath = slowestTests[i].filePath.replace(rootPathRegex, ".");
|
||||
|
||||
if (isTestSuite) {
|
||||
console.log(` ${duration / 1000} seconds ${filePath}`);
|
||||
} else {
|
||||
const fullName = slowestTests[i].fullName;
|
||||
console.log(` ${fullName}`);
|
||||
console.log(` ${duration / 1000} seconds ${filePath}`);
|
||||
}
|
||||
}
|
||||
console.log();
|
||||
};
|
||||
|
||||
displayResult(this._slowTests);
|
||||
displayResult(this._slowTestSuites, true);
|
||||
}
|
||||
|
||||
onTestResult(test, testResult) {
|
||||
this._slowTestSuites.push({
|
||||
duration: testResult.perfStats.runtime,
|
||||
filePath: testResult.testFilePath,
|
||||
});
|
||||
for (let i = 0; i < testResult.testResults.length; i++) {
|
||||
this._slowTests.push({
|
||||
duration: testResult.testResults[i].duration,
|
||||
fullName: testResult.testResults[i].fullName,
|
||||
filePath: testResult.testFilePath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_slowTestTime(slowestTests) {
|
||||
let slowTestTime = 0;
|
||||
for (let i = 0; i < slowestTests.length; i++) {
|
||||
slowTestTime += slowestTests[i].duration;
|
||||
}
|
||||
return slowTestTime;
|
||||
}
|
||||
|
||||
_allTestTime(result) {
|
||||
let allTestTime = 0;
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
allTestTime += result[i].duration;
|
||||
}
|
||||
return allTestTime;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = JestSlowTestReporter;
|
||||
+12
-21
@@ -17,10 +17,7 @@ limitations under the License.
|
||||
import { MatrixEvent } from "../../src";
|
||||
import { M_BEACON, M_BEACON_INFO } from "../../src/@types/beacon";
|
||||
import { LocationAssetType } from "../../src/@types/location";
|
||||
import {
|
||||
makeBeaconContent,
|
||||
makeBeaconInfoContent,
|
||||
} from "../../src/content-helpers";
|
||||
import { makeBeaconContent, makeBeaconInfoContent } from "../../src/content-helpers";
|
||||
|
||||
type InfoContentProps = {
|
||||
timeout: number;
|
||||
@@ -44,13 +41,7 @@ export const makeBeaconInfoEvent = (
|
||||
contentProps: Partial<InfoContentProps> = {},
|
||||
eventId?: string,
|
||||
): MatrixEvent => {
|
||||
const {
|
||||
timeout,
|
||||
isLive,
|
||||
description,
|
||||
assetType,
|
||||
timestamp,
|
||||
} = {
|
||||
const { timeout, isLive, description, assetType, timestamp } = {
|
||||
...DEFAULT_INFO_CONTENT_PROPS,
|
||||
...contentProps,
|
||||
};
|
||||
@@ -77,9 +68,9 @@ type ContentProps = {
|
||||
description?: string;
|
||||
};
|
||||
const DEFAULT_CONTENT_PROPS: ContentProps = {
|
||||
uri: 'geo:-36.24484561954707,175.46884959563613;u=10',
|
||||
uri: "geo:-36.24484561954707,175.46884959563613;u=10",
|
||||
timestamp: 123,
|
||||
beaconInfoId: '$123',
|
||||
beaconInfoId: "$123",
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -87,10 +78,7 @@ const DEFAULT_CONTENT_PROPS: ContentProps = {
|
||||
* all required properties are mocked
|
||||
* override with contentProps
|
||||
*/
|
||||
export const makeBeaconEvent = (
|
||||
sender: string,
|
||||
contentProps: Partial<ContentProps> = {},
|
||||
): MatrixEvent => {
|
||||
export const makeBeaconEvent = (sender: string, contentProps: Partial<ContentProps> = {}): MatrixEvent => {
|
||||
const { uri, timestamp, beaconInfoId, description } = {
|
||||
...DEFAULT_CONTENT_PROPS,
|
||||
...contentProps,
|
||||
@@ -107,10 +95,13 @@ export const makeBeaconEvent = (
|
||||
* Create a mock geolocation position
|
||||
* defaults all required properties
|
||||
*/
|
||||
export const makeGeolocationPosition = (
|
||||
{ timestamp, coords }:
|
||||
{ timestamp?: number, coords: Partial<GeolocationCoordinates> },
|
||||
): GeolocationPosition => ({
|
||||
export const makeGeolocationPosition = ({
|
||||
timestamp,
|
||||
coords,
|
||||
}: {
|
||||
timestamp?: number;
|
||||
coords: Partial<GeolocationCoordinates>;
|
||||
}): GeolocationPosition => ({
|
||||
timestamp: timestamp ?? 1647256791840,
|
||||
coords: {
|
||||
accuracy: 1,
|
||||
|
||||
@@ -58,11 +58,11 @@ export const getMockClientWithEventEmitter = (
|
||||
});
|
||||
* ```
|
||||
*/
|
||||
export const mockClientMethodsUser = (userId = '@alice:domain') => ({
|
||||
export const mockClientMethodsUser = (userId = "@alice:domain") => ({
|
||||
getUserId: jest.fn().mockReturnValue(userId),
|
||||
getUser: jest.fn().mockReturnValue(new User(userId)),
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
mxcUrlToHttp: jest.fn().mockReturnValue('mock-mxcUrlToHttp'),
|
||||
mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
|
||||
credentials: { userId },
|
||||
getThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
|
||||
getAccessToken: jest.fn(),
|
||||
@@ -91,4 +91,3 @@ export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixC
|
||||
getCapabilities: jest.fn().mockReturnValue({}),
|
||||
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
|
||||
});
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ limitations under the License.
|
||||
// and avoids assuming anything about the app's behaviour.
|
||||
const realSetTimeout = setTimeout;
|
||||
export function flushPromises() {
|
||||
return new Promise(r => {
|
||||
return new Promise((r) => {
|
||||
realSetTimeout(r, 1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
import EventEmitter from "events";
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import '../olm-loader';
|
||||
import "../olm-loader";
|
||||
|
||||
import { logger } from '../../src/logger';
|
||||
import { logger } from "../../src/logger";
|
||||
import { IContent, IEvent, IEventRelation, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { ClientEvent, EventType, IPusher, MatrixClient, MsgType } from "../../src";
|
||||
import { SyncState } from "../../src/sync";
|
||||
@@ -13,9 +13,9 @@ import { eventMapperFor } from "../../src/event-mapper";
|
||||
/**
|
||||
* Return a promise that is resolved when the client next emits a
|
||||
* SYNCING event.
|
||||
* @param {Object} client The client
|
||||
* @param {Number=} count Number of syncs to wait for (default 1)
|
||||
* @return {Promise} Resolves once the client has emitted a SYNCING event
|
||||
* @param client - The client
|
||||
* @param count - Number of syncs to wait for (default 1)
|
||||
* @returns Promise which resolves once the client has emitted a SYNCING event
|
||||
*/
|
||||
export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
|
||||
if (count <= 0) {
|
||||
@@ -41,20 +41,21 @@ export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
|
||||
|
||||
/**
|
||||
* Create a spy for an object and automatically spy its methods.
|
||||
* @param {*} constr The class constructor (used with 'new')
|
||||
* @param {string} name The name of the class
|
||||
* @return {Object} An instantiated object with spied methods/properties.
|
||||
* @param constr - The class constructor (used with 'new')
|
||||
* @param name - The name of the class
|
||||
* @returns An instantiated object with spied methods/properties.
|
||||
*/
|
||||
export function mock<T>(constr: { new(...args: any[]): T }, name: string): T {
|
||||
export function mock<T>(constr: { new (...args: any[]): T }, name: string): T {
|
||||
// Based on http://eclipsesource.com/blogs/2014/03/27/mocks-in-jasmine-tests/
|
||||
const HelperConstr = new Function(); // jshint ignore:line
|
||||
HelperConstr.prototype = constr.prototype;
|
||||
// @ts-ignore
|
||||
const result = new HelperConstr();
|
||||
result.toString = function() {
|
||||
result.toString = function () {
|
||||
return "mock" + (name ? " of " + name : "");
|
||||
};
|
||||
for (const key of Object.getOwnPropertyNames(constr.prototype)) { // eslint-disable-line guard-for-in
|
||||
for (const key of Object.getOwnPropertyNames(constr.prototype)) {
|
||||
// eslint-disable-line guard-for-in
|
||||
try {
|
||||
if (constr.prototype[key] instanceof Function) {
|
||||
result[key] = jest.fn();
|
||||
@@ -84,15 +85,15 @@ interface IEventOpts {
|
||||
let testEventIndex = 1; // counter for events, easier for comparison of randomly generated events
|
||||
/**
|
||||
* Create an Event.
|
||||
* @param {Object} opts Values for the event.
|
||||
* @param {string} opts.type The event.type
|
||||
* @param {string} opts.room The event.room_id
|
||||
* @param {string} opts.sender The event.sender
|
||||
* @param {string} opts.skey Optional. The state key (auto inserts empty string)
|
||||
* @param {Object} opts.content The event.content
|
||||
* @param {boolean} opts.event True to make a MatrixEvent.
|
||||
* @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters.
|
||||
* @return {Object} a JSON object representing this event.
|
||||
* @param opts - Values for the event.
|
||||
* @param opts.type - The event.type
|
||||
* @param opts.room - The event.room_id
|
||||
* @param opts.sender - The event.sender
|
||||
* @param opts.skey - Optional. The state key (auto inserts empty string)
|
||||
* @param opts.content - The event.content
|
||||
* @param opts.event - True to make a MatrixEvent.
|
||||
* @param client - If passed along with opts.event=true will be used to set up re-emitters.
|
||||
* @returns a JSON object representing this event.
|
||||
*/
|
||||
export function mkEvent(opts: IEventOpts & { event: true }, client?: MatrixClient): MatrixEvent;
|
||||
export function mkEvent(opts: IEventOpts & { event?: false }, client?: MatrixClient): Partial<IEvent>;
|
||||
@@ -114,15 +115,17 @@ export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixC
|
||||
};
|
||||
if (opts.skey !== undefined) {
|
||||
event.state_key = opts.skey;
|
||||
} else if ([
|
||||
EventType.RoomName,
|
||||
EventType.RoomTopic,
|
||||
EventType.RoomCreate,
|
||||
EventType.RoomJoinRules,
|
||||
EventType.RoomPowerLevels,
|
||||
EventType.RoomTopic,
|
||||
"com.example.state",
|
||||
].includes(opts.type)) {
|
||||
} else if (
|
||||
[
|
||||
EventType.RoomName,
|
||||
EventType.RoomTopic,
|
||||
EventType.RoomCreate,
|
||||
EventType.RoomJoinRules,
|
||||
EventType.RoomPowerLevels,
|
||||
EventType.RoomTopic,
|
||||
"com.example.state",
|
||||
].includes(opts.type)
|
||||
) {
|
||||
event.state_key = "";
|
||||
}
|
||||
|
||||
@@ -160,8 +163,8 @@ interface IPresenceOpts {
|
||||
|
||||
/**
|
||||
* Create an m.presence event.
|
||||
* @param {Object} opts Values for the presence.
|
||||
* @return {Object|MatrixEvent} The event
|
||||
* @param opts - Values for the presence.
|
||||
* @returns The event
|
||||
*/
|
||||
export function mkPresence(opts: IPresenceOpts & { event: true }): MatrixEvent;
|
||||
export function mkPresence(opts: IPresenceOpts & { event?: false }): Partial<IEvent>;
|
||||
@@ -193,16 +196,16 @@ interface IMembershipOpts {
|
||||
|
||||
/**
|
||||
* Create an m.room.member event.
|
||||
* @param {Object} opts Values for the membership.
|
||||
* @param {string} opts.room The room ID for the event.
|
||||
* @param {string} opts.mship The content.membership for the event.
|
||||
* @param {string} opts.sender The sender user ID for the event.
|
||||
* @param {string} opts.skey The target user ID for the event if applicable
|
||||
* @param opts - Values for the membership.
|
||||
* @param opts.room - The room ID for the event.
|
||||
* @param opts.mship - The content.membership for the event.
|
||||
* @param opts.sender - The sender user ID for the event.
|
||||
* @param opts.skey - The target user ID for the event if applicable
|
||||
* e.g. for invites/bans.
|
||||
* @param {string} opts.name The content.displayname for the event.
|
||||
* @param {string} opts.url The content.avatar_url for the event.
|
||||
* @param {boolean} opts.event True to make a MatrixEvent.
|
||||
* @return {Object|MatrixEvent} The event
|
||||
* @param opts.name - The content.displayname for the event.
|
||||
* @param opts.url - The content.avatar_url for the event.
|
||||
* @param opts.event - True to make a MatrixEvent.
|
||||
* @returns The event
|
||||
*/
|
||||
export function mkMembership(opts: IMembershipOpts & { event: true }): MatrixEvent;
|
||||
export function mkMembership(opts: IMembershipOpts & { event?: false }): Partial<IEvent>;
|
||||
@@ -228,8 +231,8 @@ export function mkMembership(opts: IMembershipOpts & { event?: boolean }): Parti
|
||||
}
|
||||
|
||||
export function mkMembershipCustom<T>(
|
||||
base: T & { membership: string, sender: string, content?: IContent },
|
||||
): T & { type: EventType, sender: string, state_key: string, content: IContent } & GeneratedMetadata {
|
||||
base: T & { membership: string; sender: string; content?: IContent },
|
||||
): T & { type: EventType; sender: string; state_key: string; content: IContent } & GeneratedMetadata {
|
||||
const content = base.content || {};
|
||||
return mkEventCustom({
|
||||
...base,
|
||||
@@ -250,13 +253,13 @@ export interface IMessageOpts {
|
||||
|
||||
/**
|
||||
* Create an m.room.message event.
|
||||
* @param {Object} opts Values for the message
|
||||
* @param {string} opts.room The room ID for the event.
|
||||
* @param {string} opts.user The user ID for the event.
|
||||
* @param {string} opts.msg Optional. The content.body for the event.
|
||||
* @param {boolean} opts.event True to make a MatrixEvent.
|
||||
* @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters.
|
||||
* @return {Object|MatrixEvent} The event
|
||||
* @param opts - Values for the message
|
||||
* @param opts.room - The room ID for the event.
|
||||
* @param opts.user - The user ID for the event.
|
||||
* @param opts.msg - Optional. The content.body for the event.
|
||||
* @param opts.event - True to make a MatrixEvent.
|
||||
* @param client - If passed along with opts.event=true will be used to set up re-emitters.
|
||||
* @returns The event
|
||||
*/
|
||||
export function mkMessage(opts: IMessageOpts & { event: true }, client?: MatrixClient): MatrixEvent;
|
||||
export function mkMessage(opts: IMessageOpts & { event?: false }, client?: MatrixClient): Partial<IEvent>;
|
||||
@@ -290,14 +293,14 @@ interface IReplyMessageOpts extends IMessageOpts {
|
||||
/**
|
||||
* Create a reply message.
|
||||
*
|
||||
* @param {Object} opts Values for the message
|
||||
* @param {string} opts.room The room ID for the event.
|
||||
* @param {string} opts.user The user ID for the event.
|
||||
* @param {string} opts.msg Optional. The content.body for the event.
|
||||
* @param {MatrixEvent} opts.replyToMessage The replied message
|
||||
* @param {boolean} opts.event True to make a MatrixEvent.
|
||||
* @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters.
|
||||
* @return {Object|MatrixEvent} The event
|
||||
* @param opts - Values for the message
|
||||
* @param opts.room - The room ID for the event.
|
||||
* @param opts.user - The user ID for the event.
|
||||
* @param opts.msg - Optional. The content.body for the event.
|
||||
* @param opts.replyToMessage - The replied message
|
||||
* @param opts.event - True to make a MatrixEvent.
|
||||
* @param client - If passed along with opts.event=true will be used to set up re-emitters.
|
||||
* @returns The event
|
||||
*/
|
||||
export function mkReplyMessage(opts: IReplyMessageOpts & { event: true }, client?: MatrixClient): MatrixEvent;
|
||||
export function mkReplyMessage(opts: IReplyMessageOpts & { event?: false }, client?: MatrixClient): Partial<IEvent>;
|
||||
@@ -315,7 +318,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()!,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -329,10 +332,8 @@ export function mkReplyMessage(
|
||||
|
||||
/**
|
||||
* A mock implementation of webstorage
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
export class MockStorageApi {
|
||||
export class MockStorageApi implements Storage {
|
||||
private data: Record<string, any> = {};
|
||||
|
||||
public get length() {
|
||||
@@ -354,33 +355,43 @@ export class MockStorageApi {
|
||||
public removeItem(k: string): void {
|
||||
delete this.data[k];
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.data = {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If an event is being decrypted, wait for it to finish being decrypted.
|
||||
*
|
||||
* @param {MatrixEvent} event
|
||||
* @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
|
||||
* @returns promise which resolves (to `event`) when the event has been decrypted
|
||||
*/
|
||||
export async function awaitDecryption(event: MatrixEvent): Promise<MatrixEvent> {
|
||||
export async function awaitDecryption(
|
||||
event: MatrixEvent,
|
||||
{ waitOnDecryptionFailure = false } = {},
|
||||
): Promise<MatrixEvent> {
|
||||
// An event is not always decrypted ahead of time
|
||||
// getClearContent is a good signal to know whether an event has been decrypted
|
||||
// already
|
||||
if (event.getClearContent() !== null) {
|
||||
return event;
|
||||
if (waitOnDecryptionFailure && event.isDecryptionFailure()) {
|
||||
logger.log(`${Date.now()} event ${event.getId()} got decryption error; waiting`);
|
||||
} else {
|
||||
return event;
|
||||
}
|
||||
} else {
|
||||
logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
event.once(MatrixEventEvent.Decrypted, (ev) => {
|
||||
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
||||
resolve(ev);
|
||||
});
|
||||
});
|
||||
logger.log(`${Date.now()} event ${event.getId()} is not yet decrypted; waiting`);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
event.once(MatrixEventEvent.Decrypted, (ev) => {
|
||||
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
||||
resolve(ev);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise(r => e.once(k, r));
|
||||
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise((r) => e.once(k, r));
|
||||
|
||||
export const mkPusher = (extra: Partial<IPusher> = {}): IPusher => ({
|
||||
app_display_name: "app",
|
||||
@@ -392,3 +403,15 @@ export const mkPusher = (extra: Partial<IPusher> = {}): IPusher => ({
|
||||
pushkey: "pushpush",
|
||||
...extra,
|
||||
});
|
||||
|
||||
/**
|
||||
* a list of the supported crypto implementations, each with a callback to initialise that implementation
|
||||
* for the given client
|
||||
*/
|
||||
export const CRYPTO_BACKENDS: Record<string, InitCrypto> = {};
|
||||
export type InitCrypto = (_: MatrixClient) => Promise<void>;
|
||||
|
||||
CRYPTO_BACKENDS["rust-sdk"] = (client: MatrixClient) => client.initRustCrypto();
|
||||
if (global.Olm) {
|
||||
CRYPTO_BACKENDS["libolm"] = (client: MatrixClient) => client.initCrypto();
|
||||
}
|
||||
|
||||
+40
-31
@@ -21,18 +21,25 @@ 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,
|
||||
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"];
|
||||
@@ -50,12 +57,17 @@ type MakeThreadEventsProps = {
|
||||
};
|
||||
|
||||
export const makeThreadEvents = ({
|
||||
roomId, authorId, participantUserIds, length = 2, ts = 1, currentUserId,
|
||||
}: MakeThreadEventsProps): { rootEvent: MatrixEvent, events: MatrixEvent[] } => {
|
||||
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(),
|
||||
msg: "root event message " + Math.random(),
|
||||
ts,
|
||||
event: true,
|
||||
});
|
||||
@@ -67,16 +79,18 @@ export const makeThreadEvents = ({
|
||||
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,
|
||||
}));
|
||||
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({
|
||||
@@ -108,7 +122,7 @@ export const mkThread = ({
|
||||
participantUserIds,
|
||||
length = 2,
|
||||
ts = 1,
|
||||
}: MakeThreadProps): { thread: Thread, rootEvent: MatrixEvent, events: MatrixEvent[] } => {
|
||||
}: MakeThreadProps): { thread: Thread; rootEvent: MatrixEvent; events: MatrixEvent[] } => {
|
||||
const { rootEvent, events } = makeThreadEvents({
|
||||
roomId: room.roomId,
|
||||
authorId,
|
||||
@@ -120,15 +134,10 @@ export const mkThread = ({
|
||||
expect(rootEvent).toBeTruthy();
|
||||
|
||||
for (const evt of events) {
|
||||
room?.reEmitter.reEmit(evt, [
|
||||
MatrixEventEvent.BeforeRedaction,
|
||||
]);
|
||||
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 };
|
||||
};
|
||||
|
||||
+179
-80
@@ -26,6 +26,7 @@ import {
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
RoomMember,
|
||||
RoomState,
|
||||
RoomStateEvent,
|
||||
RoomStateEventHandlerMap,
|
||||
@@ -33,14 +34,14 @@ import {
|
||||
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 { CallEvent, CallEventHandlerMap, CallState, 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 = (
|
||||
export const DUMMY_SDP =
|
||||
"v=0\r\n" +
|
||||
"o=- 5022425983810148698 2 IN IP4 127.0.0.1\r\n" +
|
||||
"s=-\r\nt=0 0\r\na=group:BUNDLE 0\r\n" +
|
||||
@@ -77,24 +78,40 @@ export const DUMMY_SDP = (
|
||||
"a=rtpmap:112 telephone-event/32000\r\n" +
|
||||
"a=rtpmap:113 telephone-event/16000\r\n" +
|
||||
"a=rtpmap:126 telephone-event/8000\r\n" +
|
||||
"a=ssrc:3619738545 cname:2RWtmqhXLdoF4sOi\r\n"
|
||||
);
|
||||
"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";
|
||||
|
||||
export const FAKE_ROOM_ID = "!fake:test.dummy";
|
||||
export const FAKE_CONF_ID = "fakegroupcallid";
|
||||
|
||||
export const FAKE_USER_ID_1 = "@alice:test.dummy";
|
||||
export const FAKE_DEVICE_ID_1 = "@AAAAAA";
|
||||
export const FAKE_SESSION_ID_1 = "alice1";
|
||||
export const FAKE_USER_ID_2 = "@bob:test.dummy";
|
||||
export const FAKE_DEVICE_ID_2 = "@BBBBBB";
|
||||
export const FAKE_SESSION_ID_2 = "bob1";
|
||||
export const FAKE_USER_ID_3 = "@charlie:test.dummy";
|
||||
|
||||
class MockMediaStreamAudioSourceNode {
|
||||
public connect() {}
|
||||
}
|
||||
|
||||
class MockAnalyser {
|
||||
public getFloatFrequencyData() { return 0.0; }
|
||||
public getFloatFrequencyData() {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
export class MockAudioContext {
|
||||
constructor() {}
|
||||
public createAnalyser() { return new MockAnalyser(); }
|
||||
public createMediaStreamSource() { return new MockMediaStreamAudioSourceNode(); }
|
||||
public createAnalyser() {
|
||||
return new MockAnalyser();
|
||||
}
|
||||
public createMediaStreamSource() {
|
||||
return new MockMediaStreamAudioSourceNode();
|
||||
}
|
||||
public close() {}
|
||||
}
|
||||
|
||||
@@ -103,12 +120,14 @@ export class MockRTCPeerConnection {
|
||||
|
||||
private negotiationNeededListener?: () => void;
|
||||
public iceCandidateListener?: (e: RTCPeerConnectionIceEvent) => void;
|
||||
public iceConnectionStateChangeListener?: () => void;
|
||||
public onTrackListener?: (e: RTCTrackEvent) => void;
|
||||
public needsNegotiation = false;
|
||||
public readyToNegotiate: Promise<void>;
|
||||
private onReadyToNegotiate?: () => void;
|
||||
public localDescription: RTCSessionDescription;
|
||||
public signalingState: RTCSignalingState = "stable";
|
||||
public iceConnectionState: RTCIceConnectionState = "connected";
|
||||
public transceivers: MockRTCRtpTransceiver[] = [];
|
||||
|
||||
public static triggerAllNegotiations(): void {
|
||||
@@ -118,7 +137,7 @@ export class MockRTCPeerConnection {
|
||||
}
|
||||
|
||||
public static hasAnyPendingNegotiations(): boolean {
|
||||
return this.instances.some(i => i.needsNegotiation);
|
||||
return this.instances.some((i) => i.needsNegotiation);
|
||||
}
|
||||
|
||||
public static resetInstances() {
|
||||
@@ -128,11 +147,11 @@ export class MockRTCPeerConnection {
|
||||
constructor() {
|
||||
this.localDescription = {
|
||||
sdp: DUMMY_SDP,
|
||||
type: 'offer',
|
||||
toJSON: function() { },
|
||||
type: "offer",
|
||||
toJSON: function () {},
|
||||
};
|
||||
|
||||
this.readyToNegotiate = new Promise<void>(resolve => {
|
||||
this.readyToNegotiate = new Promise<void>((resolve) => {
|
||||
this.onReadyToNegotiate = resolve;
|
||||
});
|
||||
|
||||
@@ -140,24 +159,28 @@ export class MockRTCPeerConnection {
|
||||
}
|
||||
|
||||
public addEventListener(type: string, listener: () => void) {
|
||||
if (type === 'negotiationneeded') {
|
||||
if (type === "negotiationneeded") {
|
||||
this.negotiationNeededListener = listener;
|
||||
} else if (type == 'icecandidate') {
|
||||
} else if (type == "icecandidate") {
|
||||
this.iceCandidateListener = listener;
|
||||
} else if (type == 'track') {
|
||||
} else if (type === "iceconnectionstatechange") {
|
||||
this.iceConnectionStateChangeListener = listener;
|
||||
} else if (type == "track") {
|
||||
this.onTrackListener = listener;
|
||||
}
|
||||
}
|
||||
public createDataChannel(label: string, opts: RTCDataChannelInit) { return { label, ...opts }; }
|
||||
public createDataChannel(label: string, opts: RTCDataChannelInit) {
|
||||
return { label, ...opts };
|
||||
}
|
||||
public createOffer() {
|
||||
return Promise.resolve({
|
||||
type: 'offer',
|
||||
type: "offer",
|
||||
sdp: DUMMY_SDP,
|
||||
});
|
||||
}
|
||||
public createAnswer() {
|
||||
return Promise.resolve({
|
||||
type: 'answer',
|
||||
type: "answer",
|
||||
sdp: DUMMY_SDP,
|
||||
});
|
||||
}
|
||||
@@ -167,8 +190,10 @@ export class MockRTCPeerConnection {
|
||||
public setLocalDescription() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
public close() { }
|
||||
public getStats() { return []; }
|
||||
public close() {}
|
||||
public getStats() {
|
||||
return [];
|
||||
}
|
||||
public addTransceiver(track: MockMediaStreamTrack): MockRTCRtpTransceiver {
|
||||
this.needsNegotiation = true;
|
||||
if (this.onReadyToNegotiate) this.onReadyToNegotiate();
|
||||
@@ -193,9 +218,11 @@ export class MockRTCPeerConnection {
|
||||
if (this.onReadyToNegotiate) this.onReadyToNegotiate();
|
||||
}
|
||||
|
||||
public getTransceivers(): MockRTCRtpTransceiver[] { return this.transceivers; }
|
||||
public getTransceivers(): MockRTCRtpTransceiver[] {
|
||||
return this.transceivers;
|
||||
}
|
||||
public getSenders(): MockRTCRtpSender[] {
|
||||
return this.transceivers.map(t => t.sender as unknown as MockRTCRtpSender);
|
||||
return this.transceivers.map((t) => t.sender as unknown as MockRTCRtpSender);
|
||||
}
|
||||
|
||||
public doNegotiation() {
|
||||
@@ -207,13 +234,15 @@ export class MockRTCPeerConnection {
|
||||
}
|
||||
|
||||
export class MockRTCRtpSender {
|
||||
constructor(public track: MockMediaStreamTrack) { }
|
||||
constructor(public track: MockMediaStreamTrack) {}
|
||||
|
||||
public replaceTrack(track: MockMediaStreamTrack) { this.track = track; }
|
||||
public replaceTrack(track: MockMediaStreamTrack) {
|
||||
this.track = track;
|
||||
}
|
||||
}
|
||||
|
||||
export class MockRTCRtpReceiver {
|
||||
constructor(public track: MockMediaStreamTrack) { }
|
||||
constructor(public track: MockMediaStreamTrack) {}
|
||||
}
|
||||
|
||||
export class MockRTCRtpTransceiver {
|
||||
@@ -230,7 +259,7 @@ export class MockRTCRtpTransceiver {
|
||||
}
|
||||
|
||||
export class MockMediaStreamTrack {
|
||||
constructor(public readonly id: string, public readonly kind: "audio" | "video", public enabled = true) { }
|
||||
constructor(public readonly id: string, public readonly kind: "audio" | "video", public enabled = true) {}
|
||||
|
||||
public stop = jest.fn<void, []>();
|
||||
|
||||
@@ -238,7 +267,9 @@ export class MockMediaStreamTrack {
|
||||
public isStopped = false;
|
||||
public settings?: MediaTrackSettings;
|
||||
|
||||
public getSettings(): MediaTrackSettings { return this.settings!; }
|
||||
public getSettings(): MediaTrackSettings {
|
||||
return this.settings!;
|
||||
}
|
||||
|
||||
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
|
||||
// implementation
|
||||
@@ -257,16 +288,15 @@ export class MockMediaStreamTrack {
|
||||
});
|
||||
}
|
||||
|
||||
public typed(): MediaStreamTrack { return this as unknown as MediaStreamTrack; }
|
||||
public typed(): MediaStreamTrack {
|
||||
return this as unknown as MediaStreamTrack;
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
|
||||
// implementation
|
||||
export class MockMediaStream {
|
||||
constructor(
|
||||
public id: string,
|
||||
private tracks: MockMediaStreamTrack[] = [],
|
||||
) {}
|
||||
constructor(public id: string, private tracks: MockMediaStreamTrack[] = []) {}
|
||||
|
||||
public listeners: [string, (...args: any[]) => any][] = [];
|
||||
public isStopped = false;
|
||||
@@ -277,9 +307,15 @@ export class MockMediaStream {
|
||||
c();
|
||||
});
|
||||
}
|
||||
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 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]);
|
||||
}
|
||||
@@ -292,7 +328,9 @@ export class MockMediaStream {
|
||||
this.tracks.push(track);
|
||||
this.dispatchEvent("addtrack");
|
||||
}
|
||||
public 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();
|
||||
@@ -309,11 +347,11 @@ export class MockMediaStream {
|
||||
}
|
||||
|
||||
export class MockMediaDeviceInfo {
|
||||
constructor(
|
||||
public kind: "audioinput" | "videoinput" | "audiooutput",
|
||||
) { }
|
||||
constructor(public kind: "audioinput" | "videoinput" | "audiooutput") {}
|
||||
|
||||
public typed(): MediaDeviceInfo { return this as unknown as MediaDeviceInfo; }
|
||||
public typed(): MediaDeviceInfo {
|
||||
return this as unknown as MediaDeviceInfo;
|
||||
}
|
||||
}
|
||||
|
||||
export class MockMediaHandler {
|
||||
@@ -343,28 +381,38 @@ export class MockMediaHandler {
|
||||
public stopScreensharingStream(stream: MockMediaStream) {
|
||||
stream.isStopped = true;
|
||||
}
|
||||
public hasAudioDevice() { return true; }
|
||||
public hasVideoDevice() { return true; }
|
||||
public hasAudioDevice() {
|
||||
return true;
|
||||
}
|
||||
public hasVideoDevice() {
|
||||
return true;
|
||||
}
|
||||
public stopAllStreams() {}
|
||||
|
||||
public typed(): MediaHandler { return this as unknown as MediaHandler; }
|
||||
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 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 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 getDisplayMedia = jest
|
||||
.fn<Promise<MediaStream>, [MediaStreamConstraints]>()
|
||||
.mockReturnValue(Promise.resolve(new MockMediaStream("local_display_stream").typed()));
|
||||
|
||||
public typed(): MediaDevices { return this as unknown as MediaDevices; }
|
||||
public typed(): MediaDevices {
|
||||
return this as unknown as MediaDevices;
|
||||
}
|
||||
}
|
||||
|
||||
type EmittedEvents = CallEventHandlerEvent | CallEvent | ClientEvent | RoomStateEvent | GroupCallEventHandlerEvent;
|
||||
@@ -389,21 +437,33 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
|
||||
calls: new Map<string, MatrixCall>(),
|
||||
};
|
||||
|
||||
public sendStateEvent = jest.fn<Promise<ISendEventResponse>, [
|
||||
roomId: string, eventType: EventType, content: any, statekey: string,
|
||||
]>();
|
||||
public sendToDevice = jest.fn<Promise<{}>, [
|
||||
eventType: string,
|
||||
contentMap: { [userId: string]: { [deviceId: string]: Record<string, any> } },
|
||||
txnId?: string,
|
||||
]>();
|
||||
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 getMediaHandler(): MediaHandler {
|
||||
return this.mediaHandler.typed();
|
||||
}
|
||||
|
||||
public getUserId(): string { return this.userId; }
|
||||
public getUserId(): string {
|
||||
return this.userId;
|
||||
}
|
||||
|
||||
public getDeviceId(): string { return this.deviceId; }
|
||||
public getSessionId(): string { return this.sessionId; }
|
||||
public getDeviceId(): string {
|
||||
return this.deviceId;
|
||||
}
|
||||
public getSessionId(): string {
|
||||
return this.sessionId;
|
||||
}
|
||||
|
||||
public getTurnServers = () => [];
|
||||
public isFallbackICEServerAllowed = () => false;
|
||||
@@ -416,23 +476,58 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
|
||||
public getRooms = jest.fn<Room[], []>().mockReturnValue([]);
|
||||
public getRoom = jest.fn();
|
||||
|
||||
public typed(): MatrixClient { return this as unknown as MatrixClient; }
|
||||
public supportsExperimentalThreads(): boolean {
|
||||
return true;
|
||||
}
|
||||
public async decryptEventIfNeeded(): Promise<void> {}
|
||||
|
||||
public typed(): MatrixClient {
|
||||
return this as unknown as MatrixClient;
|
||||
}
|
||||
|
||||
public emitRoomState(event: MatrixEvent, state: RoomState): void {
|
||||
this.emit(
|
||||
RoomStateEvent.Events,
|
||||
event,
|
||||
state,
|
||||
null,
|
||||
);
|
||||
this.emit(RoomStateEvent.Events, event, state, null);
|
||||
}
|
||||
}
|
||||
|
||||
export class MockMatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap> {
|
||||
constructor(public roomId: string, public groupCallId?: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
public state = CallState.Ringing;
|
||||
public opponentUserId = FAKE_USER_ID_1;
|
||||
public opponentDeviceId = FAKE_DEVICE_ID_1;
|
||||
public opponentMember = { userId: this.opponentUserId };
|
||||
public callId = "1";
|
||||
public localUsermediaFeed = {
|
||||
setAudioVideoMuted: jest.fn<void, [boolean, boolean]>(),
|
||||
stream: new MockMediaStream("stream"),
|
||||
};
|
||||
public remoteUsermediaFeed?: CallFeed;
|
||||
public remoteScreensharingFeed?: CallFeed;
|
||||
|
||||
public reject = jest.fn<void, []>();
|
||||
public answerWithCallFeeds = jest.fn<void, [CallFeed[]]>();
|
||||
public hangup = jest.fn<void, []>();
|
||||
|
||||
public sendMetadataUpdate = jest.fn<void, []>();
|
||||
|
||||
public getOpponentMember(): Partial<RoomMember> {
|
||||
return this.opponentMember;
|
||||
}
|
||||
|
||||
public getOpponentDeviceId(): string | undefined {
|
||||
return this.opponentDeviceId;
|
||||
}
|
||||
|
||||
public typed(): MatrixCall {
|
||||
return this as unknown as MatrixCall;
|
||||
}
|
||||
}
|
||||
|
||||
export class MockCallFeed {
|
||||
constructor(
|
||||
public userId: string,
|
||||
public stream: MockMediaStream,
|
||||
) {}
|
||||
constructor(public userId: string, public deviceId: string | undefined, public stream: MockMediaStream) {}
|
||||
|
||||
public measureVolumeActivity(val: boolean) {}
|
||||
public dispose() {}
|
||||
@@ -479,10 +574,14 @@ export function installWebRTCMocks() {
|
||||
};
|
||||
}
|
||||
|
||||
export function makeMockGroupCallStateEvent(roomId: string, groupCallId: string, content: IContent = {
|
||||
"m.type": GroupCallType.Video,
|
||||
"m.intent": GroupCallIntent.Prompt,
|
||||
}): MatrixEvent {
|
||||
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),
|
||||
|
||||
@@ -27,16 +27,14 @@ class EventSource extends EventEmitter {
|
||||
}
|
||||
|
||||
doAnError() {
|
||||
this.emit('error');
|
||||
this.emit("error");
|
||||
}
|
||||
}
|
||||
|
||||
class EventTarget extends EventEmitter {
|
||||
class EventTarget extends EventEmitter {}
|
||||
|
||||
}
|
||||
|
||||
describe("ReEmitter", function() {
|
||||
it("Re-Emits events with the same args", function() {
|
||||
describe("ReEmitter", function () {
|
||||
it("Re-Emits events with the same args", function () {
|
||||
const src = new EventSource();
|
||||
const tgt = new EventTarget();
|
||||
|
||||
@@ -53,18 +51,18 @@ describe("ReEmitter", function() {
|
||||
expect(handler).toHaveBeenCalledWith("foo", "bar", src);
|
||||
});
|
||||
|
||||
it("Doesn't throw if no handler for 'error' event", function() {
|
||||
it("Doesn't throw if no handler for 'error' event", function () {
|
||||
const src = new EventSource();
|
||||
const tgt = new EventTarget();
|
||||
|
||||
const reEmitter = new ReEmitter(tgt);
|
||||
reEmitter.reEmit(src, ['error']);
|
||||
reEmitter.reEmit(src, ["error"]);
|
||||
|
||||
// without the workaround in ReEmitter, this would throw
|
||||
src.doAnError();
|
||||
|
||||
const handler = jest.fn();
|
||||
tgt.on('error', handler);
|
||||
tgt.on("error", handler);
|
||||
|
||||
src.doAnError();
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { ConnectionError } from "../../src/http-api/errors";
|
||||
import { ClientEvent, MatrixClient, Store } from "../../src/client";
|
||||
import { ToDeviceMessageQueue } from "../../src/ToDeviceMessageQueue";
|
||||
import { getMockClientWithEventEmitter } from "../test-utils/client";
|
||||
import { StubStore } from "../../src/store/stub";
|
||||
import { IndexedToDeviceBatch } from "../../src/models/ToDeviceMessage";
|
||||
import { SyncState } from "../../src/sync";
|
||||
|
||||
describe("onResumedSync", () => {
|
||||
let batch: IndexedToDeviceBatch | null;
|
||||
let shouldFailSendToDevice: Boolean;
|
||||
let onSendToDeviceFailure: () => void;
|
||||
let onSendToDeviceSuccess: () => void;
|
||||
let resumeSync: (newState: SyncState, oldState: SyncState) => void;
|
||||
|
||||
let store: Store;
|
||||
let mockClient: MatrixClient;
|
||||
let queue: ToDeviceMessageQueue;
|
||||
|
||||
beforeEach(() => {
|
||||
batch = {
|
||||
id: 0,
|
||||
txnId: "123",
|
||||
eventType: "m.dummy",
|
||||
batch: [],
|
||||
};
|
||||
|
||||
shouldFailSendToDevice = true;
|
||||
onSendToDeviceFailure = () => {};
|
||||
onSendToDeviceSuccess = () => {};
|
||||
resumeSync = (newState, oldState) => {
|
||||
shouldFailSendToDevice = false;
|
||||
mockClient.emit(ClientEvent.Sync, newState, oldState);
|
||||
};
|
||||
|
||||
store = new StubStore();
|
||||
store.getOldestToDeviceBatch = jest.fn().mockImplementation(() => {
|
||||
return batch;
|
||||
});
|
||||
store.removeToDeviceBatch = jest.fn().mockImplementation(() => {
|
||||
batch = null;
|
||||
});
|
||||
|
||||
mockClient = getMockClientWithEventEmitter({});
|
||||
mockClient.store = store;
|
||||
mockClient.sendToDevice = jest.fn().mockImplementation(async () => {
|
||||
if (shouldFailSendToDevice) {
|
||||
await Promise.reject(new ConnectionError("")).finally(() => {
|
||||
setTimeout(onSendToDeviceFailure, 0);
|
||||
});
|
||||
} else {
|
||||
await Promise.resolve({}).finally(() => {
|
||||
setTimeout(onSendToDeviceSuccess, 0);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
queue = new ToDeviceMessageQueue(mockClient);
|
||||
});
|
||||
|
||||
it("resends queue after connectivity restored", (done) => {
|
||||
onSendToDeviceFailure = () => {
|
||||
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
|
||||
expect(store.removeToDeviceBatch).not.toHaveBeenCalled();
|
||||
|
||||
resumeSync(SyncState.Syncing, SyncState.Catchup);
|
||||
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(2);
|
||||
};
|
||||
|
||||
onSendToDeviceSuccess = () => {
|
||||
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(3);
|
||||
expect(store.removeToDeviceBatch).toHaveBeenCalled();
|
||||
done();
|
||||
};
|
||||
|
||||
queue.start();
|
||||
});
|
||||
|
||||
it("does not resend queue if client sync still catching up", (done) => {
|
||||
onSendToDeviceFailure = () => {
|
||||
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
|
||||
expect(store.removeToDeviceBatch).not.toHaveBeenCalled();
|
||||
|
||||
resumeSync(SyncState.Catchup, SyncState.Catchup);
|
||||
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
|
||||
done();
|
||||
};
|
||||
|
||||
queue.start();
|
||||
});
|
||||
|
||||
it("does not resend queue if connectivity restored after queue stopped", (done) => {
|
||||
onSendToDeviceFailure = () => {
|
||||
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
|
||||
expect(store.removeToDeviceBatch).not.toHaveBeenCalled();
|
||||
|
||||
queue.stop();
|
||||
|
||||
resumeSync(SyncState.Syncing, SyncState.Catchup);
|
||||
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
|
||||
done();
|
||||
};
|
||||
|
||||
queue.start();
|
||||
});
|
||||
});
|
||||
+372
-325
@@ -19,44 +19,56 @@ import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import { AutoDiscovery } from "../../src/autodiscovery";
|
||||
|
||||
describe("AutoDiscovery", function() {
|
||||
describe("AutoDiscovery", function () {
|
||||
const getHttpBackend = (): MockHttpBackend => {
|
||||
const httpBackend = new MockHttpBackend();
|
||||
AutoDiscovery.setFetchFn(httpBackend.fetchFn as typeof global.fetch);
|
||||
return httpBackend;
|
||||
};
|
||||
|
||||
it("should throw an error when no domain is specified", function() {
|
||||
it("should throw an error when no domain is specified", function () {
|
||||
getHttpBackend();
|
||||
return Promise.all([
|
||||
// @ts-ignore testing no args
|
||||
AutoDiscovery.findClientConfig(/* no args */).then(() => {
|
||||
throw new Error("Expected a failure, not success with no args");
|
||||
}, () => {
|
||||
return true;
|
||||
}),
|
||||
AutoDiscovery.findClientConfig(/* no args */).then(
|
||||
() => {
|
||||
throw new Error("Expected a failure, not success with no args");
|
||||
},
|
||||
() => {
|
||||
return true;
|
||||
},
|
||||
),
|
||||
|
||||
AutoDiscovery.findClientConfig("").then(() => {
|
||||
throw new Error("Expected a failure, not success with an empty string");
|
||||
}, () => {
|
||||
return true;
|
||||
}),
|
||||
AutoDiscovery.findClientConfig("").then(
|
||||
() => {
|
||||
throw new Error("Expected a failure, not success with an empty string");
|
||||
},
|
||||
() => {
|
||||
return true;
|
||||
},
|
||||
),
|
||||
|
||||
AutoDiscovery.findClientConfig(null as any).then(() => {
|
||||
throw new Error("Expected a failure, not success with null");
|
||||
}, () => {
|
||||
return true;
|
||||
}),
|
||||
AutoDiscovery.findClientConfig(null as any).then(
|
||||
() => {
|
||||
throw new Error("Expected a failure, not success with null");
|
||||
},
|
||||
() => {
|
||||
return true;
|
||||
},
|
||||
),
|
||||
|
||||
AutoDiscovery.findClientConfig(true as any).then(() => {
|
||||
throw new Error("Expected a failure, not success with a non-string");
|
||||
}, () => {
|
||||
return true;
|
||||
}),
|
||||
AutoDiscovery.findClientConfig(true as any).then(
|
||||
() => {
|
||||
throw new Error("Expected a failure, not success with a non-string");
|
||||
},
|
||||
() => {
|
||||
return true;
|
||||
},
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return PROMPT when .well-known 404s", function() {
|
||||
it("should return PROMPT when .well-known 404s", function () {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(404, {});
|
||||
return Promise.all([
|
||||
@@ -80,7 +92,7 @@ describe("AutoDiscovery", function() {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT when .well-known returns a 500 error", function() {
|
||||
it("should return FAIL_PROMPT when .well-known returns a 500 error", function () {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(500, {});
|
||||
return Promise.all([
|
||||
@@ -104,7 +116,7 @@ describe("AutoDiscovery", function() {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT when .well-known returns a 400 error", function() {
|
||||
it("should return FAIL_PROMPT when .well-known returns a 400 error", function () {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(400, {});
|
||||
return Promise.all([
|
||||
@@ -128,7 +140,7 @@ describe("AutoDiscovery", function() {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT when .well-known returns an empty body", function() {
|
||||
it("should return FAIL_PROMPT when .well-known returns an empty body", function () {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "");
|
||||
return Promise.all([
|
||||
@@ -169,9 +181,7 @@ describe("AutoDiscovery", function() {
|
||||
};
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then(
|
||||
expect(expected).toEqual,
|
||||
),
|
||||
AutoDiscovery.findClientConfig("example.org").then(expect(expected).toEqual),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -257,106 +267,117 @@ describe("AutoDiscovery", function() {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
|
||||
"m.homeserver (verification failure: 404)", function() {
|
||||
it(
|
||||
"should return FAIL_ERROR when .well-known has an invalid base_url for " +
|
||||
"m.homeserver (verification failure: 404)",
|
||||
function () {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(404, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
"should return FAIL_ERROR when .well-known has an invalid base_url for " +
|
||||
"m.homeserver (verification failure: 500)",
|
||||
function () {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(500, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
"should return FAIL_ERROR when .well-known has an invalid base_url for " +
|
||||
"m.homeserver (verification failure: 200 but wrong content)",
|
||||
function () {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
||||
not_matrix_versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
it("should return SUCCESS when .well-known has a verifiably accurate base_url for " + "m.homeserver", function () {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(404, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
|
||||
"m.homeserver (verification failure: 500)", function() {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(500, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
|
||||
"m.homeserver (verification failure: 200 but wrong content)", function() {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
||||
not_matrix_versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return SUCCESS when .well-known has a verifiably accurate base_url for " +
|
||||
"m.homeserver", function() {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.path).toEqual("https://example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/_matrix/client/versions")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual("https://example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
base_url: "https://example.org",
|
||||
@@ -383,14 +404,16 @@ describe("AutoDiscovery", function() {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return SUCCESS with the right homeserver URL", function() {
|
||||
it("should return SUCCESS with the right homeserver URL", function () {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.path)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/_matrix/client/versions")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
@@ -418,185 +441,206 @@ describe("AutoDiscovery", function() {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return SUCCESS / FAIL_PROMPT when the identity server configuration " +
|
||||
"is wrong (missing base_url)", function() {
|
||||
it(
|
||||
"should return SUCCESS / FAIL_PROMPT when the identity server configuration " + "is wrong (missing base_url)",
|
||||
function () {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend
|
||||
.when("GET", "/_matrix/client/versions")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
"m.identity_server": {
|
||||
not_base_url: "https://identity.example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "SUCCESS",
|
||||
error: null,
|
||||
|
||||
// We still expect the base_url to be here for debugging purposes.
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
"should return SUCCESS / FAIL_PROMPT when the identity server configuration " + "is wrong (empty base_url)",
|
||||
function () {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend
|
||||
.when("GET", "/_matrix/client/versions")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
"m.identity_server": {
|
||||
base_url: "",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "SUCCESS",
|
||||
error: null,
|
||||
|
||||
// We still expect the base_url to be here for debugging purposes.
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
"should return SUCCESS / FAIL_PROMPT when the identity server configuration " +
|
||||
"is wrong (validation error: 404)",
|
||||
function () {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend
|
||||
.when("GET", "/_matrix/client/versions")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/_matrix/identity/v2").respond(404, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
"m.identity_server": {
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "SUCCESS",
|
||||
error: null,
|
||||
|
||||
// We still expect the base_url to be here for debugging purposes.
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
"should return SUCCESS / FAIL_PROMPT when the identity server configuration " +
|
||||
"is wrong (validation error: 500)",
|
||||
function () {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend
|
||||
.when("GET", "/_matrix/client/versions")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/_matrix/identity/v2").respond(500, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
"m.identity_server": {
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "SUCCESS",
|
||||
error: null,
|
||||
|
||||
// We still expect the base_url to be here for debugging purposes
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
it("should return SUCCESS when the identity server configuration is " + "verifiably accurate", function () {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.path)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
"m.identity_server": {
|
||||
not_base_url: "https://identity.example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "SUCCESS",
|
||||
error: null,
|
||||
|
||||
// We still expect the base_url to be here for debugging purposes.
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return SUCCESS / FAIL_PROMPT when the identity server configuration " +
|
||||
"is wrong (empty base_url)", function() {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.path)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
"m.identity_server": {
|
||||
base_url: "",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "SUCCESS",
|
||||
error: null,
|
||||
|
||||
// We still expect the base_url to be here for debugging purposes.
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return SUCCESS / FAIL_PROMPT when the identity server configuration " +
|
||||
"is wrong (validation error: 404)", function() {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.path)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/_matrix/identity/api/v1").respond(404, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
"m.identity_server": {
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "SUCCESS",
|
||||
error: null,
|
||||
|
||||
// We still expect the base_url to be here for debugging purposes.
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return SUCCESS / FAIL_PROMPT when the identity server configuration " +
|
||||
"is wrong (validation error: 500)", function() {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.path)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/_matrix/identity/api/v1").respond(500, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
"m.identity_server": {
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "SUCCESS",
|
||||
error: null,
|
||||
|
||||
// We still expect the base_url to be here for debugging purposes
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return SUCCESS when the identity server configuration is " +
|
||||
"verifiably accurate", function() {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.path)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/_matrix/identity/api/v1").check((req) => {
|
||||
expect(req.path)
|
||||
.toEqual("https://identity.example.org/_matrix/identity/api/v1");
|
||||
}).respond(200, {});
|
||||
httpBackend
|
||||
.when("GET", "/_matrix/client/versions")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/_matrix/identity/v2")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual("https://identity.example.org/_matrix/identity/v2");
|
||||
})
|
||||
.respond(200, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
@@ -627,19 +671,22 @@ describe("AutoDiscovery", function() {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return SUCCESS and preserve non-standard keys from the " +
|
||||
".well-known response", function() {
|
||||
it("should return SUCCESS and preserve non-standard keys from the " + ".well-known response", function () {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.path)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/_matrix/identity/api/v1").check((req) => {
|
||||
expect(req.path)
|
||||
.toEqual("https://identity.example.org/_matrix/identity/api/v1");
|
||||
}).respond(200, {});
|
||||
httpBackend
|
||||
.when("GET", "/_matrix/client/versions")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/_matrix/identity/v2")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual("https://identity.example.org/_matrix/identity/v2");
|
||||
})
|
||||
.respond(200, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
|
||||
+108
-103
@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { REFERENCE_RELATION } from "matrix-events-sdk";
|
||||
|
||||
import { LocationAssetType, M_ASSET, M_LOCATION, M_TIMESTAMP } from "../../src/@types/location";
|
||||
import { M_TOPIC } from "../../src/@types/topic";
|
||||
import {
|
||||
@@ -25,24 +23,20 @@ import {
|
||||
parseBeaconContent,
|
||||
parseTopicContent,
|
||||
} from "../../src/content-helpers";
|
||||
import { REFERENCE_RELATION } from "../../src/@types/extensible_events";
|
||||
|
||||
describe('Beacon content helpers', () => {
|
||||
describe('makeBeaconInfoContent()', () => {
|
||||
describe("Beacon content helpers", () => {
|
||||
describe("makeBeaconInfoContent()", () => {
|
||||
const mockDateNow = 123456789;
|
||||
beforeEach(() => {
|
||||
jest.spyOn(global.Date, 'now').mockReturnValue(mockDateNow);
|
||||
jest.spyOn(global.Date, "now").mockReturnValue(mockDateNow);
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.spyOn(global.Date, 'now').mockRestore();
|
||||
jest.spyOn(global.Date, "now").mockRestore();
|
||||
});
|
||||
it('create fully defined event content', () => {
|
||||
expect(makeBeaconInfoContent(
|
||||
1234,
|
||||
true,
|
||||
'nice beacon_info',
|
||||
LocationAssetType.Pin,
|
||||
)).toEqual({
|
||||
description: 'nice beacon_info',
|
||||
it("create fully defined event content", () => {
|
||||
expect(makeBeaconInfoContent(1234, true, "nice beacon_info", LocationAssetType.Pin)).toEqual({
|
||||
description: "nice beacon_info",
|
||||
timeout: 1234,
|
||||
live: true,
|
||||
[M_TIMESTAMP.name]: mockDateNow,
|
||||
@@ -52,78 +46,72 @@ describe('Beacon content helpers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults timestamp to current time', () => {
|
||||
expect(makeBeaconInfoContent(
|
||||
1234,
|
||||
true,
|
||||
'nice beacon_info',
|
||||
LocationAssetType.Pin,
|
||||
)).toEqual(expect.objectContaining({
|
||||
[M_TIMESTAMP.name]: mockDateNow,
|
||||
}));
|
||||
it("defaults timestamp to current time", () => {
|
||||
expect(makeBeaconInfoContent(1234, true, "nice beacon_info", LocationAssetType.Pin)).toEqual(
|
||||
expect.objectContaining({
|
||||
[M_TIMESTAMP.name]: mockDateNow,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('uses timestamp when provided', () => {
|
||||
expect(makeBeaconInfoContent(
|
||||
1234,
|
||||
true,
|
||||
'nice beacon_info',
|
||||
LocationAssetType.Pin,
|
||||
99999,
|
||||
)).toEqual(expect.objectContaining({
|
||||
[M_TIMESTAMP.name]: 99999,
|
||||
}));
|
||||
it("uses timestamp when provided", () => {
|
||||
expect(makeBeaconInfoContent(1234, true, "nice beacon_info", LocationAssetType.Pin, 99999)).toEqual(
|
||||
expect.objectContaining({
|
||||
[M_TIMESTAMP.name]: 99999,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('defaults asset type to self when not set', () => {
|
||||
expect(makeBeaconInfoContent(
|
||||
1234,
|
||||
true,
|
||||
'nice beacon_info',
|
||||
// no assetType passed
|
||||
)).toEqual(expect.objectContaining({
|
||||
[M_ASSET.name]: {
|
||||
type: LocationAssetType.Self,
|
||||
},
|
||||
}));
|
||||
it("defaults asset type to self when not set", () => {
|
||||
expect(
|
||||
makeBeaconInfoContent(
|
||||
1234,
|
||||
true,
|
||||
"nice beacon_info",
|
||||
// no assetType passed
|
||||
),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
[M_ASSET.name]: {
|
||||
type: LocationAssetType.Self,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeBeaconContent()', () => {
|
||||
it('creates event content without description', () => {
|
||||
expect(makeBeaconContent(
|
||||
'geo:foo',
|
||||
123,
|
||||
'$1234',
|
||||
// no description
|
||||
)).toEqual({
|
||||
describe("makeBeaconContent()", () => {
|
||||
it("creates event content without description", () => {
|
||||
expect(
|
||||
makeBeaconContent(
|
||||
"geo:foo",
|
||||
123,
|
||||
"$1234",
|
||||
// no description
|
||||
),
|
||||
).toEqual({
|
||||
[M_LOCATION.name]: {
|
||||
description: undefined,
|
||||
uri: 'geo:foo',
|
||||
uri: "geo:foo",
|
||||
},
|
||||
[M_TIMESTAMP.name]: 123,
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: '$1234',
|
||||
event_id: "$1234",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('creates event content with description', () => {
|
||||
expect(makeBeaconContent(
|
||||
'geo:foo',
|
||||
123,
|
||||
'$1234',
|
||||
'test description',
|
||||
)).toEqual({
|
||||
it("creates event content with description", () => {
|
||||
expect(makeBeaconContent("geo:foo", 123, "$1234", "test description")).toEqual({
|
||||
[M_LOCATION.name]: {
|
||||
description: 'test description',
|
||||
uri: 'geo:foo',
|
||||
description: "test description",
|
||||
uri: "geo:foo",
|
||||
},
|
||||
[M_TIMESTAMP.name]: 123,
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: '$1234',
|
||||
event_id: "$1234",
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -190,64 +178,81 @@ describe('Beacon content helpers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Topic content helpers', () => {
|
||||
describe('makeTopicContent()', () => {
|
||||
it('creates fully defined event content without html', () => {
|
||||
describe("Topic content helpers", () => {
|
||||
describe("makeTopicContent()", () => {
|
||||
it("creates fully defined event content without html", () => {
|
||||
expect(makeTopicContent("pizza")).toEqual({
|
||||
topic: "pizza",
|
||||
[M_TOPIC.name]: [{
|
||||
body: "pizza",
|
||||
mimetype: "text/plain",
|
||||
}],
|
||||
[M_TOPIC.name]: [
|
||||
{
|
||||
body: "pizza",
|
||||
mimetype: "text/plain",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('creates fully defined event content with html', () => {
|
||||
it("creates fully defined event content with html", () => {
|
||||
expect(makeTopicContent("pizza", "<b>pizza</b>")).toEqual({
|
||||
topic: "pizza",
|
||||
[M_TOPIC.name]: [{
|
||||
body: "pizza",
|
||||
mimetype: "text/plain",
|
||||
}, {
|
||||
body: "<b>pizza</b>",
|
||||
mimetype: "text/html",
|
||||
}],
|
||||
[M_TOPIC.name]: [
|
||||
{
|
||||
body: "pizza",
|
||||
mimetype: "text/plain",
|
||||
},
|
||||
{
|
||||
body: "<b>pizza</b>",
|
||||
mimetype: "text/html",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseTopicContent()', () => {
|
||||
it('parses event content with plain text topic without mimetype', () => {
|
||||
expect(parseTopicContent({
|
||||
topic: "pizza",
|
||||
[M_TOPIC.name]: [{
|
||||
body: "pizza",
|
||||
}],
|
||||
})).toEqual({
|
||||
describe("parseTopicContent()", () => {
|
||||
it("parses event content with plain text topic without mimetype", () => {
|
||||
expect(
|
||||
parseTopicContent({
|
||||
topic: "pizza",
|
||||
[M_TOPIC.name]: [
|
||||
{
|
||||
body: "pizza",
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual({
|
||||
text: "pizza",
|
||||
});
|
||||
});
|
||||
|
||||
it('parses event content with plain text topic', () => {
|
||||
expect(parseTopicContent({
|
||||
topic: "pizza",
|
||||
[M_TOPIC.name]: [{
|
||||
body: "pizza",
|
||||
mimetype: "text/plain",
|
||||
}],
|
||||
})).toEqual({
|
||||
it("parses event content with plain text topic", () => {
|
||||
expect(
|
||||
parseTopicContent({
|
||||
topic: "pizza",
|
||||
[M_TOPIC.name]: [
|
||||
{
|
||||
body: "pizza",
|
||||
mimetype: "text/plain",
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual({
|
||||
text: "pizza",
|
||||
});
|
||||
});
|
||||
|
||||
it('parses event content with html topic', () => {
|
||||
expect(parseTopicContent({
|
||||
topic: "pizza",
|
||||
[M_TOPIC.name]: [{
|
||||
body: "<b>pizza</b>",
|
||||
mimetype: "text/html",
|
||||
}],
|
||||
})).toEqual({
|
||||
it("parses event content with html topic", () => {
|
||||
expect(
|
||||
parseTopicContent({
|
||||
topic: "pizza",
|
||||
[M_TOPIC.name]: [
|
||||
{
|
||||
body: "<b>pizza</b>",
|
||||
mimetype: "text/html",
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual({
|
||||
text: "pizza",
|
||||
html: "<b>pizza</b>",
|
||||
});
|
||||
|
||||
@@ -16,60 +16,50 @@ limitations under the License.
|
||||
|
||||
import { getHttpUriForMxc } from "../../src/content-repo";
|
||||
|
||||
describe("ContentRepo", function() {
|
||||
describe("ContentRepo", function () {
|
||||
const baseUrl = "https://my.home.server";
|
||||
|
||||
describe("getHttpUriForMxc", function() {
|
||||
it("should do nothing to HTTP URLs when allowing direct links", function() {
|
||||
describe("getHttpUriForMxc", function () {
|
||||
it("should do nothing to HTTP URLs when allowing direct links", function () {
|
||||
const httpUrl = "http://example.com/image.jpeg";
|
||||
expect(
|
||||
getHttpUriForMxc(
|
||||
baseUrl, httpUrl, undefined, undefined, undefined, true,
|
||||
),
|
||||
).toEqual(httpUrl);
|
||||
expect(getHttpUriForMxc(baseUrl, httpUrl, undefined, undefined, undefined, true)).toEqual(httpUrl);
|
||||
});
|
||||
|
||||
it("should return the empty string HTTP URLs by default", function() {
|
||||
it("should return the empty string HTTP URLs by default", function () {
|
||||
const httpUrl = "http://example.com/image.jpeg";
|
||||
expect(getHttpUriForMxc(baseUrl, httpUrl)).toEqual("");
|
||||
});
|
||||
|
||||
it("should return a download URL if no width/height/resize are specified",
|
||||
function() {
|
||||
const mxcUri = "mxc://server.name/resourceid";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
|
||||
baseUrl + "/_matrix/media/r0/download/server.name/resourceid",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return the empty string for null input", function() {
|
||||
expect(getHttpUriForMxc(null as any, '')).toEqual("");
|
||||
it("should return a download URL if no width/height/resize are specified", function () {
|
||||
const mxcUri = "mxc://server.name/resourceid";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
|
||||
baseUrl + "/_matrix/media/r0/download/server.name/resourceid",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return a thumbnail URL if a width/height/resize is specified",
|
||||
function() {
|
||||
const mxcUri = "mxc://server.name/resourceid";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
|
||||
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
|
||||
"?width=32&height=64&method=crop",
|
||||
);
|
||||
});
|
||||
it("should return the empty string for null input", function () {
|
||||
expect(getHttpUriForMxc(null as any, "")).toEqual("");
|
||||
});
|
||||
|
||||
it("should put fragments from mxc:// URIs after any query parameters",
|
||||
function() {
|
||||
const mxcUri = "mxc://server.name/resourceid#automade";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
|
||||
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
|
||||
"?width=32#automade",
|
||||
);
|
||||
});
|
||||
it("should return a thumbnail URL if a width/height/resize is specified", function () {
|
||||
const mxcUri = "mxc://server.name/resourceid";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
|
||||
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" + "?width=32&height=64&method=crop",
|
||||
);
|
||||
});
|
||||
|
||||
it("should put fragments from mxc:// URIs at the end of the HTTP URI",
|
||||
function() {
|
||||
const mxcUri = "mxc://server.name/resourceid#automade";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
|
||||
baseUrl + "/_matrix/media/r0/download/server.name/resourceid#automade",
|
||||
);
|
||||
});
|
||||
it("should put fragments from mxc:// URIs after any query parameters", function () {
|
||||
const mxcUri = "mxc://server.name/resourceid#automade";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
|
||||
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" + "?width=32#automade",
|
||||
);
|
||||
});
|
||||
|
||||
it("should put fragments from mxc:// URIs at the end of the HTTP URI", function () {
|
||||
const mxcUri = "mxc://server.name/resourceid#automade";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
|
||||
baseUrl + "/_matrix/media/r0/download/server.name/resourceid#automade",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+401
-295
File diff suppressed because it is too large
Load Diff
@@ -14,28 +14,21 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import '../../olm-loader';
|
||||
import {
|
||||
CrossSigningInfo,
|
||||
createCryptoStoreCacheCallbacks,
|
||||
} from '../../../src/crypto/CrossSigning';
|
||||
import {
|
||||
IndexedDBCryptoStore,
|
||||
} from '../../../src/crypto/store/indexeddb-crypto-store';
|
||||
import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store';
|
||||
import 'fake-indexeddb/auto';
|
||||
import 'jest-localstorage-mock';
|
||||
import "../../olm-loader";
|
||||
import { CrossSigningInfo, createCryptoStoreCacheCallbacks } from "../../../src/crypto/CrossSigning";
|
||||
import { IndexedDBCryptoStore } from "../../../src/crypto/store/indexeddb-crypto-store";
|
||||
import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store";
|
||||
import "fake-indexeddb/auto";
|
||||
import "jest-localstorage-mock";
|
||||
import { OlmDevice } from "../../../src/crypto/OlmDevice";
|
||||
import { logger } from '../../../src/logger';
|
||||
import { logger } from "../../../src/logger";
|
||||
|
||||
const userId = "@alice:example.com";
|
||||
|
||||
// Private key for tests only
|
||||
const testKey = new Uint8Array([
|
||||
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82,
|
||||
0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef,
|
||||
0xae, 0xb1, 0x05, 0xc1, 0xe7, 0x62, 0x78, 0xa6,
|
||||
0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
|
||||
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82, 0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef, 0xae, 0xb1, 0x05,
|
||||
0xc1, 0xe7, 0x62, 0x78, 0xa6, 0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
|
||||
]);
|
||||
|
||||
const types = [
|
||||
@@ -50,13 +43,13 @@ badKey[0] ^= 1;
|
||||
|
||||
const masterKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
|
||||
|
||||
describe("CrossSigningInfo.getCrossSigningKey", function() {
|
||||
describe("CrossSigningInfo.getCrossSigningKey", function () {
|
||||
if (!global.Olm) {
|
||||
logger.warn('Not running megolm backup unit tests: libolm not present');
|
||||
logger.warn("Not running megolm backup unit tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
beforeAll(function () {
|
||||
return global.Olm.init();
|
||||
});
|
||||
|
||||
@@ -65,13 +58,12 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
|
||||
await expect(info.getCrossSigningKey("master")).rejects.toThrow();
|
||||
});
|
||||
|
||||
it.each(types)("should throw if the callback returns falsey",
|
||||
async ({ type, shouldCache }) => {
|
||||
const info = new CrossSigningInfo(userId, {
|
||||
getCrossSigningKey: async () => false as unknown as Uint8Array,
|
||||
});
|
||||
await expect(info.getCrossSigningKey(type)).rejects.toThrow("falsey");
|
||||
it.each(types)("should throw if the callback returns falsey", async ({ type, shouldCache }) => {
|
||||
const info = new CrossSigningInfo(userId, {
|
||||
getCrossSigningKey: async () => false as unknown as Uint8Array,
|
||||
});
|
||||
await expect(info.getCrossSigningKey(type)).rejects.toThrow("falsey");
|
||||
});
|
||||
|
||||
it("should throw if the expected key doesn't come back", async () => {
|
||||
const info = new CrossSigningInfo(userId, {
|
||||
@@ -96,63 +88,8 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
|
||||
}
|
||||
});
|
||||
|
||||
it.each(types)("should request a key from the cache callback (if set)" +
|
||||
" and does not call app if one is found" +
|
||||
" %o",
|
||||
async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockImplementation(() => {
|
||||
if (shouldCache) {
|
||||
return Promise.reject(new Error("Regular callback called"));
|
||||
} else {
|
||||
return Promise.resolve(testKey);
|
||||
}
|
||||
});
|
||||
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(testKey);
|
||||
const info = new CrossSigningInfo(
|
||||
userId,
|
||||
{ getCrossSigningKey },
|
||||
{ getCrossSigningKeyCache },
|
||||
);
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
|
||||
if (shouldCache) {
|
||||
expect(getCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
|
||||
}
|
||||
});
|
||||
|
||||
it.each(types)("should store a key with the cache callback (if set)",
|
||||
async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
|
||||
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
|
||||
const info = new CrossSigningInfo(
|
||||
userId,
|
||||
{ getCrossSigningKey },
|
||||
{ storeCrossSigningKeyCache },
|
||||
);
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(shouldCache ? 1 : 0);
|
||||
if (shouldCache) {
|
||||
expect(storeCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
|
||||
expect(storeCrossSigningKeyCache.mock.calls[0][1]).toBe(testKey);
|
||||
}
|
||||
});
|
||||
|
||||
it.each(types)("does not store a bad key to the cache",
|
||||
async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockResolvedValue(badKey);
|
||||
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
|
||||
const info = new CrossSigningInfo(
|
||||
userId,
|
||||
{ getCrossSigningKey },
|
||||
{ storeCrossSigningKeyCache },
|
||||
);
|
||||
await expect(info.getCrossSigningKey(type, masterKeyPub)).rejects.toThrow();
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(0);
|
||||
});
|
||||
|
||||
it.each(types)("does not store a value to the cache if it came from the cache",
|
||||
it.each(types)(
|
||||
"should request a key from the cache callback (if set)" + " and does not call app if one is found" + " %o",
|
||||
async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockImplementation(() => {
|
||||
if (shouldCache) {
|
||||
@@ -162,56 +99,98 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
|
||||
}
|
||||
});
|
||||
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(testKey);
|
||||
const storeCrossSigningKeyCache = jest.fn().mockRejectedValue(
|
||||
new Error("Tried to store a value from cache"),
|
||||
);
|
||||
const info = new CrossSigningInfo(userId, { getCrossSigningKey }, { getCrossSigningKeyCache });
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
|
||||
if (shouldCache) {
|
||||
expect(getCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each(types)("should store a key with the cache callback (if set)", async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
|
||||
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
|
||||
const info = new CrossSigningInfo(userId, { getCrossSigningKey }, { storeCrossSigningKeyCache });
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(shouldCache ? 1 : 0);
|
||||
if (shouldCache) {
|
||||
expect(storeCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
|
||||
expect(storeCrossSigningKeyCache.mock.calls[0][1]).toBe(testKey);
|
||||
}
|
||||
});
|
||||
|
||||
it.each(types)("does not store a bad key to the cache", async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockResolvedValue(badKey);
|
||||
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
|
||||
const info = new CrossSigningInfo(userId, { getCrossSigningKey }, { storeCrossSigningKeyCache });
|
||||
await expect(info.getCrossSigningKey(type, masterKeyPub)).rejects.toThrow();
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(0);
|
||||
});
|
||||
|
||||
it.each(types)("does not store a value to the cache if it came from the cache", async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockImplementation(() => {
|
||||
if (shouldCache) {
|
||||
return Promise.reject(new Error("Regular callback called"));
|
||||
} else {
|
||||
return Promise.resolve(testKey);
|
||||
}
|
||||
});
|
||||
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(testKey);
|
||||
const storeCrossSigningKeyCache = jest.fn().mockRejectedValue(new Error("Tried to store a value from cache"));
|
||||
const info = new CrossSigningInfo(
|
||||
userId,
|
||||
{ getCrossSigningKey },
|
||||
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
|
||||
);
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(0);
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
});
|
||||
|
||||
it.each(types)(
|
||||
"requests a key from the cache callback (if set) and then calls app" + " if one is not found",
|
||||
async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
|
||||
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
|
||||
const storeCrossSigningKeyCache = jest.fn();
|
||||
const info = new CrossSigningInfo(
|
||||
userId,
|
||||
{ getCrossSigningKey },
|
||||
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
|
||||
);
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(0);
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
});
|
||||
expect(getCrossSigningKey.mock.calls.length).toBe(1);
|
||||
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
|
||||
|
||||
it.each(types)("requests a key from the cache callback (if set) and then calls app" +
|
||||
" if one is not found", async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
|
||||
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
|
||||
const storeCrossSigningKeyCache = jest.fn();
|
||||
const info = new CrossSigningInfo(
|
||||
userId,
|
||||
{ getCrossSigningKey },
|
||||
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
|
||||
);
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
expect(getCrossSigningKey.mock.calls.length).toBe(1);
|
||||
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
|
||||
/* Also expect that the cache gets updated */
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
|
||||
},
|
||||
);
|
||||
|
||||
/* Also expect that the cache gets updated */
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
|
||||
});
|
||||
it.each(types)(
|
||||
"requests a key from the cache callback (if set) and then" + " calls app if that key doesn't match",
|
||||
async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
|
||||
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(badKey);
|
||||
const storeCrossSigningKeyCache = jest.fn();
|
||||
const info = new CrossSigningInfo(
|
||||
userId,
|
||||
{ getCrossSigningKey },
|
||||
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
|
||||
);
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
expect(getCrossSigningKey.mock.calls.length).toBe(1);
|
||||
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
|
||||
|
||||
it.each(types)("requests a key from the cache callback (if set) and then" +
|
||||
" calls app if that key doesn't match", async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
|
||||
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(badKey);
|
||||
const storeCrossSigningKeyCache = jest.fn();
|
||||
const info = new CrossSigningInfo(
|
||||
userId,
|
||||
{ getCrossSigningKey },
|
||||
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
|
||||
);
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
expect(getCrossSigningKey.mock.calls.length).toBe(1);
|
||||
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
|
||||
|
||||
/* Also expect that the cache gets updated */
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
|
||||
});
|
||||
/* Also expect that the cache gets updated */
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
@@ -219,20 +198,21 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
|
||||
* it's not possible to get one in normal execution unless you hack as we do here.
|
||||
*/
|
||||
describe.each([
|
||||
["IndexedDBCryptoStore",
|
||||
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
|
||||
["LocalStorageCryptoStore",
|
||||
() => new IndexedDBCryptoStore(undefined!, "tests")],
|
||||
["MemoryCryptoStore", () => {
|
||||
const store = new IndexedDBCryptoStore(undefined!, "tests");
|
||||
// @ts-ignore set private properties
|
||||
store._backend = new MemoryCryptoStore();
|
||||
// @ts-ignore
|
||||
store._backendPromise = Promise.resolve(store._backend);
|
||||
return store;
|
||||
}],
|
||||
])("CrossSigning > createCryptoStoreCacheCallbacks [%s]", function(name, dbFactory) {
|
||||
let store;
|
||||
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(global.indexedDB, "tests")],
|
||||
["LocalStorageCryptoStore", () => new IndexedDBCryptoStore(undefined!, "tests")],
|
||||
[
|
||||
"MemoryCryptoStore",
|
||||
() => {
|
||||
const store = new IndexedDBCryptoStore(undefined!, "tests");
|
||||
// @ts-ignore set private properties
|
||||
store._backend = new MemoryCryptoStore();
|
||||
// @ts-ignore
|
||||
store._backendPromise = Promise.resolve(store._backend);
|
||||
return store;
|
||||
},
|
||||
],
|
||||
])("CrossSigning > createCryptoStoreCacheCallbacks [%s]", function (name, dbFactory) {
|
||||
let store: IndexedDBCryptoStore;
|
||||
|
||||
beforeAll(() => {
|
||||
store = dbFactory();
|
||||
@@ -245,8 +225,10 @@ describe.each([
|
||||
it("should cache data to the store and retrieve it", async () => {
|
||||
await store.startup();
|
||||
const olmDevice = new OlmDevice(store);
|
||||
const { getCrossSigningKeyCache, storeCrossSigningKeyCache } =
|
||||
createCryptoStoreCacheCallbacks(store, olmDevice);
|
||||
const { getCrossSigningKeyCache, storeCrossSigningKeyCache } = createCryptoStoreCacheCallbacks(
|
||||
store,
|
||||
olmDevice,
|
||||
);
|
||||
await storeCrossSigningKeyCache!("self_signing", testKey);
|
||||
|
||||
// If we've not saved anything, don't expect anything
|
||||
|
||||
@@ -22,33 +22,29 @@ import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store
|
||||
import { DeviceList } from "../../../src/crypto/DeviceList";
|
||||
import { IDownloadKeyResult, MatrixClient } from "../../../src";
|
||||
import { OlmDevice } from "../../../src/crypto/OlmDevice";
|
||||
import { CryptoStore } from "../../../src/crypto/store/base";
|
||||
|
||||
const signedDeviceList: IDownloadKeyResult = {
|
||||
"failures": {},
|
||||
"device_keys": {
|
||||
failures: {},
|
||||
device_keys: {
|
||||
"@test1:sw1v.org": {
|
||||
"HGKAWHRVJQ": {
|
||||
"signatures": {
|
||||
HGKAWHRVJQ: {
|
||||
signatures: {
|
||||
"@test1:sw1v.org": {
|
||||
"ed25519:HGKAWHRVJQ":
|
||||
"8PB450fxKDn5s8IiRZ2N2t6MiueQYVRLHFEzqIi1eLdxx1w" +
|
||||
"XEPC1/1Uz9T4gwnKlMVAKkhB5hXQA/3kjaeLABw",
|
||||
},
|
||||
},
|
||||
"user_id": "@test1:sw1v.org",
|
||||
"keys": {
|
||||
"ed25519:HGKAWHRVJQ":
|
||||
"0gI/T6C+mn1pjtvnnW2yB2l1IIBb/5ULlBXi/LXFSEQ",
|
||||
"curve25519:HGKAWHRVJQ":
|
||||
"mbIZED1dBsgIgkgzxDpxKkJmsr4hiWlGzQTvUnQe3RY",
|
||||
user_id: "@test1:sw1v.org",
|
||||
keys: {
|
||||
"ed25519:HGKAWHRVJQ": "0gI/T6C+mn1pjtvnnW2yB2l1IIBb/5ULlBXi/LXFSEQ",
|
||||
"curve25519:HGKAWHRVJQ": "mbIZED1dBsgIgkgzxDpxKkJmsr4hiWlGzQTvUnQe3RY",
|
||||
},
|
||||
"algorithms": [
|
||||
"m.olm.v1.curve25519-aes-sha2",
|
||||
"m.megolm.v1.aes-sha2",
|
||||
],
|
||||
"device_id": "HGKAWHRVJQ",
|
||||
"unsigned": {
|
||||
"device_display_name": "",
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "HGKAWHRVJQ",
|
||||
unsigned: {
|
||||
device_display_name: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -56,50 +52,45 @@ const signedDeviceList: IDownloadKeyResult = {
|
||||
};
|
||||
|
||||
const signedDeviceList2: IDownloadKeyResult = {
|
||||
"failures": {},
|
||||
"device_keys": {
|
||||
failures: {},
|
||||
device_keys: {
|
||||
"@test2:sw1v.org": {
|
||||
"QJVRHWAKGH": {
|
||||
"signatures": {
|
||||
QJVRHWAKGH: {
|
||||
signatures: {
|
||||
"@test2:sw1v.org": {
|
||||
"ed25519:QJVRHWAKGH":
|
||||
"w1xxdLe1iIqzEFHLRVYQeuiM6t2N2ZRiI8s5nDKxf054BP8" +
|
||||
"1CPEX/AQXh5BhkKAVMlKnwg4T9zU1/wBALeajk3",
|
||||
},
|
||||
},
|
||||
"user_id": "@test2:sw1v.org",
|
||||
"keys": {
|
||||
"ed25519:QJVRHWAKGH":
|
||||
"Ig0/C6T+bBII1l2By2Wnnvtjp1nm/iXBlLU5/QESFXL",
|
||||
"curve25519:QJVRHWAKGH":
|
||||
"YR3eQnUvTQzGlWih4rsmJkKxpDxzgkgIgsBd1DEZIbm",
|
||||
user_id: "@test2:sw1v.org",
|
||||
keys: {
|
||||
"ed25519:QJVRHWAKGH": "Ig0/C6T+bBII1l2By2Wnnvtjp1nm/iXBlLU5/QESFXL",
|
||||
"curve25519:QJVRHWAKGH": "YR3eQnUvTQzGlWih4rsmJkKxpDxzgkgIgsBd1DEZIbm",
|
||||
},
|
||||
"algorithms": [
|
||||
"m.olm.v1.curve25519-aes-sha2",
|
||||
"m.megolm.v1.aes-sha2",
|
||||
],
|
||||
"device_id": "QJVRHWAKGH",
|
||||
"unsigned": {
|
||||
"device_display_name": "",
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "QJVRHWAKGH",
|
||||
unsigned: {
|
||||
device_display_name: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('DeviceList', function() {
|
||||
let downloadSpy;
|
||||
let cryptoStore;
|
||||
describe("DeviceList", function () {
|
||||
let downloadSpy: jest.Mock;
|
||||
let cryptoStore: CryptoStore;
|
||||
let deviceLists: DeviceList[] = [];
|
||||
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
deviceLists = [];
|
||||
|
||||
downloadSpy = jest.fn();
|
||||
cryptoStore = new MemoryCryptoStore();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
afterEach(function () {
|
||||
for (const dl of deviceLists) {
|
||||
dl.stop();
|
||||
}
|
||||
@@ -108,94 +99,96 @@ describe('DeviceList', function() {
|
||||
function createTestDeviceList(keyDownloadChunkSize = 250) {
|
||||
const baseApis = {
|
||||
downloadKeysForUsers: downloadSpy,
|
||||
getUserId: () => '@test1:sw1v.org',
|
||||
deviceId: 'HGKAWHRVJQ',
|
||||
getUserId: () => "@test1:sw1v.org",
|
||||
deviceId: "HGKAWHRVJQ",
|
||||
} as unknown as MatrixClient;
|
||||
const mockOlm = {
|
||||
verifySignature: function(key, message, signature) {},
|
||||
verifySignature: function (key: string, message: string, signature: string) {},
|
||||
} as unknown as OlmDevice;
|
||||
const dl = new DeviceList(baseApis, cryptoStore, mockOlm, keyDownloadChunkSize);
|
||||
deviceLists.push(dl);
|
||||
return dl;
|
||||
}
|
||||
|
||||
it("should successfully download and store device keys", function() {
|
||||
it("should successfully download and store device keys", function () {
|
||||
const dl = createTestDeviceList();
|
||||
|
||||
dl.startTrackingDeviceList('@test1:sw1v.org');
|
||||
dl.startTrackingDeviceList("@test1:sw1v.org");
|
||||
|
||||
const queryDefer1 = utils.defer<IDownloadKeyResult>();
|
||||
downloadSpy.mockReturnValue(queryDefer1.promise);
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
|
||||
expect(downloadSpy).toHaveBeenCalledWith(["@test1:sw1v.org"], {});
|
||||
queryDefer1.resolve(utils.deepCopy(signedDeviceList));
|
||||
|
||||
return prom1.then(() => {
|
||||
const storedKeys = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
|
||||
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
|
||||
const storedKeys = dl.getRawStoredDevicesForUser("@test1:sw1v.org");
|
||||
expect(Object.keys(storedKeys)).toEqual(["HGKAWHRVJQ"]);
|
||||
dl.stop();
|
||||
});
|
||||
});
|
||||
|
||||
it("should have an outdated devicelist on an invalidation while an " +
|
||||
"update is in progress", function() {
|
||||
it("should have an outdated devicelist on an invalidation while an " + "update is in progress", function () {
|
||||
const dl = createTestDeviceList();
|
||||
|
||||
dl.startTrackingDeviceList('@test1:sw1v.org');
|
||||
dl.startTrackingDeviceList("@test1:sw1v.org");
|
||||
|
||||
const queryDefer1 = utils.defer<IDownloadKeyResult>();
|
||||
downloadSpy.mockReturnValue(queryDefer1.promise);
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
|
||||
expect(downloadSpy).toHaveBeenCalledWith(["@test1:sw1v.org"], {});
|
||||
downloadSpy.mockReset();
|
||||
|
||||
// outdated notif arrives while the request is in flight.
|
||||
const queryDefer2 = utils.defer();
|
||||
downloadSpy.mockReturnValue(queryDefer2.promise);
|
||||
|
||||
dl.invalidateUserDeviceList('@test1:sw1v.org');
|
||||
dl.invalidateUserDeviceList("@test1:sw1v.org");
|
||||
dl.refreshOutdatedDeviceLists();
|
||||
|
||||
dl.saveIfDirty().then(() => {
|
||||
// the first request completes
|
||||
queryDefer1.resolve({
|
||||
failures: {},
|
||||
device_keys: {
|
||||
'@test1:sw1v.org': {},
|
||||
},
|
||||
dl.saveIfDirty()
|
||||
.then(() => {
|
||||
// the first request completes
|
||||
queryDefer1.resolve({
|
||||
failures: {},
|
||||
device_keys: {
|
||||
"@test1:sw1v.org": {},
|
||||
},
|
||||
});
|
||||
return prom1;
|
||||
})
|
||||
.then(() => {
|
||||
// uh-oh; user restarts before second request completes. The new instance
|
||||
// should know we never got a complete device list.
|
||||
logger.log("Creating new devicelist to simulate app reload");
|
||||
downloadSpy.mockReset();
|
||||
const dl2 = createTestDeviceList();
|
||||
const queryDefer3 = utils.defer<IDownloadKeyResult>();
|
||||
downloadSpy.mockReturnValue(queryDefer3.promise);
|
||||
|
||||
const prom3 = dl2.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toHaveBeenCalledWith(["@test1:sw1v.org"], {});
|
||||
dl2.stop();
|
||||
|
||||
queryDefer3.resolve(utils.deepCopy(signedDeviceList));
|
||||
|
||||
// allow promise chain to complete
|
||||
return prom3;
|
||||
})
|
||||
.then(() => {
|
||||
const storedKeys = dl.getRawStoredDevicesForUser("@test1:sw1v.org");
|
||||
expect(Object.keys(storedKeys)).toEqual(["HGKAWHRVJQ"]);
|
||||
dl.stop();
|
||||
});
|
||||
return prom1;
|
||||
}).then(() => {
|
||||
// uh-oh; user restarts before second request completes. The new instance
|
||||
// should know we never got a complete device list.
|
||||
logger.log("Creating new devicelist to simulate app reload");
|
||||
downloadSpy.mockReset();
|
||||
const dl2 = createTestDeviceList();
|
||||
const queryDefer3 = utils.defer<IDownloadKeyResult>();
|
||||
downloadSpy.mockReturnValue(queryDefer3.promise);
|
||||
|
||||
const prom3 = dl2.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
|
||||
dl2.stop();
|
||||
|
||||
queryDefer3.resolve(utils.deepCopy(signedDeviceList));
|
||||
|
||||
// allow promise chain to complete
|
||||
return prom3;
|
||||
}).then(() => {
|
||||
const storedKeys = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
|
||||
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
|
||||
dl.stop();
|
||||
});
|
||||
});
|
||||
|
||||
it("should download device keys in batches", function() {
|
||||
it("should download device keys in batches", function () {
|
||||
const dl = createTestDeviceList(1);
|
||||
|
||||
dl.startTrackingDeviceList('@test1:sw1v.org');
|
||||
dl.startTrackingDeviceList('@test2:sw1v.org');
|
||||
dl.startTrackingDeviceList("@test1:sw1v.org");
|
||||
dl.startTrackingDeviceList("@test2:sw1v.org");
|
||||
|
||||
const queryDefer1 = utils.defer<IDownloadKeyResult>();
|
||||
downloadSpy.mockReturnValueOnce(queryDefer1.promise);
|
||||
@@ -204,16 +197,16 @@ describe('DeviceList', function() {
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toBeCalledTimes(2);
|
||||
expect(downloadSpy).toHaveBeenNthCalledWith(1, ['@test1:sw1v.org'], {});
|
||||
expect(downloadSpy).toHaveBeenNthCalledWith(2, ['@test2:sw1v.org'], {});
|
||||
expect(downloadSpy).toHaveBeenNthCalledWith(1, ["@test1:sw1v.org"], {});
|
||||
expect(downloadSpy).toHaveBeenNthCalledWith(2, ["@test2:sw1v.org"], {});
|
||||
queryDefer1.resolve(utils.deepCopy(signedDeviceList));
|
||||
queryDefer2.resolve(utils.deepCopy(signedDeviceList2));
|
||||
|
||||
return prom1.then(() => {
|
||||
const storedKeys1 = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
|
||||
expect(Object.keys(storedKeys1)).toEqual(['HGKAWHRVJQ']);
|
||||
const storedKeys2 = dl.getRawStoredDevicesForUser('@test2:sw1v.org');
|
||||
expect(Object.keys(storedKeys2)).toEqual(['QJVRHWAKGH']);
|
||||
const storedKeys1 = dl.getRawStoredDevicesForUser("@test1:sw1v.org");
|
||||
expect(Object.keys(storedKeys1)).toEqual(["HGKAWHRVJQ"]);
|
||||
const storedKeys2 = dl.getRawStoredDevicesForUser("@test2:sw1v.org");
|
||||
expect(Object.keys(storedKeys2)).toEqual(["QJVRHWAKGH"]);
|
||||
dl.stop();
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,15 +15,15 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MockedObject } from 'jest-mock';
|
||||
import { MockedObject } from "jest-mock";
|
||||
|
||||
import '../../../olm-loader';
|
||||
import "../../../olm-loader";
|
||||
import { MemoryCryptoStore } from "../../../../src/crypto/store/memory-crypto-store";
|
||||
import { logger } from "../../../../src/logger";
|
||||
import { OlmDevice } from "../../../../src/crypto/OlmDevice";
|
||||
import * as olmlib from "../../../../src/crypto/olmlib";
|
||||
import { DeviceInfo } from "../../../../src/crypto/deviceinfo";
|
||||
import { MatrixClient } from '../../../../src';
|
||||
import { MatrixClient } from "../../../../src";
|
||||
|
||||
function makeOlmDevice() {
|
||||
const cryptoStore = new MemoryCryptoStore();
|
||||
@@ -31,73 +31,72 @@ function makeOlmDevice() {
|
||||
return olmDevice;
|
||||
}
|
||||
|
||||
async function setupSession(initiator, opponent) {
|
||||
async function setupSession(initiator: OlmDevice, opponent: OlmDevice) {
|
||||
await opponent.generateOneTimeKeys(1);
|
||||
const keys = await opponent.getOneTimeKeys();
|
||||
const firstKey = Object.values(keys['curve25519'])[0];
|
||||
const firstKey = Object.values(keys["curve25519"])[0];
|
||||
|
||||
const sid = await initiator.createOutboundSession(
|
||||
opponent.deviceCurve25519Key, firstKey,
|
||||
);
|
||||
const sid = await initiator.createOutboundSession(opponent.deviceCurve25519Key!, firstKey);
|
||||
return sid;
|
||||
}
|
||||
|
||||
describe("OlmDevice", function() {
|
||||
function alwaysSucceed<T>(promise: Promise<T>): Promise<T | void> {
|
||||
// swallow any exception thrown by a promise, so that
|
||||
// Promise.all doesn't abort
|
||||
return promise.catch(() => {});
|
||||
}
|
||||
|
||||
describe("OlmDevice", function () {
|
||||
if (!global.Olm) {
|
||||
logger.warn('Not running megolm unit tests: libolm not present');
|
||||
logger.warn("Not running megolm unit tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
beforeAll(function () {
|
||||
return global.Olm.init();
|
||||
});
|
||||
|
||||
let aliceOlmDevice: OlmDevice;
|
||||
let bobOlmDevice: OlmDevice;
|
||||
|
||||
beforeEach(async function() {
|
||||
beforeEach(async function () {
|
||||
aliceOlmDevice = makeOlmDevice();
|
||||
bobOlmDevice = makeOlmDevice();
|
||||
await aliceOlmDevice.init();
|
||||
await bobOlmDevice.init();
|
||||
});
|
||||
|
||||
describe('olm', function() {
|
||||
it("can decrypt messages", async function() {
|
||||
describe("olm", function () {
|
||||
it("can decrypt messages", async function () {
|
||||
const sid = await setupSession(aliceOlmDevice, bobOlmDevice);
|
||||
|
||||
const ciphertext = await aliceOlmDevice.encryptMessage(
|
||||
const ciphertext = (await aliceOlmDevice.encryptMessage(
|
||||
bobOlmDevice.deviceCurve25519Key!,
|
||||
sid,
|
||||
"The olm or proteus is an aquatic salamander in the family Proteidae",
|
||||
) as any; // OlmDevice.encryptMessage has incorrect return type
|
||||
)) as any; // OlmDevice.encryptMessage has incorrect return type
|
||||
|
||||
const result = await bobOlmDevice.createInboundSession(
|
||||
aliceOlmDevice.deviceCurve25519Key!,
|
||||
ciphertext.type,
|
||||
ciphertext.body,
|
||||
);
|
||||
expect(result.payload).toEqual(
|
||||
"The olm or proteus is an aquatic salamander in the family Proteidae",
|
||||
);
|
||||
expect(result.payload).toEqual("The olm or proteus is an aquatic salamander in the family Proteidae");
|
||||
});
|
||||
|
||||
it('exports picked account and olm sessions', async function() {
|
||||
it("exports picked account and olm sessions", async function () {
|
||||
const sessionId = await setupSession(aliceOlmDevice, bobOlmDevice);
|
||||
|
||||
const exported = await bobOlmDevice.export();
|
||||
// At this moment only Alice (the “initiator” in setupSession) has a session
|
||||
expect(exported.sessions).toEqual([]);
|
||||
|
||||
const MESSAGE = (
|
||||
"The olm or proteus is an aquatic salamander"
|
||||
+ " in the family Proteidae"
|
||||
);
|
||||
const ciphertext = await aliceOlmDevice.encryptMessage(
|
||||
const MESSAGE = "The olm or proteus is an aquatic salamander" + " in the family Proteidae";
|
||||
const ciphertext = (await aliceOlmDevice.encryptMessage(
|
||||
bobOlmDevice.deviceCurve25519Key!,
|
||||
sessionId,
|
||||
MESSAGE,
|
||||
) as any; // OlmDevice.encryptMessage has incorrect return type
|
||||
)) as any; // OlmDevice.encryptMessage has incorrect return type
|
||||
|
||||
const bobRecreatedOlmDevice = makeOlmDevice();
|
||||
bobRecreatedOlmDevice.init({ fromExportedDevice: exported });
|
||||
@@ -113,15 +112,12 @@ describe("OlmDevice", function() {
|
||||
// this time we expect Bob to have a session to export
|
||||
expect(exportedAgain.sessions).toHaveLength(1);
|
||||
|
||||
const MESSAGE_2 = (
|
||||
"In contrast to most amphibians,"
|
||||
+ " the olm is entirely aquatic"
|
||||
);
|
||||
const ciphertext2 = await aliceOlmDevice.encryptMessage(
|
||||
const MESSAGE_2 = "In contrast to most amphibians," + " the olm is entirely aquatic";
|
||||
const ciphertext2 = (await aliceOlmDevice.encryptMessage(
|
||||
bobOlmDevice.deviceCurve25519Key!,
|
||||
sessionId,
|
||||
MESSAGE_2,
|
||||
) as any; // OlmDevice.encryptMessage has incorrect return type
|
||||
)) as any; // OlmDevice.encryptMessage has incorrect return type
|
||||
|
||||
const bobRecreatedAgainOlmDevice = makeOlmDevice();
|
||||
bobRecreatedAgainOlmDevice.init({ fromExportedDevice: exportedAgain });
|
||||
@@ -136,7 +132,7 @@ describe("OlmDevice", function() {
|
||||
expect(decrypted2).toEqual(MESSAGE_2);
|
||||
});
|
||||
|
||||
it("creates only one session at a time", async function() {
|
||||
it("creates only one session at a time", async function () {
|
||||
// if we call ensureOlmSessionsForDevices multiple times, it should
|
||||
// only try to create one session at a time, even if the server is
|
||||
// slow
|
||||
@@ -152,27 +148,21 @@ describe("OlmDevice", function() {
|
||||
} as unknown as MockedObject<MatrixClient>;
|
||||
const devicesByUser = {
|
||||
"@bob:example.com": [
|
||||
DeviceInfo.fromStorage({
|
||||
keys: {
|
||||
"curve25519:ABCDEFG": "akey",
|
||||
DeviceInfo.fromStorage(
|
||||
{
|
||||
keys: {
|
||||
"curve25519:ABCDEFG": "akey",
|
||||
},
|
||||
},
|
||||
}, "ABCDEFG"),
|
||||
"ABCDEFG",
|
||||
),
|
||||
],
|
||||
};
|
||||
function alwaysSucceed(promise) {
|
||||
// swallow any exception thrown by a promise, so that
|
||||
// Promise.all doesn't abort
|
||||
return promise.catch(() => {});
|
||||
}
|
||||
|
||||
// start two tasks that try to ensure that there's an olm session
|
||||
const promises = Promise.all([
|
||||
alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
|
||||
aliceOlmDevice, baseApis, devicesByUser,
|
||||
)),
|
||||
alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
|
||||
aliceOlmDevice, baseApis, devicesByUser,
|
||||
)),
|
||||
alwaysSucceed(olmlib.ensureOlmSessionsForDevices(aliceOlmDevice, baseApis, devicesByUser)),
|
||||
alwaysSucceed(olmlib.ensureOlmSessionsForDevices(aliceOlmDevice, baseApis, devicesByUser)),
|
||||
]);
|
||||
|
||||
await new Promise((resolve) => {
|
||||
@@ -192,7 +182,7 @@ describe("OlmDevice", function() {
|
||||
expect(count).toBe(2);
|
||||
});
|
||||
|
||||
it("avoids deadlocks when two tasks are ensuring the same devices", async function() {
|
||||
it("avoids deadlocks when two tasks are ensuring the same devices", async function () {
|
||||
// This test checks whether `ensureOlmSessionsForDevices` properly
|
||||
// handles multiple tasks in flight ensuring some set of devices in
|
||||
// common without deadlocks.
|
||||
@@ -208,60 +198,47 @@ describe("OlmDevice", function() {
|
||||
},
|
||||
} as unknown as MockedObject<MatrixClient>;
|
||||
|
||||
const deviceBobA = DeviceInfo.fromStorage({
|
||||
keys: {
|
||||
"curve25519:BOB-A": "akey",
|
||||
const deviceBobA = DeviceInfo.fromStorage(
|
||||
{
|
||||
keys: {
|
||||
"curve25519:BOB-A": "akey",
|
||||
},
|
||||
},
|
||||
}, "BOB-A");
|
||||
const deviceBobB = DeviceInfo.fromStorage({
|
||||
keys: {
|
||||
"curve25519:BOB-B": "bkey",
|
||||
"BOB-A",
|
||||
);
|
||||
const deviceBobB = DeviceInfo.fromStorage(
|
||||
{
|
||||
keys: {
|
||||
"curve25519:BOB-B": "bkey",
|
||||
},
|
||||
},
|
||||
}, "BOB-B");
|
||||
"BOB-B",
|
||||
);
|
||||
|
||||
// There's no required ordering of devices per user, so here we
|
||||
// create two different orderings so that each task reserves a
|
||||
// device the other task needs before continuing.
|
||||
const devicesByUserAB = {
|
||||
"@bob:example.com": [
|
||||
deviceBobA,
|
||||
deviceBobB,
|
||||
],
|
||||
"@bob:example.com": [deviceBobA, deviceBobB],
|
||||
};
|
||||
const devicesByUserBA = {
|
||||
"@bob:example.com": [
|
||||
deviceBobB,
|
||||
deviceBobA,
|
||||
],
|
||||
"@bob:example.com": [deviceBobB, deviceBobA],
|
||||
};
|
||||
|
||||
function alwaysSucceed(promise) {
|
||||
// swallow any exception thrown by a promise, so that
|
||||
// Promise.all doesn't abort
|
||||
return promise.catch(() => {});
|
||||
}
|
||||
|
||||
const task1 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
|
||||
aliceOlmDevice, baseApis, devicesByUserAB,
|
||||
));
|
||||
const task1 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(aliceOlmDevice, baseApis, devicesByUserAB));
|
||||
|
||||
// After a single tick through the first task, it should have
|
||||
// claimed ownership of all devices to avoid deadlocking others.
|
||||
expect(Object.keys(aliceOlmDevice.sessionsInProgress).length).toBe(2);
|
||||
|
||||
const task2 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
|
||||
aliceOlmDevice, baseApis, devicesByUserBA,
|
||||
));
|
||||
const task2 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(aliceOlmDevice, baseApis, devicesByUserBA));
|
||||
|
||||
// The second task should not have changed the ownership count, as
|
||||
// it's waiting on the first task.
|
||||
expect(Object.keys(aliceOlmDevice.sessionsInProgress).length).toBe(2);
|
||||
|
||||
// Track the tasks, but don't await them yet.
|
||||
const promises = Promise.all([
|
||||
task1,
|
||||
task2,
|
||||
]);
|
||||
const promises = Promise.all([task1, task2]);
|
||||
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 200);
|
||||
|
||||
+282
-259
@@ -15,9 +15,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MockedObject } from "jest-mock";
|
||||
|
||||
import '../../olm-loader';
|
||||
import "../../olm-loader";
|
||||
import { logger } from "../../../src/logger";
|
||||
import * as olmlib from "../../../src/crypto/olmlib";
|
||||
import { MatrixClient } from "../../../src/client";
|
||||
@@ -30,27 +28,31 @@ import { Crypto } from "../../../src/crypto";
|
||||
import { resetCrossSigningKeys } from "./crypto-utils";
|
||||
import { BackupManager } from "../../../src/crypto/backup";
|
||||
import { StubStore } from "../../../src/store/stub";
|
||||
import { MatrixScheduler } from '../../../src';
|
||||
import { IndexedDBCryptoStore, MatrixScheduler } from "../../../src";
|
||||
import { CryptoStore } from "../../../src/crypto/store/base";
|
||||
import { MegolmDecryption as MegolmDecryptionClass } from "../../../src/crypto/algorithms/megolm";
|
||||
import { IKeyBackupInfo } from "../../../src/crypto/keybackup";
|
||||
|
||||
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';
|
||||
const ROOM_ID = "!ROOM:ID";
|
||||
|
||||
const SESSION_ID = 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc';
|
||||
const SESSION_ID = "o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc";
|
||||
const ENCRYPTED_EVENT = new MatrixEvent({
|
||||
type: 'm.room.encrypted',
|
||||
room_id: '!ROOM:ID',
|
||||
type: "m.room.encrypted",
|
||||
room_id: "!ROOM:ID",
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
sender_key: 'SENDER_CURVE25519',
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
sender_key: "SENDER_CURVE25519",
|
||||
session_id: SESSION_ID,
|
||||
ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N'
|
||||
+ 'CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl'
|
||||
+ 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs',
|
||||
ciphertext:
|
||||
"AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N" +
|
||||
"CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl" +
|
||||
"mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs",
|
||||
},
|
||||
event_id: '$event1',
|
||||
event_id: "$event1",
|
||||
origin_server_ts: 1507753886000,
|
||||
});
|
||||
|
||||
@@ -59,19 +61,20 @@ const CURVE25519_KEY_BACKUP_DATA = {
|
||||
forwarded_count: 0,
|
||||
is_verified: false,
|
||||
session_data: {
|
||||
ciphertext: '2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw'
|
||||
+ '6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ'
|
||||
+ 'Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9'
|
||||
+ 'SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy'
|
||||
+ 'Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF'
|
||||
+ 'ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV'
|
||||
+ '4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv'
|
||||
+ 'C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe'
|
||||
+ 'Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf'
|
||||
+ 'QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy'
|
||||
+ 'iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg',
|
||||
mac: '5lxYBHQU80M',
|
||||
ephemeral: '/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14',
|
||||
ciphertext:
|
||||
"2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw" +
|
||||
"6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ" +
|
||||
"Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9" +
|
||||
"SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy" +
|
||||
"Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF" +
|
||||
"ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV" +
|
||||
"4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv" +
|
||||
"C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe" +
|
||||
"Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf" +
|
||||
"QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy" +
|
||||
"iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg",
|
||||
mac: "5lxYBHQU80M",
|
||||
ephemeral: "/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -80,54 +83,60 @@ const AES256_KEY_BACKUP_DATA = {
|
||||
forwarded_count: 0,
|
||||
is_verified: false,
|
||||
session_data: {
|
||||
iv: 'b3Jqqvm5S9QdmXrzssspLQ',
|
||||
ciphertext: 'GOOASO3E9ThogkG0zMjEduGLM3u9jHZTkS7AvNNbNj3q1znwk4OlaVKXce'
|
||||
+ '7ynofiiYIiS865VlOqrKEEXv96XzRyUpgn68e3WsicwYl96EtjIEh/iY003PG2Qd'
|
||||
+ 'EluT899Ax7PydpUHxEktbWckMppYomUR5q8x1KI1SsOQIiJaIGThmIMPANRCFiK0'
|
||||
+ 'WQj+q+dnhzx4lt9AFqU5bKov8qKnw2qGYP7/+6RmJ0Kpvs8tG6lrcNDEHtFc2r0r'
|
||||
+ 'KKubDypo0Vc8EWSwsAHdKa36ewRavpreOuE8Z9RLfY0QIR1ecXrMqW0CdGFr7H3P'
|
||||
+ 'vcjF8sjwvQAavzxEKT1WMGizSMLeKWo2mgZ5cKnwV5HGUAw596JQvKs9laG2U89K'
|
||||
+ 'YrT0sH30vi62HKzcBLcDkWkUSNYPz7UiZ1MM0L380UA+1ZOXSOmtBA9xxzzbc8Xd'
|
||||
+ 'fRimVgklGdxrxjzuNLYhL2BvVH4oPWonD9j0bvRwE6XkimdbGQA8HB7UmXXjE8WA'
|
||||
+ 'RgaDHkfzoA3g3aeQ',
|
||||
mac: 'uR988UYgGL99jrvLLPX3V1ows+UYbktTmMxPAo2kxnU',
|
||||
iv: "b3Jqqvm5S9QdmXrzssspLQ",
|
||||
ciphertext:
|
||||
"GOOASO3E9ThogkG0zMjEduGLM3u9jHZTkS7AvNNbNj3q1znwk4OlaVKXce" +
|
||||
"7ynofiiYIiS865VlOqrKEEXv96XzRyUpgn68e3WsicwYl96EtjIEh/iY003PG2Qd" +
|
||||
"EluT899Ax7PydpUHxEktbWckMppYomUR5q8x1KI1SsOQIiJaIGThmIMPANRCFiK0" +
|
||||
"WQj+q+dnhzx4lt9AFqU5bKov8qKnw2qGYP7/+6RmJ0Kpvs8tG6lrcNDEHtFc2r0r" +
|
||||
"KKubDypo0Vc8EWSwsAHdKa36ewRavpreOuE8Z9RLfY0QIR1ecXrMqW0CdGFr7H3P" +
|
||||
"vcjF8sjwvQAavzxEKT1WMGizSMLeKWo2mgZ5cKnwV5HGUAw596JQvKs9laG2U89K" +
|
||||
"YrT0sH30vi62HKzcBLcDkWkUSNYPz7UiZ1MM0L380UA+1ZOXSOmtBA9xxzzbc8Xd" +
|
||||
"fRimVgklGdxrxjzuNLYhL2BvVH4oPWonD9j0bvRwE6XkimdbGQA8HB7UmXXjE8WA" +
|
||||
"RgaDHkfzoA3g3aeQ",
|
||||
mac: "uR988UYgGL99jrvLLPX3V1ows+UYbktTmMxPAo2kxnU",
|
||||
},
|
||||
};
|
||||
|
||||
const CURVE25519_BACKUP_INFO = {
|
||||
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
|
||||
version: '1',
|
||||
version: "1",
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
},
|
||||
};
|
||||
|
||||
const AES256_BACKUP_INFO = {
|
||||
const AES256_BACKUP_INFO: IKeyBackupInfo = {
|
||||
algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2",
|
||||
version: '1',
|
||||
auth_data: {
|
||||
// FIXME: add iv and mac
|
||||
},
|
||||
version: "1",
|
||||
auth_data: {} as IKeyBackupInfo["auth_data"],
|
||||
};
|
||||
|
||||
const keys = {};
|
||||
const keys: Record<string, Uint8Array> = {};
|
||||
|
||||
function getCrossSigningKey(type) {
|
||||
return keys[type];
|
||||
function getCrossSigningKey(type: string) {
|
||||
return Promise.resolve(keys[type]);
|
||||
}
|
||||
|
||||
function saveCrossSigningKeys(k) {
|
||||
function saveCrossSigningKeys(k: Record<string, Uint8Array>) {
|
||||
Object.assign(keys, k);
|
||||
}
|
||||
|
||||
function makeTestClient(cryptoStore) {
|
||||
const scheduler = [
|
||||
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
|
||||
"setProcessFunction",
|
||||
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}) as MockedObject<MatrixScheduler>;
|
||||
function makeTestScheduler(): MatrixScheduler {
|
||||
return (["getQueueForEvent", "queueEvent", "removeEventFromQueue", "setProcessFunction"] as const).reduce(
|
||||
(r, k) => {
|
||||
r[k] = jest.fn();
|
||||
return r;
|
||||
},
|
||||
{} as MatrixScheduler,
|
||||
);
|
||||
}
|
||||
|
||||
function makeTestClient(cryptoStore: CryptoStore) {
|
||||
const scheduler = makeTestScheduler();
|
||||
const store = new StubStore();
|
||||
|
||||
return new MatrixClient({
|
||||
const client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: "https://identity.server",
|
||||
accessToken: "my.access.token",
|
||||
@@ -139,80 +148,81 @@ function makeTestClient(cryptoStore) {
|
||||
cryptoStore: cryptoStore,
|
||||
cryptoCallbacks: { getCrossSigningKey, saveCrossSigningKeys },
|
||||
});
|
||||
|
||||
// initialising the crypto library will trigger a key upload request, which we can stub out
|
||||
client.uploadKeysRequest = jest.fn();
|
||||
return client;
|
||||
}
|
||||
|
||||
describe("MegolmBackup", function() {
|
||||
describe("MegolmBackup", function () {
|
||||
if (!global.Olm) {
|
||||
logger.warn('Not running megolm backup unit tests: libolm not present');
|
||||
logger.warn("Not running megolm backup unit tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
beforeAll(function () {
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
let olmDevice;
|
||||
let mockOlmLib;
|
||||
let mockCrypto;
|
||||
let cryptoStore;
|
||||
let megolmDecryption;
|
||||
beforeEach(async function() {
|
||||
mockCrypto = testUtils.mock(Crypto, 'Crypto');
|
||||
let olmDevice: OlmDevice;
|
||||
let mockOlmLib: typeof olmlib;
|
||||
let mockCrypto: Crypto;
|
||||
let cryptoStore: CryptoStore;
|
||||
let megolmDecryption: MegolmDecryptionClass;
|
||||
beforeEach(async function () {
|
||||
mockCrypto = testUtils.mock(Crypto, "Crypto");
|
||||
// @ts-ignore making mock
|
||||
mockCrypto.backupManager = testUtils.mock(BackupManager, "BackupManager");
|
||||
mockCrypto.backupKey = new Olm.PkEncryption();
|
||||
mockCrypto.backupKey.set_recipient_key(
|
||||
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
);
|
||||
mockCrypto.backupInfo = CURVE25519_BACKUP_INFO;
|
||||
mockCrypto.backupManager.backupInfo = CURVE25519_BACKUP_INFO;
|
||||
|
||||
cryptoStore = new MemoryCryptoStore();
|
||||
|
||||
olmDevice = new OlmDevice(cryptoStore);
|
||||
|
||||
// we stub out the olm encryption bits
|
||||
mockOlmLib = {};
|
||||
mockOlmLib = {} as unknown as typeof olmlib;
|
||||
mockOlmLib.ensureOlmSessionsForDevices = jest.fn();
|
||||
mockOlmLib.encryptMessageForDevice =
|
||||
jest.fn().mockResolvedValue(undefined);
|
||||
mockOlmLib.encryptMessageForDevice = jest.fn().mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
describe("backup", function() {
|
||||
let mockBaseApis;
|
||||
describe("backup", function () {
|
||||
let mockBaseApis: MatrixClient;
|
||||
|
||||
beforeEach(function() {
|
||||
mockBaseApis = {};
|
||||
beforeEach(function () {
|
||||
mockBaseApis = {} as unknown as MatrixClient;
|
||||
|
||||
megolmDecryption = new MegolmDecryption({
|
||||
userId: '@user:id',
|
||||
userId: "@user:id",
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: mockBaseApis,
|
||||
roomId: ROOM_ID,
|
||||
});
|
||||
}) as MegolmDecryptionClass;
|
||||
|
||||
// @ts-ignore private field access
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
|
||||
// clobber the setTimeout function to run 100x faster.
|
||||
// ideally we would use lolex, but we have no oportunity
|
||||
// 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);
|
||||
jest.spyOn(global, "setTimeout").mockImplementation(function (f, n) {
|
||||
return realSetTimeout(f!, n! / 100);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
jest.spyOn(global, 'setTimeout').mockRestore();
|
||||
afterEach(function () {
|
||||
jest.spyOn(global, "setTimeout").mockRestore();
|
||||
});
|
||||
|
||||
it('automatically calls the key back up', function() {
|
||||
it("automatically calls the key back up", function () {
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
// construct a fake decrypted key event via the use of a mocked
|
||||
// 'crypto' implementation.
|
||||
const event = new MatrixEvent({
|
||||
type: 'm.room.encrypted',
|
||||
type: "m.room.encrypted",
|
||||
});
|
||||
event.getWireType = () => "m.room.encrypted";
|
||||
event.getWireContent = () => {
|
||||
@@ -222,9 +232,9 @@ describe("MegolmBackup", function() {
|
||||
};
|
||||
const decryptedData = {
|
||||
clearEvent: {
|
||||
type: 'm.room_key',
|
||||
type: "m.room_key",
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: ROOM_ID,
|
||||
session_id: groupSession.session_id(),
|
||||
session_key: groupSession.session_key(),
|
||||
@@ -234,23 +244,27 @@ describe("MegolmBackup", function() {
|
||||
claimedEd25519Key: "SENDER_ED25519",
|
||||
};
|
||||
|
||||
mockCrypto.decryptEvent = function() {
|
||||
mockCrypto.decryptEvent = function () {
|
||||
return Promise.resolve(decryptedData);
|
||||
};
|
||||
mockCrypto.cancelRoomKeyRequest = function() {};
|
||||
mockCrypto.cancelRoomKeyRequest = function () {};
|
||||
|
||||
// @ts-ignore readonly field write
|
||||
mockCrypto.backupManager = {
|
||||
backupGroupSession: jest.fn(),
|
||||
};
|
||||
|
||||
return event.attemptDecryption(mockCrypto).then(() => {
|
||||
return megolmDecryption.onRoomKeyEvent(event);
|
||||
}).then(() => {
|
||||
expect(mockCrypto.backupManager.backupGroupSession).toHaveBeenCalled();
|
||||
});
|
||||
return event
|
||||
.attemptDecryption(mockCrypto)
|
||||
.then(() => {
|
||||
return megolmDecryption.onRoomKeyEvent(event);
|
||||
})
|
||||
.then(() => {
|
||||
expect(mockCrypto.backupManager.backupGroupSession).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('sends backups to the server (Curve25519 version)', function() {
|
||||
it("sends backups to the server (Curve25519 version)", function () {
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
const ibGroupSession = new Olm.InboundGroupSession();
|
||||
@@ -259,64 +273,62 @@ describe("MegolmBackup", function() {
|
||||
const client = makeTestClient(cryptoStore);
|
||||
|
||||
megolmDecryption = new MegolmDecryption({
|
||||
userId: '@user:id',
|
||||
userId: "@user:id",
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: client,
|
||||
roomId: ROOM_ID,
|
||||
});
|
||||
}) as MegolmDecryptionClass;
|
||||
|
||||
// @ts-ignore private field access
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
|
||||
return client.initCrypto()
|
||||
return client
|
||||
.initCrypto()
|
||||
.then(() => {
|
||||
return cryptoStore.doTxn(
|
||||
"readwrite",
|
||||
[cryptoStore.STORE_SESSION],
|
||||
(txn) => {
|
||||
cryptoStore.addEndToEndInboundGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
groupSession.session_id(),
|
||||
{
|
||||
forwardingCurve25519KeyChain: undefined,
|
||||
keysClaimed: {
|
||||
ed25519: "SENDER_ED25519",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
session: ibGroupSession.pickle(olmDevice.pickleKey),
|
||||
return cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => {
|
||||
cryptoStore.addEndToEndInboundGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
groupSession.session_id(),
|
||||
{
|
||||
forwardingCurve25519KeyChain: undefined!,
|
||||
keysClaimed: {
|
||||
ed25519: "SENDER_ED25519",
|
||||
},
|
||||
txn);
|
||||
});
|
||||
room_id: ROOM_ID,
|
||||
session: ibGroupSession.pickle(olmDevice.pickleKey),
|
||||
},
|
||||
txn,
|
||||
);
|
||||
});
|
||||
})
|
||||
.then(async () => {
|
||||
await client.enableKeyBackup({
|
||||
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
|
||||
version: '1',
|
||||
version: "1",
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
},
|
||||
});
|
||||
let numCalls = 0;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
client.http.authedRequest = function<T>(
|
||||
method, path, queryParams, data, opts,
|
||||
): Promise<T> {
|
||||
client.http.authedRequest = function (method, path, queryParams, data, opts): any {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqual(1);
|
||||
if (numCalls >= 2) {
|
||||
// exit out of retry loop if there's something wrong
|
||||
reject(new Error("authedRequest called too many timmes"));
|
||||
return Promise.resolve({} as T);
|
||||
return Promise.resolve({});
|
||||
}
|
||||
expect(method).toBe("PUT");
|
||||
expect(path).toBe("/room_keys/keys");
|
||||
expect(queryParams.version).toBe('1');
|
||||
expect(queryParams?.version).toBe("1");
|
||||
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
|
||||
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
|
||||
groupSession.session_id(),
|
||||
);
|
||||
resolve();
|
||||
return Promise.resolve({} as T);
|
||||
return Promise.resolve({});
|
||||
};
|
||||
client.crypto!.backupManager.backupGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
@@ -329,7 +341,7 @@ describe("MegolmBackup", function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('sends backups to the server (AES-256 version)', function() {
|
||||
it("sends backups to the server (AES-256 version)", function () {
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
const ibGroupSession = new Olm.InboundGroupSession();
|
||||
@@ -338,42 +350,42 @@ describe("MegolmBackup", function() {
|
||||
const client = makeTestClient(cryptoStore);
|
||||
|
||||
megolmDecryption = new MegolmDecryption({
|
||||
userId: '@user:id',
|
||||
userId: "@user:id",
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: client,
|
||||
roomId: ROOM_ID,
|
||||
});
|
||||
}) as MegolmDecryptionClass;
|
||||
|
||||
// @ts-ignore private field access
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
|
||||
return client.initCrypto()
|
||||
return client
|
||||
.initCrypto()
|
||||
.then(() => {
|
||||
return client.crypto!.storeSessionBackupPrivateKey(new Uint8Array(32));
|
||||
})
|
||||
.then(() => {
|
||||
return cryptoStore.doTxn(
|
||||
"readwrite",
|
||||
[cryptoStore.STORE_SESSION],
|
||||
(txn) => {
|
||||
cryptoStore.addEndToEndInboundGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
groupSession.session_id(),
|
||||
{
|
||||
forwardingCurve25519KeyChain: undefined,
|
||||
keysClaimed: {
|
||||
ed25519: "SENDER_ED25519",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
session: ibGroupSession.pickle(olmDevice.pickleKey),
|
||||
return cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => {
|
||||
cryptoStore.addEndToEndInboundGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
groupSession.session_id(),
|
||||
{
|
||||
forwardingCurve25519KeyChain: undefined!,
|
||||
keysClaimed: {
|
||||
ed25519: "SENDER_ED25519",
|
||||
},
|
||||
txn);
|
||||
});
|
||||
room_id: ROOM_ID,
|
||||
session: ibGroupSession.pickle(olmDevice.pickleKey),
|
||||
},
|
||||
txn,
|
||||
);
|
||||
});
|
||||
})
|
||||
.then(async () => {
|
||||
await client.enableKeyBackup({
|
||||
algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2",
|
||||
version: '1',
|
||||
version: "1",
|
||||
auth_data: {
|
||||
iv: "PsCAtR7gMc4xBd9YS3A9Ow",
|
||||
mac: "ZSDsTFEZK7QzlauCLMleUcX96GQZZM7UNtk4sripSqQ",
|
||||
@@ -381,25 +393,23 @@ describe("MegolmBackup", function() {
|
||||
});
|
||||
let numCalls = 0;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
client.http.authedRequest = function<T>(
|
||||
method, path, queryParams, data, opts,
|
||||
): Promise<T> {
|
||||
client.http.authedRequest = function (method, path, queryParams, data, opts): any {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqual(1);
|
||||
if (numCalls >= 2) {
|
||||
// exit out of retry loop if there's something wrong
|
||||
reject(new Error("authedRequest called too many timmes"));
|
||||
return Promise.resolve({} as T);
|
||||
return Promise.resolve({});
|
||||
}
|
||||
expect(method).toBe("PUT");
|
||||
expect(path).toBe("/room_keys/keys");
|
||||
expect(queryParams.version).toBe('1');
|
||||
expect(queryParams?.version).toBe("1");
|
||||
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
|
||||
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
|
||||
groupSession.session_id(),
|
||||
);
|
||||
resolve();
|
||||
return Promise.resolve({} as T);
|
||||
return Promise.resolve({});
|
||||
};
|
||||
client.crypto!.backupManager.backupGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
@@ -412,7 +422,7 @@ describe("MegolmBackup", function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('signs backups with the cross-signing master key', async function() {
|
||||
it("signs backups with the cross-signing master key", async function () {
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
const ibGroupSession = new Olm.InboundGroupSession();
|
||||
@@ -421,26 +431,29 @@ describe("MegolmBackup", function() {
|
||||
const client = makeTestClient(cryptoStore);
|
||||
|
||||
megolmDecryption = new MegolmDecryption({
|
||||
userId: '@user:id',
|
||||
userId: "@user:id",
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: client,
|
||||
roomId: ROOM_ID,
|
||||
});
|
||||
}) as MegolmDecryptionClass;
|
||||
|
||||
// @ts-ignore private field access
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
|
||||
await client.initCrypto();
|
||||
client.uploadDeviceSigningKeys = async function(e) {return {};};
|
||||
client.uploadKeySignatures = async function(e) {return { failures: {} };};
|
||||
client.uploadDeviceSigningKeys = async function (e) {
|
||||
return {};
|
||||
};
|
||||
client.uploadKeySignatures = async function (e) {
|
||||
return { failures: {} };
|
||||
};
|
||||
await resetCrossSigningKeys(client);
|
||||
let numCalls = 0;
|
||||
await Promise.all([
|
||||
new Promise<void>((resolve, reject) => {
|
||||
let backupInfo;
|
||||
client.http.authedRequest = function(
|
||||
method, path, queryParams, data, opts,
|
||||
) {
|
||||
let backupInfo: Record<string, any> | BodyInit | undefined;
|
||||
client.http.authedRequest = function (method, path, queryParams, data, opts): any {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqual(2);
|
||||
if (numCalls === 1) {
|
||||
@@ -449,7 +462,9 @@ 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);
|
||||
@@ -480,16 +495,13 @@ describe("MegolmBackup", function() {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it('retries when a backup fails', async function() {
|
||||
it("retries when a backup fails", async function () {
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
const ibGroupSession = new Olm.InboundGroupSession();
|
||||
ibGroupSession.create(groupSession.session_key());
|
||||
|
||||
const scheduler = [
|
||||
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
|
||||
"setProcessFunction",
|
||||
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}) as MockedObject<MatrixScheduler>;
|
||||
const scheduler = makeTestScheduler();
|
||||
const store = new StubStore();
|
||||
const client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
@@ -502,39 +514,40 @@ describe("MegolmBackup", function() {
|
||||
deviceId: "device",
|
||||
cryptoStore: cryptoStore,
|
||||
});
|
||||
// initialising the crypto library will trigger a key upload request, which we can stub out
|
||||
client.uploadKeysRequest = jest.fn();
|
||||
|
||||
megolmDecryption = new MegolmDecryption({
|
||||
userId: '@user:id',
|
||||
userId: "@user:id",
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: client,
|
||||
roomId: ROOM_ID,
|
||||
});
|
||||
}) as MegolmDecryptionClass;
|
||||
|
||||
// @ts-ignore private field access
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
|
||||
await client.initCrypto();
|
||||
await cryptoStore.doTxn(
|
||||
"readwrite",
|
||||
[cryptoStore.STORE_SESSION],
|
||||
(txn) => {
|
||||
cryptoStore.addEndToEndInboundGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
groupSession.session_id(),
|
||||
{
|
||||
forwardingCurve25519KeyChain: undefined,
|
||||
keysClaimed: {
|
||||
ed25519: "SENDER_ED25519",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
session: ibGroupSession.pickle(olmDevice.pickleKey),
|
||||
await cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => {
|
||||
cryptoStore.addEndToEndInboundGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
groupSession.session_id(),
|
||||
{
|
||||
forwardingCurve25519KeyChain: undefined!,
|
||||
keysClaimed: {
|
||||
ed25519: "SENDER_ED25519",
|
||||
},
|
||||
txn);
|
||||
});
|
||||
room_id: ROOM_ID,
|
||||
session: ibGroupSession.pickle(olmDevice.pickleKey),
|
||||
},
|
||||
txn,
|
||||
);
|
||||
});
|
||||
|
||||
await client.enableKeyBackup({
|
||||
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
|
||||
version: '1',
|
||||
version: "1",
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
},
|
||||
@@ -542,30 +555,26 @@ describe("MegolmBackup", function() {
|
||||
let numCalls = 0;
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
client.http.authedRequest = function<T>(
|
||||
method, path, queryParams, data, opts,
|
||||
): Promise<T> {
|
||||
client.http.authedRequest = function (method, path, queryParams, data, opts): any {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqual(2);
|
||||
if (numCalls >= 3) {
|
||||
// exit out of retry loop if there's something wrong
|
||||
reject(new Error("authedRequest called too many timmes"));
|
||||
return Promise.resolve({} as T);
|
||||
return Promise.resolve({});
|
||||
}
|
||||
expect(method).toBe("PUT");
|
||||
expect(path).toBe("/room_keys/keys");
|
||||
expect(queryParams.version).toBe('1');
|
||||
expect(queryParams?.version).toBe("1");
|
||||
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
|
||||
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
|
||||
groupSession.session_id(),
|
||||
);
|
||||
if (numCalls > 1) {
|
||||
resolve();
|
||||
return Promise.resolve({} as T);
|
||||
return Promise.resolve({});
|
||||
} else {
|
||||
return Promise.reject(
|
||||
new Error("this is an expected failure"),
|
||||
);
|
||||
return Promise.reject(new Error("this is an expected failure"));
|
||||
}
|
||||
};
|
||||
return client.crypto!.backupManager.backupGroupSession(
|
||||
@@ -578,66 +587,73 @@ describe("MegolmBackup", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("restore", function() {
|
||||
let client;
|
||||
describe("restore", function () {
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
client = makeTestClient(cryptoStore);
|
||||
|
||||
megolmDecryption = new MegolmDecryption({
|
||||
userId: '@user:id',
|
||||
userId: "@user:id",
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: client,
|
||||
roomId: ROOM_ID,
|
||||
});
|
||||
}) as MegolmDecryptionClass;
|
||||
|
||||
// @ts-ignore private field access
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
|
||||
return client.initCrypto();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
afterEach(function () {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it('can restore from backup (Curve25519 version)', function() {
|
||||
client.http.authedRequest = function() {
|
||||
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
|
||||
it("can restore from backup (Curve25519 version)", function () {
|
||||
client.http.authedRequest = function () {
|
||||
return Promise.resolve<any>(CURVE25519_KEY_BACKUP_DATA);
|
||||
};
|
||||
return client.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
ROOM_ID,
|
||||
SESSION_ID,
|
||||
CURVE25519_BACKUP_INFO,
|
||||
).then(() => {
|
||||
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
|
||||
}).then((res) => {
|
||||
expect(res.clearEvent.content).toEqual('testytest');
|
||||
expect(res.untrusted).toBeTruthy(); // keys from Curve25519 backup are untrusted
|
||||
});
|
||||
return client
|
||||
.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
ROOM_ID,
|
||||
SESSION_ID,
|
||||
CURVE25519_BACKUP_INFO,
|
||||
)
|
||||
.then(() => {
|
||||
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
|
||||
})
|
||||
.then((res) => {
|
||||
expect(res.clearEvent.content).toEqual("testytest");
|
||||
expect(res.untrusted).toBeTruthy(); // keys from Curve25519 backup are untrusted
|
||||
});
|
||||
});
|
||||
|
||||
it('can restore from backup (AES-256 version)', function() {
|
||||
client.http.authedRequest = function() {
|
||||
return Promise.resolve(AES256_KEY_BACKUP_DATA);
|
||||
it("can restore from backup (AES-256 version)", function () {
|
||||
client.http.authedRequest = function () {
|
||||
return Promise.resolve<any>(AES256_KEY_BACKUP_DATA);
|
||||
};
|
||||
return client.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
ROOM_ID,
|
||||
SESSION_ID,
|
||||
AES256_BACKUP_INFO,
|
||||
).then(() => {
|
||||
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
|
||||
}).then((res) => {
|
||||
expect(res.clearEvent.content).toEqual('testytest');
|
||||
expect(res.untrusted).toBeFalsy(); // keys from AES backup are trusted
|
||||
});
|
||||
return client
|
||||
.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
ROOM_ID,
|
||||
SESSION_ID,
|
||||
AES256_BACKUP_INFO,
|
||||
)
|
||||
.then(() => {
|
||||
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
|
||||
})
|
||||
.then((res) => {
|
||||
expect(res.clearEvent.content).toEqual("testytest");
|
||||
expect(res.untrusted).toBeFalsy(); // keys from AES backup are trusted
|
||||
});
|
||||
});
|
||||
|
||||
it('can restore backup by room (Curve25519 version)', function() {
|
||||
client.http.authedRequest = function() {
|
||||
return Promise.resolve({
|
||||
it("can restore backup by room (Curve25519 version)", function () {
|
||||
client.http.authedRequest = function () {
|
||||
return Promise.resolve<any>({
|
||||
rooms: {
|
||||
[ROOM_ID]: {
|
||||
sessions: {
|
||||
@@ -647,30 +663,35 @@ describe("MegolmBackup", function() {
|
||||
},
|
||||
});
|
||||
};
|
||||
return client.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
null, null, CURVE25519_BACKUP_INFO,
|
||||
).then(() => {
|
||||
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
|
||||
}).then((res) => {
|
||||
expect(res.clearEvent.content).toEqual('testytest');
|
||||
});
|
||||
return client
|
||||
.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
null!,
|
||||
null!,
|
||||
CURVE25519_BACKUP_INFO,
|
||||
)
|
||||
.then(() => {
|
||||
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
|
||||
})
|
||||
.then((res) => {
|
||||
expect(res.clearEvent.content).toEqual("testytest");
|
||||
});
|
||||
});
|
||||
|
||||
it('has working cache functions', async function() {
|
||||
it("has working cache functions", async function () {
|
||||
const key = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
await client.crypto.storeSessionBackupPrivateKey(key);
|
||||
const result = await client.crypto.getSessionBackupPrivateKey();
|
||||
expect(new Uint8Array(result)).toEqual(key);
|
||||
await client.crypto!.storeSessionBackupPrivateKey(key);
|
||||
const result = await client.crypto!.getSessionBackupPrivateKey();
|
||||
expect(new Uint8Array(result!)).toEqual(key);
|
||||
});
|
||||
|
||||
it('caches session backup keys as it encounters them', async function() {
|
||||
const cachedNull = await client.crypto.getSessionBackupPrivateKey();
|
||||
it("caches session backup keys as it encounters them", async function () {
|
||||
const cachedNull = await client.crypto!.getSessionBackupPrivateKey();
|
||||
expect(cachedNull).toBeNull();
|
||||
client.http.authedRequest = function() {
|
||||
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
|
||||
client.http.authedRequest = function () {
|
||||
return Promise.resolve<any>(CURVE25519_KEY_BACKUP_DATA);
|
||||
};
|
||||
await new Promise((resolve) => {
|
||||
await new Promise<void>((resolve) => {
|
||||
client.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
ROOM_ID,
|
||||
@@ -679,33 +700,32 @@ describe("MegolmBackup", function() {
|
||||
{ cacheCompleteCallback: resolve },
|
||||
);
|
||||
});
|
||||
const cachedKey = await client.crypto.getSessionBackupPrivateKey();
|
||||
const cachedKey = await client.crypto!.getSessionBackupPrivateKey();
|
||||
expect(cachedKey).not.toBeNull();
|
||||
});
|
||||
|
||||
it("fails if an known algorithm is used", async function() {
|
||||
it("fails if an known algorithm is used", async function () {
|
||||
const BAD_BACKUP_INFO = Object.assign({}, CURVE25519_BACKUP_INFO, {
|
||||
algorithm: "this.algorithm.does.not.exist",
|
||||
});
|
||||
client.http.authedRequest = function() {
|
||||
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
|
||||
client.http.authedRequest = function () {
|
||||
return Promise.resolve<any>(CURVE25519_KEY_BACKUP_DATA);
|
||||
};
|
||||
|
||||
await expect(client.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
ROOM_ID,
|
||||
SESSION_ID,
|
||||
BAD_BACKUP_INFO,
|
||||
)).rejects.toThrow();
|
||||
await expect(
|
||||
client.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
ROOM_ID,
|
||||
SESSION_ID,
|
||||
BAD_BACKUP_INFO,
|
||||
),
|
||||
).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 scheduler = makeTestScheduler();
|
||||
const store = new StubStore();
|
||||
const client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
@@ -718,6 +738,9 @@ describe("MegolmBackup", function() {
|
||||
deviceId: "device",
|
||||
cryptoStore,
|
||||
});
|
||||
// initialising the crypto library will trigger a key upload request, which we can stub out
|
||||
client.uploadKeysRequest = jest.fn();
|
||||
|
||||
await client.initCrypto();
|
||||
|
||||
cryptoStore.countSessionsNeedingBackup = jest.fn().mockReturnValue(6);
|
||||
|
||||
@@ -15,26 +15,27 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import '../../olm-loader';
|
||||
import anotherjson from 'another-json';
|
||||
import { PkSigning } from '@matrix-org/olm';
|
||||
import "../../olm-loader";
|
||||
import anotherjson from "another-json";
|
||||
import { PkSigning } from "@matrix-org/olm";
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
|
||||
import * as olmlib from "../../../src/crypto/olmlib";
|
||||
import { MatrixError } from '../../../src/http-api';
|
||||
import { logger } from '../../../src/logger';
|
||||
import { ICrossSigningKey, ICreateClientOpts, ISignedKey } from '../../../src/client';
|
||||
import { CryptoEvent } from '../../../src/crypto';
|
||||
import { IDevice } from '../../../src/crypto/deviceinfo';
|
||||
import { TestClient } from '../../TestClient';
|
||||
import { MatrixError } from "../../../src/http-api";
|
||||
import { logger } from "../../../src/logger";
|
||||
import { ICrossSigningKey, ICreateClientOpts, ISignedKey, MatrixClient } from "../../../src/client";
|
||||
import { CryptoEvent, IBootstrapCrossSigningOpts } from "../../../src/crypto";
|
||||
import { IDevice } from "../../../src/crypto/deviceinfo";
|
||||
import { TestClient } from "../../TestClient";
|
||||
import { resetCrossSigningKeys } from "./crypto-utils";
|
||||
|
||||
const PUSH_RULES_RESPONSE = {
|
||||
const PUSH_RULES_RESPONSE: Response = {
|
||||
method: "GET",
|
||||
path: "/pushrules/",
|
||||
data: {},
|
||||
};
|
||||
|
||||
const filterResponse = function(userId) {
|
||||
const filterResponse = function (userId: string): Response {
|
||||
const filterPath = "/user/" + encodeURIComponent(userId) + "/filter";
|
||||
return {
|
||||
method: "POST",
|
||||
@@ -43,33 +44,37 @@ const filterResponse = function(userId) {
|
||||
};
|
||||
};
|
||||
|
||||
function setHttpResponses(httpBackend, responses) {
|
||||
responses.forEach(response => {
|
||||
httpBackend
|
||||
.when(response.method, response.path)
|
||||
.respond(200, response.data);
|
||||
interface Response {
|
||||
method: "GET" | "PUT" | "POST" | "DELETE";
|
||||
path: string;
|
||||
data: object;
|
||||
}
|
||||
|
||||
function setHttpResponses(httpBackend: HttpBackend, responses: Response[]) {
|
||||
responses.forEach((response) => {
|
||||
httpBackend.when(response.method, response.path).respond(200, response.data);
|
||||
});
|
||||
}
|
||||
|
||||
async function makeTestClient(
|
||||
userInfo: { userId: string, deviceId: string},
|
||||
userInfo: { userId: string; deviceId: string },
|
||||
options: Partial<ICreateClientOpts> = {},
|
||||
keys = {},
|
||||
keys: Record<string, Uint8Array> = {},
|
||||
) {
|
||||
function getCrossSigningKey(type) {
|
||||
return keys[type];
|
||||
function getCrossSigningKey(type: string) {
|
||||
return keys[type] ?? null;
|
||||
}
|
||||
|
||||
function saveCrossSigningKeys(k) {
|
||||
function saveCrossSigningKeys(k: Record<string, Uint8Array>) {
|
||||
Object.assign(keys, k);
|
||||
}
|
||||
|
||||
options.cryptoCallbacks = Object.assign(
|
||||
{}, { getCrossSigningKey, saveCrossSigningKeys }, options.cryptoCallbacks || {},
|
||||
);
|
||||
const testClient = new TestClient(
|
||||
userInfo.userId, userInfo.deviceId, undefined, undefined, options,
|
||||
{},
|
||||
{ getCrossSigningKey, saveCrossSigningKeys },
|
||||
options.cryptoCallbacks || {},
|
||||
);
|
||||
const testClient = new TestClient(userInfo.userId, userInfo.deviceId, undefined, undefined, options);
|
||||
const client = testClient.client;
|
||||
|
||||
await client.initCrypto();
|
||||
@@ -77,24 +82,25 @@ async function makeTestClient(
|
||||
return { client, httpBackend: testClient.httpBackend };
|
||||
}
|
||||
|
||||
describe("Cross Signing", function() {
|
||||
describe("Cross Signing", function () {
|
||||
if (!global.Olm) {
|
||||
logger.warn('Not running megolm backup unit tests: libolm not present');
|
||||
logger.warn("Not running megolm backup unit tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
beforeAll(function () {
|
||||
return global.Olm.init();
|
||||
});
|
||||
|
||||
it("should sign the master key with the device key", async function() {
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
it("should sign the master key with the device key", async function () {
|
||||
const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
|
||||
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: {} });
|
||||
@@ -102,24 +108,22 @@ describe("Cross Signing", function() {
|
||||
alice.getAccountDataFromServer = async <T>() => ({} as T);
|
||||
// set Alice's cross-signing key
|
||||
await alice.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => { await func({}); },
|
||||
authUploadDeviceSigningKeys: async (func) => {
|
||||
await func({});
|
||||
},
|
||||
});
|
||||
expect(alice.uploadDeviceSigningKeys).toHaveBeenCalled();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should abort bootstrap if device signing auth fails", async function() {
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
it("should abort bootstrap if device signing auth fails", async function () {
|
||||
const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
|
||||
alice.uploadDeviceSigningKeys = async (auth, keys) => {
|
||||
const errorResponse = {
|
||||
session: "sessionId",
|
||||
flows: [
|
||||
{
|
||||
stages: [
|
||||
"m.login.password",
|
||||
],
|
||||
stages: ["m.login.password"],
|
||||
},
|
||||
],
|
||||
params: {},
|
||||
@@ -141,8 +145,10 @@ describe("Cross Signing", function() {
|
||||
};
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
alice.setAccountData = async () => ({});
|
||||
alice.getAccountDataFromServer = async <T extends {[k: string]: any}>(): Promise<T | null> => ({} as T);
|
||||
const authUploadDeviceSigningKeys = async func => await func({});
|
||||
alice.getAccountDataFromServer = async <T extends { [k: string]: any }>(): Promise<T | null> => ({} as T);
|
||||
const authUploadDeviceSigningKeys: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"] = async (func) => {
|
||||
await func({});
|
||||
};
|
||||
|
||||
// Try bootstrap, expecting `authUploadDeviceSigningKeys` to pass
|
||||
// through failure, stopping before actually applying changes.
|
||||
@@ -160,10 +166,8 @@ describe("Cross Signing", function() {
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should upload a signature when a user is verified", async function() {
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
it("should upload a signature when a user is verified", async function () {
|
||||
const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
|
||||
alice.uploadDeviceSigningKeys = async () => ({});
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
// set Alice's cross-signing key
|
||||
@@ -195,18 +199,14 @@ describe("Cross Signing", function() {
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it.skip("should get cross-signing keys from sync", async function() {
|
||||
it.skip("should get cross-signing keys from sync", async function () {
|
||||
const masterKey = new Uint8Array([
|
||||
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82,
|
||||
0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef,
|
||||
0xae, 0xb1, 0x05, 0xc1, 0xe7, 0x62, 0x78, 0xa6,
|
||||
0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
|
||||
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82, 0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef, 0xae, 0xb1,
|
||||
0x05, 0xc1, 0xe7, 0x62, 0x78, 0xa6, 0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
|
||||
]);
|
||||
const selfSigningKey = new Uint8Array([
|
||||
0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66,
|
||||
0x6b, 0xf8, 0x93, 0xf5, 0xb0, 0x4d, 0x17, 0xc0,
|
||||
0x17, 0xb5, 0xa5, 0xf6, 0x59, 0x11, 0x8b, 0x49,
|
||||
0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f,
|
||||
0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66, 0x6b, 0xf8, 0x93, 0xf5, 0xb0, 0x4d, 0x17, 0xc0, 0x17, 0xb5,
|
||||
0xa5, 0xf6, 0x59, 0x11, 0x8b, 0x49, 0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f,
|
||||
]);
|
||||
|
||||
const { client: alice, httpBackend } = await makeTestClient(
|
||||
@@ -214,8 +214,8 @@ describe("Cross Signing", function() {
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
// will be called to sign our own device
|
||||
getCrossSigningKey: async type => {
|
||||
if (type === 'master') {
|
||||
getCrossSigningKey: async (type) => {
|
||||
if (type === "master") {
|
||||
return masterKey;
|
||||
} else {
|
||||
return selfSigningKey;
|
||||
@@ -239,11 +239,10 @@ describe("Cross Signing", function() {
|
||||
try {
|
||||
await olmlib.verifySignature(
|
||||
alice.crypto!.olmDevice,
|
||||
content["@alice:example.com"][
|
||||
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
|
||||
],
|
||||
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,8 +257,7 @@ describe("Cross Signing", function() {
|
||||
});
|
||||
|
||||
// @ts-ignore private property
|
||||
const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"]
|
||||
.Osborne2;
|
||||
const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"].Osborne2;
|
||||
const aliceDevice = {
|
||||
user_id: "@alice:example.com",
|
||||
device_id: "Osborne2",
|
||||
@@ -267,15 +265,10 @@ describe("Cross Signing", function() {
|
||||
algorithms: deviceInfo.algorithms,
|
||||
};
|
||||
await alice.crypto!.signObject(aliceDevice);
|
||||
olmlib.pkSign(
|
||||
aliceDevice as ISignedKey,
|
||||
selfSigningKey as unknown as PkSigning,
|
||||
"@alice:example.com",
|
||||
'',
|
||||
);
|
||||
olmlib.pkSign(aliceDevice as ISignedKey, selfSigningKey as unknown as PkSigning, "@alice:example.com", "");
|
||||
|
||||
// feed sync result that includes master key, ssk, device key
|
||||
const responses = [
|
||||
const responses: Response[] = [
|
||||
PUSH_RULES_RESPONSE,
|
||||
{
|
||||
method: "POST",
|
||||
@@ -294,10 +287,7 @@ describe("Cross Signing", function() {
|
||||
data: {
|
||||
next_batch: "abcdefg",
|
||||
device_lists: {
|
||||
changed: [
|
||||
"@alice:example.com",
|
||||
"@bob:example.com",
|
||||
],
|
||||
changed: ["@alice:example.com", "@bob:example.com"],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -305,35 +295,35 @@ describe("Cross Signing", function() {
|
||||
method: "POST",
|
||||
path: "/keys/query",
|
||||
data: {
|
||||
"failures": {},
|
||||
"device_keys": {
|
||||
failures: {},
|
||||
device_keys: {
|
||||
"@alice:example.com": {
|
||||
"Osborne2": aliceDevice,
|
||||
Osborne2: aliceDevice,
|
||||
},
|
||||
},
|
||||
"master_keys": {
|
||||
master_keys: {
|
||||
"@alice:example.com": {
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk":
|
||||
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk",
|
||||
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk",
|
||||
},
|
||||
},
|
||||
},
|
||||
"self_signing_keys": {
|
||||
self_signing_keys: {
|
||||
"@alice:example.com": {
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["self-signing"],
|
||||
keys: {
|
||||
"ed25519:EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ":
|
||||
"EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ",
|
||||
"EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ",
|
||||
},
|
||||
signatures: {
|
||||
"@alice:example.com": {
|
||||
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk":
|
||||
"Wqx/HXR851KIi8/u/UX+fbAMtq9Uj8sr8FsOcqrLfVYa6lAmbXs"
|
||||
+ "Vhfy4AlZ3dnEtjgZx0U0QDrghEn2eYBeOCA",
|
||||
"Wqx/HXR851KIi8/u/UX+fbAMtq9Uj8sr8FsOcqrLfVYa6lAmbXs" +
|
||||
"Vhfy4AlZ3dnEtjgZx0U0QDrghEn2eYBeOCA",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -373,10 +363,8 @@ describe("Cross Signing", function() {
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should use trust chain to determine device verification", async function() {
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
it("should use trust chain to determine device verification", async function () {
|
||||
const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
|
||||
alice.uploadDeviceSigningKeys = async () => ({});
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
// set Alice's cross-signing key
|
||||
@@ -463,8 +451,8 @@ describe("Cross Signing", function() {
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it.skip("should trust signatures received from other devices", async function() {
|
||||
const aliceKeys: Record<string, PkSigning> = {};
|
||||
it.skip("should trust signatures received from other devices", async function () {
|
||||
const aliceKeys: Record<string, Uint8Array> = {};
|
||||
const { client: alice, httpBackend } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
undefined,
|
||||
@@ -479,10 +467,8 @@ describe("Cross Signing", function() {
|
||||
await resetCrossSigningKeys(alice);
|
||||
|
||||
const selfSigningKey = new Uint8Array([
|
||||
0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66,
|
||||
0x6b, 0xf8, 0x93, 0xf5, 0xb0, 0x4d, 0x17, 0xc0,
|
||||
0x17, 0xb5, 0xa5, 0xf6, 0x59, 0x11, 0x8b, 0x49,
|
||||
0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f,
|
||||
0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66, 0x6b, 0xf8, 0x93, 0xf5, 0xb0, 0x4d, 0x17, 0xc0, 0x17, 0xb5,
|
||||
0xa5, 0xf6, 0x59, 0x11, 0x8b, 0x49, 0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f,
|
||||
]);
|
||||
|
||||
const keyChangePromise = new Promise<void>((resolve, reject) => {
|
||||
@@ -494,8 +480,7 @@ describe("Cross Signing", function() {
|
||||
});
|
||||
|
||||
// @ts-ignore private property
|
||||
const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"]
|
||||
.Osborne2;
|
||||
const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"].Osborne2;
|
||||
const aliceDevice = {
|
||||
user_id: "@alice:example.com",
|
||||
device_id: "Osborne2",
|
||||
@@ -527,29 +512,23 @@ describe("Cross Signing", function() {
|
||||
verified: 0,
|
||||
known: false,
|
||||
};
|
||||
olmlib.pkSign(
|
||||
bobDevice,
|
||||
selfSigningKey as unknown as PkSigning,
|
||||
"@bob:example.com",
|
||||
'',
|
||||
);
|
||||
olmlib.pkSign(bobDevice, selfSigningKey as unknown as PkSigning, "@bob:example.com", "");
|
||||
|
||||
const bobMaster: ICrossSigningKey = {
|
||||
user_id: "@bob:example.com",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk":
|
||||
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk",
|
||||
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk",
|
||||
},
|
||||
};
|
||||
olmlib.pkSign(bobMaster, aliceKeys.user_signing, "@alice:example.com", '');
|
||||
olmlib.pkSign(bobMaster, aliceKeys.user_signing, "@alice:example.com", "");
|
||||
|
||||
// Alice downloads Bob's keys
|
||||
// - device key
|
||||
// - ssk
|
||||
// - master key signed by her usk (pretend that it was signed by another
|
||||
// of Alice's devices)
|
||||
const responses = [
|
||||
const responses: Response[] = [
|
||||
PUSH_RULES_RESPONSE,
|
||||
{
|
||||
method: "POST",
|
||||
@@ -568,9 +547,7 @@ describe("Cross Signing", function() {
|
||||
data: {
|
||||
next_batch: "abcdefg",
|
||||
device_lists: {
|
||||
changed: [
|
||||
"@bob:example.com",
|
||||
],
|
||||
changed: ["@bob:example.com"],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -578,31 +555,31 @@ describe("Cross Signing", function() {
|
||||
method: "POST",
|
||||
path: "/keys/query",
|
||||
data: {
|
||||
"failures": {},
|
||||
"device_keys": {
|
||||
failures: {},
|
||||
device_keys: {
|
||||
"@alice:example.com": {
|
||||
"Osborne2": aliceDevice,
|
||||
Osborne2: aliceDevice,
|
||||
},
|
||||
"@bob:example.com": {
|
||||
"Dynabook": bobDevice,
|
||||
Dynabook: bobDevice,
|
||||
},
|
||||
},
|
||||
"master_keys": {
|
||||
master_keys: {
|
||||
"@bob:example.com": bobMaster,
|
||||
},
|
||||
"self_signing_keys": {
|
||||
self_signing_keys: {
|
||||
"@bob:example.com": {
|
||||
user_id: "@bob:example.com",
|
||||
usage: ["self-signing"],
|
||||
keys: {
|
||||
"ed25519:EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ":
|
||||
"EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ",
|
||||
"EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ",
|
||||
},
|
||||
signatures: {
|
||||
"@bob:example.com": {
|
||||
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk":
|
||||
"2KLiufImvEbfJuAFvsaZD+PsL8ELWl7N1u9yr/9hZvwRghBfQMB"
|
||||
+ "LAI86b1kDV9+Cq1lt85ykReeCEzmTEPY2BQ",
|
||||
"2KLiufImvEbfJuAFvsaZD+PsL8ELWl7N1u9yr/9hZvwRghBfQMB" +
|
||||
"LAI86b1kDV9+Cq1lt85ykReeCEzmTEPY2BQ",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -638,10 +615,8 @@ describe("Cross Signing", function() {
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should dis-trust an unsigned device", async function() {
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
it("should dis-trust an unsigned device", async function () {
|
||||
const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
|
||||
alice.uploadDeviceSigningKeys = async () => ({});
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
// set Alice's cross-signing key
|
||||
@@ -708,10 +683,8 @@ describe("Cross Signing", function() {
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should dis-trust a user when their ssk changes", async function() {
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
it("should dis-trust a user when their ssk changes", async function () {
|
||||
const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
|
||||
alice.uploadDeviceSigningKeys = async () => ({});
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
await resetCrossSigningKeys(alice);
|
||||
@@ -852,8 +825,8 @@ describe("Cross Signing", function() {
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should offer to upgrade device verifications to cross-signing", async function() {
|
||||
let upgradeResolveFunc;
|
||||
it("should offer to upgrade device verifications to cross-signing", async function () {
|
||||
let upgradeResolveFunc: Function;
|
||||
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
@@ -866,11 +839,8 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
);
|
||||
const { client: bob } = await makeTestClient(
|
||||
{ userId: "@bob:example.com", deviceId: "Dynabook" },
|
||||
);
|
||||
const { client: bob } = await makeTestClient({ userId: "@bob:example.com", deviceId: "Dynabook" });
|
||||
|
||||
bob.uploadDeviceSigningKeys = async () => ({});
|
||||
bob.uploadKeySignatures = async () => ({ failures: {} });
|
||||
@@ -887,10 +857,7 @@ describe("Cross Signing", function() {
|
||||
known: true,
|
||||
},
|
||||
});
|
||||
alice.crypto!.deviceList.storeCrossSigningForUser(
|
||||
"@bob:example.com",
|
||||
bob.crypto!.crossSigningInfo.toStorage(),
|
||||
);
|
||||
alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", bob.crypto!.crossSigningInfo.toStorage());
|
||||
|
||||
alice.uploadDeviceSigningKeys = async () => ({});
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
@@ -909,8 +876,9 @@ 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();
|
||||
@@ -932,91 +900,83 @@ describe("Cross Signing", function() {
|
||||
bob.stopClient();
|
||||
});
|
||||
|
||||
it(
|
||||
"should observe that our own device is cross-signed, even if this device doesn't trust the key",
|
||||
async function() {
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => ({});
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
it("should observe that our own device is cross-signed, even if this device doesn't trust the key", async function () {
|
||||
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,
|
||||
// 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, but doesn't trust them yet
|
||||
alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
["ed25519:" + aliceMasterPubkey]: aliceMasterPubkey,
|
||||
},
|
||||
},
|
||||
};
|
||||
const sskSig = aliceMasterSigning.sign(anotherjson.stringify(aliceSSK));
|
||||
aliceSSK.signatures = {
|
||||
self_signing: aliceSSK,
|
||||
},
|
||||
firstUse: true,
|
||||
crossSigningVerifiedBefore: false,
|
||||
});
|
||||
|
||||
// Alice has a second device that's cross-signed
|
||||
const aliceDeviceId = "Dynabook";
|
||||
const aliceUnsignedDevice = {
|
||||
user_id: "@alice:example.com",
|
||||
device_id: aliceDeviceId,
|
||||
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
|
||||
keys: {
|
||||
"curve25519:Dynabook": "somePubkey",
|
||||
"ed25519:Dynabook": "someOtherPubkey",
|
||||
},
|
||||
};
|
||||
const sig = aliceSigning.sign(anotherjson.stringify(aliceUnsignedDevice));
|
||||
const aliceCrossSignedDevice: IDevice = {
|
||||
...aliceUnsignedDevice,
|
||||
verified: 0,
|
||||
known: false,
|
||||
signatures: {
|
||||
"@alice:example.com": {
|
||||
["ed25519:" + aliceMasterPubkey]: sskSig,
|
||||
["ed25519:" + alicePubkey]: sig,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
alice.crypto!.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
[aliceDeviceId]: aliceCrossSignedDevice,
|
||||
});
|
||||
|
||||
// Alice's device downloads the keys, but doesn't trust them yet
|
||||
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,
|
||||
});
|
||||
// We don't trust the cross-signing keys yet...
|
||||
expect(alice.checkDeviceTrust("@alice:example.com", aliceDeviceId).isCrossSigningVerified()).toBeFalsy();
|
||||
// ... but we do acknowledge that the device is signed by them
|
||||
expect(alice.checkIfOwnDeviceCrossSigned(aliceDeviceId)).toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
// Alice has a second device that's cross-signed
|
||||
const aliceDeviceId = 'Dynabook';
|
||||
const aliceUnsignedDevice = {
|
||||
user_id: "@alice:example.com",
|
||||
device_id: aliceDeviceId,
|
||||
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
|
||||
keys: {
|
||||
"curve25519:Dynabook": "somePubkey",
|
||||
"ed25519:Dynabook": "someOtherPubkey",
|
||||
},
|
||||
};
|
||||
const sig = aliceSigning.sign(anotherjson.stringify(aliceUnsignedDevice));
|
||||
const aliceCrossSignedDevice: IDevice = {
|
||||
...aliceUnsignedDevice,
|
||||
verified: 0,
|
||||
known: false,
|
||||
signatures: {
|
||||
"@alice:example.com": {
|
||||
["ed25519:" + alicePubkey]: sig,
|
||||
},
|
||||
} };
|
||||
alice.crypto!.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
[aliceDeviceId]: aliceCrossSignedDevice,
|
||||
});
|
||||
|
||||
// We don't trust the cross-signing keys yet...
|
||||
expect(
|
||||
alice.checkDeviceTrust("@alice:example.com", aliceDeviceId).isCrossSigningVerified(),
|
||||
).toBeFalsy();
|
||||
// ... but we do acknowledge that the device is signed by them
|
||||
expect(alice.checkIfOwnDeviceCrossSigned(aliceDeviceId)).toBeTruthy();
|
||||
alice.stopClient();
|
||||
},
|
||||
);
|
||||
|
||||
it("should observe that our own device isn't cross-signed", async function() {
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
it("should observe that our own device isn't cross-signed", async function () {
|
||||
const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
|
||||
alice.uploadDeviceSigningKeys = async () => ({});
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
|
||||
@@ -1076,9 +1036,7 @@ describe("Cross Signing", function() {
|
||||
});
|
||||
|
||||
it("checkIfOwnDeviceCrossSigned should sanely handle unknown devices", async () => {
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
|
||||
alice.uploadDeviceSigningKeys = async () => ({});
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
|
||||
@@ -1129,3 +1087,67 @@ describe("Cross Signing", function() {
|
||||
alice.stopClient();
|
||||
});
|
||||
});
|
||||
|
||||
describe("userHasCrossSigningKeys", function () {
|
||||
if (!global.Olm) {
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
return global.Olm.init();
|
||||
});
|
||||
|
||||
let aliceClient: MatrixClient;
|
||||
let httpBackend: HttpBackend;
|
||||
beforeEach(async () => {
|
||||
const testClient = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
|
||||
aliceClient = testClient.client;
|
||||
httpBackend = testClient.httpBackend;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
aliceClient.stopClient();
|
||||
});
|
||||
|
||||
it("should download devices and return true if one is a cross-signing key", async () => {
|
||||
httpBackend.when("POST", "/keys/query").respond(200, {
|
||||
master_keys: {
|
||||
"@alice:example.com": {
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk":
|
||||
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let result: boolean;
|
||||
await Promise.all([
|
||||
httpBackend.flush("/keys/query"),
|
||||
aliceClient.userHasCrossSigningKeys().then((res) => {
|
||||
result = res;
|
||||
}),
|
||||
]);
|
||||
expect(result!).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should download devices and return false if there is no cross-signing key", async () => {
|
||||
httpBackend.when("POST", "/keys/query").respond(200, {});
|
||||
|
||||
let result: boolean;
|
||||
await Promise.all([
|
||||
httpBackend.flush("/keys/query"),
|
||||
aliceClient.userHasCrossSigningKeys().then((res) => {
|
||||
result = res;
|
||||
}),
|
||||
]);
|
||||
expect(result!).toBeFalsy();
|
||||
});
|
||||
|
||||
it("throws an error if crypto is disabled", () => {
|
||||
aliceClient["cryptoBackend"] = undefined;
|
||||
expect(() => aliceClient.userHasCrossSigningKeys()).toThrowError("encryption disabled");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,34 +1,33 @@
|
||||
import { IRecoveryKey } from '../../../src/crypto/api';
|
||||
import { CrossSigningLevel } from '../../../src/crypto/CrossSigning';
|
||||
import { IndexedDBCryptoStore } from '../../../src/crypto/store/indexeddb-crypto-store';
|
||||
import { IRecoveryKey } from "../../../src/crypto/api";
|
||||
import { CrossSigningLevel } from "../../../src/crypto/CrossSigning";
|
||||
import { IndexedDBCryptoStore } from "../../../src/crypto/store/indexeddb-crypto-store";
|
||||
import { MatrixClient } from "../../../src";
|
||||
import { CryptoEvent } from "../../../src/crypto";
|
||||
|
||||
// needs to be phased out and replaced with bootstrapSecretStorage,
|
||||
// but that is doing too much extra stuff for it to be an easy transition.
|
||||
export async function resetCrossSigningKeys(
|
||||
client,
|
||||
{ level }: { level?: CrossSigningLevel} = {},
|
||||
client: MatrixClient,
|
||||
{ level }: { level?: CrossSigningLevel } = {},
|
||||
): Promise<void> {
|
||||
const crypto = client.crypto;
|
||||
const crypto = client.crypto!;
|
||||
|
||||
const oldKeys = Object.assign({}, crypto.crossSigningInfo.keys);
|
||||
try {
|
||||
await crypto.crossSigningInfo.resetKeys(level);
|
||||
await crypto.signObject(crypto.crossSigningInfo.keys.master);
|
||||
// write a copy locally so we know these are trusted keys
|
||||
await crypto.cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
crypto.cryptoStore.storeCrossSigningKeys(
|
||||
txn, crypto.crossSigningInfo.keys);
|
||||
},
|
||||
);
|
||||
await crypto.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
|
||||
crypto.cryptoStore.storeCrossSigningKeys(txn, crypto.crossSigningInfo.keys);
|
||||
});
|
||||
} catch (e) {
|
||||
// If anything failed here, revert the keys so we know to try again from the start
|
||||
// next time.
|
||||
crypto.crossSigningInfo.keys = oldKeys;
|
||||
throw e;
|
||||
}
|
||||
crypto.emit("crossSigning.keysChanged", {});
|
||||
crypto.emit(CryptoEvent.KeysChanged, {});
|
||||
// @ts-ignore
|
||||
await crypto.afterCrossSigningLocalKeyChange();
|
||||
}
|
||||
|
||||
|
||||
@@ -14,33 +14,30 @@ 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';
|
||||
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');
|
||||
logger.warn("Not running dehydration unit tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
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 alice = new TestClient("@alice:example.com", "Osborne2", undefined, undefined, {
|
||||
cryptoCallbacks: {
|
||||
getDehydrationKey: async (t) => key,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const dehydratedDevice = new Olm.Account();
|
||||
dehydratedDevice.create();
|
||||
@@ -56,25 +53,20 @@ describe("Dehydration", () => {
|
||||
success: true,
|
||||
});
|
||||
|
||||
expect((await Promise.all([
|
||||
alice.client.rehydrateDevice(),
|
||||
alice.httpBackend.flushAllExpected(),
|
||||
]))[0])
|
||||
.toEqual("ABCDEFG");
|
||||
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,
|
||||
},
|
||||
const alice = new TestClient("@alice:example.com", "Osborne2", undefined, undefined, {
|
||||
cryptoCallbacks: {
|
||||
getDehydrationKey: async (t) => key,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
await alice.client.initCrypto();
|
||||
|
||||
@@ -84,7 +76,8 @@ describe("Dehydration", () => {
|
||||
|
||||
let pickledAccount = "";
|
||||
|
||||
alice.httpBackend.when("PUT", "/dehydrated_device")
|
||||
alice.httpBackend
|
||||
.when("PUT", "/dehydrated_device")
|
||||
.check((req) => {
|
||||
expect(req.data.device_data).toMatchObject({
|
||||
algorithm: DEHYDRATION_ALGORITHM,
|
||||
@@ -95,7 +88,8 @@ describe("Dehydration", () => {
|
||||
.respond(200, {
|
||||
device_id: "ABCDEFG",
|
||||
});
|
||||
alice.httpBackend.when("POST", "/keys/upload/ABCDEFG")
|
||||
alice.httpBackend
|
||||
.when("POST", "/keys/upload/ABCDEFG")
|
||||
.check((req) => {
|
||||
expect(req.data).toMatchObject({
|
||||
"device_keys": expect.objectContaining({
|
||||
@@ -119,11 +113,12 @@ describe("Dehydration", () => {
|
||||
.respond(200, {});
|
||||
|
||||
try {
|
||||
const deviceId =
|
||||
(await Promise.all([
|
||||
const deviceId = (
|
||||
await Promise.all([
|
||||
alice.client.createDehydratedDevice(new Uint8Array(key), {}),
|
||||
alice.httpBackend.flushAllExpected(),
|
||||
]))[0];
|
||||
])
|
||||
)[0];
|
||||
|
||||
expect(deviceId).toEqual("ABCDEFG");
|
||||
expect(deviceId).not.toEqual("");
|
||||
|
||||
@@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { CryptoStore } from '../../../src/crypto/store/base';
|
||||
import { IndexedDBCryptoStore } from '../../../src/crypto/store/indexeddb-crypto-store';
|
||||
import { LocalStorageCryptoStore } from '../../../src/crypto/store/localStorage-crypto-store';
|
||||
import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store';
|
||||
import { RoomKeyRequestState } from '../../../src/crypto/OutgoingRoomKeyRequestManager';
|
||||
import { CryptoStore } from "../../../src/crypto/store/base";
|
||||
import { IndexedDBCryptoStore } from "../../../src/crypto/store/indexeddb-crypto-store";
|
||||
import { LocalStorageCryptoStore } from "../../../src/crypto/store/localStorage-crypto-store";
|
||||
import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store";
|
||||
import { RoomKeyRequestState } from "../../../src/crypto/OutgoingRoomKeyRequestManager";
|
||||
|
||||
import 'fake-indexeddb/auto';
|
||||
import 'jest-localstorage-mock';
|
||||
import "fake-indexeddb/auto";
|
||||
import "jest-localstorage-mock";
|
||||
|
||||
const requests = [
|
||||
{
|
||||
@@ -46,54 +46,46 @@ const requests = [
|
||||
requestId: "C",
|
||||
requestBody: { session_id: "C", room_id: "C", sender_key: "B", algorithm: "m.megolm.v1.aes-sha2" },
|
||||
state: RoomKeyRequestState.Unsent,
|
||||
recipients: [
|
||||
{ userId: "@becca:example.com", deviceId: "foobarbaz" },
|
||||
],
|
||||
recipients: [{ userId: "@becca:example.com", deviceId: "foobarbaz" }],
|
||||
},
|
||||
];
|
||||
|
||||
describe.each([
|
||||
["IndexedDBCryptoStore",
|
||||
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
|
||||
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(global.indexedDB, "tests")],
|
||||
["LocalStorageCryptoStore", () => new LocalStorageCryptoStore(localStorage)],
|
||||
["MemoryCryptoStore", () => new MemoryCryptoStore()],
|
||||
])("Outgoing room key requests [%s]", function(name, dbFactory) {
|
||||
])("Outgoing room key requests [%s]", function (name, dbFactory) {
|
||||
let store: CryptoStore;
|
||||
|
||||
beforeAll(async () => {
|
||||
store = dbFactory();
|
||||
await store.startup();
|
||||
await Promise.all(requests.map((request) =>
|
||||
store.getOrAddOutgoingRoomKeyRequest(request),
|
||||
));
|
||||
await Promise.all(requests.map((request) => store.getOrAddOutgoingRoomKeyRequest(request)));
|
||||
});
|
||||
|
||||
it("getAllOutgoingRoomKeyRequestsByState retrieves all entries in a given state",
|
||||
async () => {
|
||||
const r = await
|
||||
store.getAllOutgoingRoomKeyRequestsByState(RoomKeyRequestState.Sent);
|
||||
expect(r).toHaveLength(2);
|
||||
requests.filter((e) => e.state === RoomKeyRequestState.Sent).forEach((e) => {
|
||||
it("getAllOutgoingRoomKeyRequestsByState retrieves all entries in a given state", async () => {
|
||||
const r = await store.getAllOutgoingRoomKeyRequestsByState(RoomKeyRequestState.Sent);
|
||||
expect(r).toHaveLength(2);
|
||||
requests
|
||||
.filter((e) => e.state === RoomKeyRequestState.Sent)
|
||||
.forEach((e) => {
|
||||
expect(r).toContainEqual(e);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("getOutgoingRoomKeyRequestsByTarget retrieves all entries with a given target",
|
||||
async () => {
|
||||
const r = await store.getOutgoingRoomKeyRequestsByTarget(
|
||||
"@becca:example.com", "foobarbaz", [RoomKeyRequestState.Sent],
|
||||
);
|
||||
expect(r).toHaveLength(1);
|
||||
expect(r[0]).toEqual(requests[0]);
|
||||
});
|
||||
it("getOutgoingRoomKeyRequestsByTarget retrieves all entries with a given target", async () => {
|
||||
const r = await store.getOutgoingRoomKeyRequestsByTarget("@becca:example.com", "foobarbaz", [
|
||||
RoomKeyRequestState.Sent,
|
||||
]);
|
||||
expect(r).toHaveLength(1);
|
||||
expect(r[0]).toEqual(requests[0]);
|
||||
});
|
||||
|
||||
test("getOutgoingRoomKeyRequestByState retrieves any entry in a given state",
|
||||
async () => {
|
||||
const r =
|
||||
await store.getOutgoingRoomKeyRequestByState([RoomKeyRequestState.Sent]);
|
||||
expect(r).not.toBeNull();
|
||||
expect(r).not.toBeUndefined();
|
||||
expect(r!.state).toEqual(RoomKeyRequestState.Sent);
|
||||
expect(requests).toContainEqual(r);
|
||||
});
|
||||
test("getOutgoingRoomKeyRequestByState retrieves any entry in a given state", async () => {
|
||||
const r = await store.getOutgoingRoomKeyRequestByState([RoomKeyRequestState.Sent]);
|
||||
expect(r).not.toBeNull();
|
||||
expect(r).not.toBeUndefined();
|
||||
expect(r!.state).toEqual(RoomKeyRequestState.Sent);
|
||||
expect(requests).toContainEqual(r);
|
||||
});
|
||||
});
|
||||
|
||||
+177
-163
@@ -14,56 +14,70 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import '../../olm-loader';
|
||||
import "../../olm-loader";
|
||||
import * as olmlib from "../../../src/crypto/olmlib";
|
||||
import { IObject } from "../../../src/crypto/olmlib";
|
||||
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/crypto/SecretStorage";
|
||||
import { MatrixEvent } from "../../../src/models/event";
|
||||
import { TestClient } from '../../TestClient';
|
||||
import { makeTestClients } from './verification/util';
|
||||
import { TestClient } from "../../TestClient";
|
||||
import { makeTestClients } from "./verification/util";
|
||||
import { encryptAES } from "../../../src/crypto/aes";
|
||||
import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils";
|
||||
import { logger } from '../../../src/logger';
|
||||
import { ClientEvent, ICreateClientOpts } from '../../../src/client';
|
||||
import { ISecretStorageKeyInfo } from '../../../src/crypto/api';
|
||||
import { DeviceInfo } from '../../../src/crypto/deviceinfo';
|
||||
import { logger } from "../../../src/logger";
|
||||
import { ClientEvent, ICreateClientOpts, ICrossSigningKey, MatrixClient } from "../../../src/client";
|
||||
import { ISecretStorageKeyInfo } from "../../../src/crypto/api";
|
||||
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
|
||||
import { ISignatures } from "../../../src/@types/signed";
|
||||
import { ICurve25519AuthData } from "../../../src/crypto/keybackup";
|
||||
|
||||
async function makeTestClient(userInfo: { userId: string, deviceId: string}, options: Partial<ICreateClientOpts> = {}) {
|
||||
const client = (new TestClient(
|
||||
userInfo.userId, userInfo.deviceId, undefined, undefined, options,
|
||||
)).client;
|
||||
async function makeTestClient(
|
||||
userInfo: { userId: string; deviceId: string },
|
||||
options: Partial<ICreateClientOpts> = {},
|
||||
) {
|
||||
const client = new TestClient(userInfo.userId, userInfo.deviceId, undefined, undefined, options).client;
|
||||
|
||||
// Make it seem as if we've synced and thus the store can be trusted to
|
||||
// contain valid account data.
|
||||
client.isInitialSyncComplete = function() {
|
||||
client.isInitialSyncComplete = function () {
|
||||
return true;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Wrapper around pkSign to return a signed object. pkSign returns the
|
||||
// signature, rather than the signed object.
|
||||
function sign(obj, key, userId) {
|
||||
olmlib.pkSign(obj, key, userId, '');
|
||||
return obj;
|
||||
function sign<T extends IObject | ICurve25519AuthData>(
|
||||
obj: T,
|
||||
key: Uint8Array,
|
||||
userId: string,
|
||||
): T & {
|
||||
signatures: ISignatures;
|
||||
unsigned?: object;
|
||||
} {
|
||||
olmlib.pkSign(obj, key, userId, "");
|
||||
return obj as T & {
|
||||
signatures: ISignatures;
|
||||
unsigned?: object;
|
||||
};
|
||||
}
|
||||
|
||||
describe("Secrets", function() {
|
||||
describe("Secrets", function () {
|
||||
if (!global.Olm) {
|
||||
logger.warn('Not running megolm backup unit tests: libolm not present');
|
||||
logger.warn("Not running megolm backup unit tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
beforeAll(function () {
|
||||
return global.Olm.init();
|
||||
});
|
||||
|
||||
it("should store and retrieve a secret", async function() {
|
||||
it("should store and retrieve a secret", async function () {
|
||||
const key = new Uint8Array(16);
|
||||
for (let i = 0; i < 16; i++) key[i] = i;
|
||||
|
||||
@@ -73,22 +87,22 @@ describe("Secrets", function() {
|
||||
|
||||
const signingkeyInfo = {
|
||||
user_id: "@alice:example.com",
|
||||
usage: ['master'],
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
['ed25519:' + signingPubKey]: signingPubKey,
|
||||
["ed25519:" + signingPubKey]: signingPubKey,
|
||||
},
|
||||
};
|
||||
|
||||
const getKey = jest.fn().mockImplementation(async e => {
|
||||
const getKey = jest.fn().mockImplementation(async (e) => {
|
||||
expect(Object.keys(e.keys)).toEqual(["abc"]);
|
||||
return ['abc', key];
|
||||
return ["abc", key];
|
||||
});
|
||||
|
||||
const alice = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getCrossSigningKey: async t => signingKey,
|
||||
getCrossSigningKey: async (t) => signingKey,
|
||||
getSecretStorageKey: getKey,
|
||||
},
|
||||
},
|
||||
@@ -99,21 +113,20 @@ describe("Secrets", function() {
|
||||
|
||||
const secretStorage = alice.crypto!.secretStorage;
|
||||
|
||||
jest.spyOn(alice, 'setAccountData').mockImplementation(
|
||||
async function(eventType, contents) {
|
||||
alice.store.storeAccountDataEvents([
|
||||
new MatrixEvent({
|
||||
type: eventType,
|
||||
content: contents,
|
||||
}),
|
||||
]);
|
||||
return {};
|
||||
});
|
||||
jest.spyOn(alice, "setAccountData").mockImplementation(async function (eventType, contents) {
|
||||
alice.store.storeAccountDataEvents([
|
||||
new MatrixEvent({
|
||||
type: eventType,
|
||||
content: contents,
|
||||
}),
|
||||
]);
|
||||
return {};
|
||||
});
|
||||
|
||||
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({
|
||||
@@ -133,54 +146,48 @@ describe("Secrets", function() {
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should throw if given a key that doesn't exist", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
it("should throw if given a key that doesn't exist", async function () {
|
||||
const alice = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
|
||||
|
||||
try {
|
||||
await alice.storeSecret("foo", "bar", ["this secret does not exist"]);
|
||||
// should be able to use expect(...).toThrow() but mocha still fails
|
||||
// the test even when it throws for reasons I have no inclination to debug
|
||||
expect(true).toBeFalsy();
|
||||
} catch (e) {
|
||||
}
|
||||
} catch (e) {}
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should refuse to encrypt with zero keys", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
it("should refuse to encrypt with zero keys", async function () {
|
||||
const alice = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
|
||||
|
||||
try {
|
||||
await alice.storeSecret("foo", "bar", []);
|
||||
expect(true).toBeFalsy();
|
||||
} catch (e) {
|
||||
}
|
||||
} catch (e) {}
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should encrypt with default key if keys is null", async function() {
|
||||
it("should encrypt with default key if keys is null", async function () {
|
||||
const key = new Uint8Array(16);
|
||||
for (let i = 0; i < 16; i++) key[i] = i;
|
||||
const getKey = jest.fn().mockImplementation(async e => {
|
||||
const getKey = jest.fn().mockImplementation(async (e) => {
|
||||
expect(Object.keys(e.keys)).toEqual([newKeyId]);
|
||||
return [newKeyId, key];
|
||||
});
|
||||
|
||||
let keys = {};
|
||||
let keys: Record<string, Uint8Array> = {};
|
||||
const alice = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getCrossSigningKey: t => keys[t],
|
||||
saveCrossSigningKeys: k => keys = k,
|
||||
getCrossSigningKey: (t) => Promise.resolve(keys[t]),
|
||||
saveCrossSigningKeys: (k) => (keys = k),
|
||||
getSecretStorageKey: getKey,
|
||||
},
|
||||
},
|
||||
);
|
||||
alice.setAccountData = async function(eventType, contents) {
|
||||
alice.setAccountData = async function (eventType, contents) {
|
||||
alice.store.storeAccountDataEvents([
|
||||
new MatrixEvent({
|
||||
type: eventType,
|
||||
@@ -191,33 +198,31 @@ describe("Secrets", function() {
|
||||
};
|
||||
resetCrossSigningKeys(alice);
|
||||
|
||||
const { keyId: newKeyId } = await alice.addSecretStorageKey(
|
||||
SECRET_STORAGE_ALGORITHM_V1_AES, { pubkey: undefined, key: undefined },
|
||||
);
|
||||
const { keyId: newKeyId } = await alice.addSecretStorageKey(SECRET_STORAGE_ALGORITHM_V1_AES, {
|
||||
pubkey: undefined,
|
||||
key: undefined,
|
||||
});
|
||||
// we don't await on this because it waits for the event to come down the sync
|
||||
// which won't happen in the test setup
|
||||
alice.setDefaultSecretStorageKeyId(newKeyId);
|
||||
await alice.storeSecret("foo", "bar");
|
||||
|
||||
const accountData = alice.getAccountData('foo');
|
||||
const accountData = alice.getAccountData("foo");
|
||||
expect(accountData!.getContent().encrypted).toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should refuse to encrypt if no keys given and no default key", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
it("should refuse to encrypt if no keys given and no default key", async function () {
|
||||
const alice = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
|
||||
|
||||
try {
|
||||
await alice.storeSecret("foo", "bar");
|
||||
expect(true).toBeFalsy();
|
||||
} catch (e) {
|
||||
}
|
||||
} catch (e) {}
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should request secrets from other clients", async function() {
|
||||
it("should request secrets from other clients", async function () {
|
||||
const [[osborne2, vax], clearTestClientTimeouts] = await makeTestClients(
|
||||
[
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
@@ -227,7 +232,7 @@ describe("Secrets", function() {
|
||||
cryptoCallbacks: {
|
||||
onSecretRequested: (userId, deviceId, requestId, secretName, deviceTrust) => {
|
||||
expect(secretName).toBe("foo");
|
||||
return "bar";
|
||||
return Promise.resolve("bar");
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -238,7 +243,7 @@ describe("Secrets", function() {
|
||||
const secretStorage = osborne2.client.crypto!.secretStorage;
|
||||
|
||||
osborne2.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
"VAX": {
|
||||
VAX: {
|
||||
known: false,
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
keys: {
|
||||
@@ -249,7 +254,7 @@ describe("Secrets", function() {
|
||||
},
|
||||
});
|
||||
vax.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
"Osborne2": {
|
||||
Osborne2: {
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
verified: 0,
|
||||
known: false,
|
||||
@@ -280,30 +285,20 @@ describe("Secrets", function() {
|
||||
clearTestClientTimeouts();
|
||||
});
|
||||
|
||||
describe("bootstrap", function() {
|
||||
describe("bootstrap", function () {
|
||||
// keys used in some of the tests
|
||||
const XSK = new Uint8Array(
|
||||
olmlib.decodeBase64("3lo2YdJugHjfE+Or7KJ47NuKbhE7AAGLgQ/dc19913Q="),
|
||||
);
|
||||
const XSK = new Uint8Array(olmlib.decodeBase64("3lo2YdJugHjfE+Or7KJ47NuKbhE7AAGLgQ/dc19913Q="));
|
||||
const XSPubKey = "DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0";
|
||||
const USK = new Uint8Array(
|
||||
olmlib.decodeBase64("lKWi3hJGUie5xxHgySoz8PHFnZv6wvNaud/p2shN9VU="),
|
||||
);
|
||||
const USK = new Uint8Array(olmlib.decodeBase64("lKWi3hJGUie5xxHgySoz8PHFnZv6wvNaud/p2shN9VU="));
|
||||
const USPubKey = "CUpoiTtHiyXpUmd+3ohb7JVxAlUaOG1NYs9Jlx8soQU";
|
||||
const SSK = new Uint8Array(
|
||||
olmlib.decodeBase64("1R6JVlXX99UcfUZzKuCDGQgJTw8ur1/ofgPD8pp+96M="),
|
||||
);
|
||||
const SSK = new Uint8Array(olmlib.decodeBase64("1R6JVlXX99UcfUZzKuCDGQgJTw8ur1/ofgPD8pp+96M="));
|
||||
const SSPubKey = "0DfNsRDzEvkCLA0gD3m7VAGJ5VClhjEsewI35xq873Q";
|
||||
const SSSSKey = new Uint8Array(
|
||||
olmlib.decodeBase64(
|
||||
"XrmITOOdBhw6yY5Bh7trb/bgp1FRdIGyCUxxMP873R0=",
|
||||
),
|
||||
);
|
||||
const SSSSKey = new Uint8Array(olmlib.decodeBase64("XrmITOOdBhw6yY5Bh7trb/bgp1FRdIGyCUxxMP873R0="));
|
||||
|
||||
it("bootstraps when no storage or cross-signing keys locally", async function() {
|
||||
it("bootstraps when no storage or cross-signing keys locally", async function () {
|
||||
const key = new Uint8Array(16);
|
||||
for (let i = 0; i < 16; i++) key[i] = i;
|
||||
const getKey = jest.fn().mockImplementation(async e => {
|
||||
const getKey = jest.fn().mockImplementation(async (e) => {
|
||||
return [Object.keys(e.keys)[0], key];
|
||||
});
|
||||
|
||||
@@ -320,20 +315,20 @@ describe("Secrets", function() {
|
||||
);
|
||||
bob.uploadDeviceSigningKeys = async () => ({});
|
||||
bob.uploadKeySignatures = jest.fn().mockResolvedValue(undefined);
|
||||
bob.setAccountData = async function(eventType, contents) {
|
||||
bob.setAccountData = async function (eventType, contents) {
|
||||
const event = new MatrixEvent({
|
||||
type: eventType,
|
||||
content: contents,
|
||||
});
|
||||
this.store.storeAccountDataEvents([
|
||||
event,
|
||||
]);
|
||||
this.store.storeAccountDataEvents([event]);
|
||||
this.emit(ClientEvent.AccountData, event);
|
||||
return {};
|
||||
};
|
||||
|
||||
await bob.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => { await func({}); },
|
||||
authUploadDeviceSigningKeys: async (func) => {
|
||||
await func({});
|
||||
},
|
||||
});
|
||||
await bob.bootstrapSecretStorage({
|
||||
createSecretStorageKey,
|
||||
@@ -343,53 +338,53 @@ describe("Secrets", function() {
|
||||
const secretStorage = bob.crypto!.secretStorage;
|
||||
|
||||
expect(crossSigning.getId()).toBeTruthy();
|
||||
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
|
||||
.toBeTruthy();
|
||||
expect(await crossSigning.isStoredInSecretStorage(secretStorage)).toBeTruthy();
|
||||
expect(await secretStorage.hasKey()).toBeTruthy();
|
||||
bob.stopClient();
|
||||
});
|
||||
|
||||
it("bootstraps when cross-signing keys in secret storage", async function() {
|
||||
it("bootstraps when cross-signing keys in secret storage", async function () {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
const storagePublicKey = decryption.generate_key();
|
||||
const storagePrivateKey = decryption.get_private_key();
|
||||
|
||||
const bob = await makeTestClient(
|
||||
const bob: MatrixClient = await makeTestClient(
|
||||
{
|
||||
userId: "@bob:example.com",
|
||||
deviceId: "bob1",
|
||||
},
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getSecretStorageKey: async request => {
|
||||
getSecretStorageKey: async (request) => {
|
||||
const defaultKeyId = await bob.getDefaultSecretStorageKeyId();
|
||||
expect(Object.keys(request.keys)).toEqual([defaultKeyId]);
|
||||
return [defaultKeyId, storagePrivateKey];
|
||||
return [defaultKeyId!, storagePrivateKey];
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
bob.uploadDeviceSigningKeys = async () => {};
|
||||
bob.uploadKeySignatures = async () => {};
|
||||
bob.setAccountData = async function(eventType, contents, callback) {
|
||||
bob.uploadDeviceSigningKeys = async () => ({});
|
||||
bob.uploadKeySignatures = async () => ({ failures: {} });
|
||||
bob.setAccountData = async function (eventType, contents) {
|
||||
const event = new MatrixEvent({
|
||||
type: eventType,
|
||||
content: contents,
|
||||
});
|
||||
this.store.storeAccountDataEvents([
|
||||
event,
|
||||
]);
|
||||
this.emit("accountData", event);
|
||||
this.store.storeAccountDataEvents([event]);
|
||||
this.emit(ClientEvent.AccountData, event);
|
||||
return {};
|
||||
};
|
||||
bob.crypto.backupManager.checkKeyBackup = async () => {};
|
||||
bob.crypto!.backupManager.checkKeyBackup = async () => null;
|
||||
|
||||
const crossSigning = bob.crypto.crossSigningInfo;
|
||||
const secretStorage = bob.crypto.secretStorage;
|
||||
const crossSigning = bob.crypto!.crossSigningInfo;
|
||||
const secretStorage = bob.crypto!.secretStorage;
|
||||
|
||||
// Set up cross-signing keys from scratch with specific storage key
|
||||
await bob.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
authUploadDeviceSigningKeys: async (func) => {
|
||||
await func({});
|
||||
},
|
||||
});
|
||||
await bob.bootstrapSecretStorage({
|
||||
createSecretStorageKey: async () => ({
|
||||
@@ -400,37 +395,35 @@ describe("Secrets", function() {
|
||||
});
|
||||
|
||||
// Clear local cross-signing keys and read from secret storage
|
||||
bob.crypto.deviceList.storeCrossSigningForUser(
|
||||
"@bob:example.com",
|
||||
crossSigning.toStorage(),
|
||||
);
|
||||
bob.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", crossSigning.toStorage());
|
||||
crossSigning.keys = {};
|
||||
await bob.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
authUploadDeviceSigningKeys: async (func) => {
|
||||
await func({});
|
||||
},
|
||||
});
|
||||
|
||||
expect(crossSigning.getId()).toBeTruthy();
|
||||
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
|
||||
.toBeTruthy();
|
||||
expect(await crossSigning.isStoredInSecretStorage(secretStorage)).toBeTruthy();
|
||||
expect(await secretStorage.hasKey()).toBeTruthy();
|
||||
bob.stopClient();
|
||||
});
|
||||
|
||||
it("adds passphrase checking if it's lacking", async function() {
|
||||
it("adds passphrase checking if it's lacking", async function () {
|
||||
let crossSigningKeys: Record<string, Uint8Array> = {
|
||||
master: XSK,
|
||||
user_signing: USK,
|
||||
self_signing: SSK,
|
||||
};
|
||||
const secretStorageKeys = {
|
||||
const secretStorageKeys: Record<string, Uint8Array> = {
|
||||
key_id: SSSSKey,
|
||||
};
|
||||
const alice = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getCrossSigningKey: async t => crossSigningKeys[t],
|
||||
saveCrossSigningKeys: k => crossSigningKeys = k,
|
||||
getCrossSigningKey: async (t) => crossSigningKeys[t],
|
||||
saveCrossSigningKeys: (k) => (crossSigningKeys = k),
|
||||
getSecretStorageKey: async ({ keys }, name) => {
|
||||
for (const keyId of Object.keys(keys)) {
|
||||
if (secretStorageKeys[keyId]) {
|
||||
@@ -498,32 +491,44 @@ describe("Secrets", function() {
|
||||
[`ed25519:${XSPubKey}`]: XSPubKey,
|
||||
},
|
||||
},
|
||||
self_signing: sign({
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
[`ed25519:${SSPubKey}`]: SSPubKey,
|
||||
self_signing: sign<ICrossSigningKey>(
|
||||
{
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
[`ed25519:${SSPubKey}`]: SSPubKey,
|
||||
},
|
||||
},
|
||||
}, XSK, "@alice:example.com"),
|
||||
user_signing: sign({
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["user_signing"],
|
||||
keys: {
|
||||
[`ed25519:${USPubKey}`]: USPubKey,
|
||||
XSK,
|
||||
"@alice:example.com",
|
||||
),
|
||||
user_signing: sign<ICrossSigningKey>(
|
||||
{
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["user_signing"],
|
||||
keys: {
|
||||
[`ed25519:${USPubKey}`]: USPubKey,
|
||||
},
|
||||
},
|
||||
}, XSK, "@alice:example.com"),
|
||||
XSK,
|
||||
"@alice:example.com",
|
||||
),
|
||||
},
|
||||
});
|
||||
alice.getKeyBackupVersion = async () => {
|
||||
return {
|
||||
version: "1",
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
auth_data: sign({
|
||||
public_key: "pxEXhg+4vdMf/kFwP4bVawFWdb0EmytL3eFJx++zQ0A",
|
||||
}, XSK, "@alice:example.com"),
|
||||
auth_data: sign(
|
||||
{
|
||||
public_key: "pxEXhg+4vdMf/kFwP4bVawFWdb0EmytL3eFJx++zQ0A",
|
||||
},
|
||||
XSK,
|
||||
"@alice:example.com",
|
||||
),
|
||||
};
|
||||
};
|
||||
alice.setAccountData = async function(name, data) {
|
||||
alice.setAccountData = async function (name, data) {
|
||||
const event = new MatrixEvent({
|
||||
type: name,
|
||||
content: data,
|
||||
@@ -535,11 +540,9 @@ describe("Secrets", function() {
|
||||
|
||||
await alice.bootstrapSecretStorage({});
|
||||
|
||||
expect(alice.getAccountData("m.secret_storage.default_key")!.getContent())
|
||||
.toEqual({ key: "key_id" });
|
||||
expect(alice.getAccountData("m.secret_storage.default_key")!.getContent()).toEqual({ key: "key_id" });
|
||||
const keyInfo = alice.getAccountData("m.secret_storage.key.key_id")!.getContent<ISecretStorageKeyInfo>();
|
||||
expect(keyInfo.algorithm)
|
||||
.toEqual("m.secret_storage.v1.aes-hmac-sha2");
|
||||
expect(keyInfo.algorithm).toEqual("m.secret_storage.v1.aes-hmac-sha2");
|
||||
expect(keyInfo.passphrase).toEqual({
|
||||
algorithm: "m.pbkdf2",
|
||||
iterations: 500000,
|
||||
@@ -547,25 +550,24 @@ describe("Secrets", function() {
|
||||
});
|
||||
expect(keyInfo).toHaveProperty("iv");
|
||||
expect(keyInfo).toHaveProperty("mac");
|
||||
expect(alice.checkSecretStorageKey(secretStorageKeys.key_id, keyInfo))
|
||||
.toBeTruthy();
|
||||
expect(alice.checkSecretStorageKey(secretStorageKeys.key_id, keyInfo)).toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
it("fixes backup keys in the wrong format", async function() {
|
||||
it("fixes backup keys in the wrong format", async function () {
|
||||
let crossSigningKeys: Record<string, Uint8Array> = {
|
||||
master: XSK,
|
||||
user_signing: USK,
|
||||
self_signing: SSK,
|
||||
};
|
||||
const secretStorageKeys = {
|
||||
const secretStorageKeys: Record<string, Uint8Array> = {
|
||||
key_id: SSSSKey,
|
||||
};
|
||||
const alice = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getCrossSigningKey: async t => crossSigningKeys[t],
|
||||
saveCrossSigningKeys: k => crossSigningKeys = k,
|
||||
getCrossSigningKey: async (t) => crossSigningKeys[t],
|
||||
saveCrossSigningKeys: (k) => (crossSigningKeys = k),
|
||||
getSecretStorageKey: async ({ keys }, name) => {
|
||||
for (const keyId of Object.keys(keys)) {
|
||||
if (secretStorageKeys[keyId]) {
|
||||
@@ -625,7 +627,8 @@ describe("Secrets", function() {
|
||||
encrypted: {
|
||||
key_id: await encryptAES(
|
||||
"123,45,6,7,89,1,234,56,78,90,12,34,5,67,8,90",
|
||||
secretStorageKeys.key_id, "m.megolm_backup.v1",
|
||||
secretStorageKeys.key_id,
|
||||
"m.megolm_backup.v1",
|
||||
),
|
||||
},
|
||||
},
|
||||
@@ -642,32 +645,44 @@ describe("Secrets", function() {
|
||||
[`ed25519:${XSPubKey}`]: XSPubKey,
|
||||
},
|
||||
},
|
||||
self_signing: sign({
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
[`ed25519:${SSPubKey}`]: SSPubKey,
|
||||
self_signing: sign<ICrossSigningKey>(
|
||||
{
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
[`ed25519:${SSPubKey}`]: SSPubKey,
|
||||
},
|
||||
},
|
||||
}, XSK, "@alice:example.com"),
|
||||
user_signing: sign({
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["user_signing"],
|
||||
keys: {
|
||||
[`ed25519:${USPubKey}`]: USPubKey,
|
||||
XSK,
|
||||
"@alice:example.com",
|
||||
),
|
||||
user_signing: sign<ICrossSigningKey>(
|
||||
{
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["user_signing"],
|
||||
keys: {
|
||||
[`ed25519:${USPubKey}`]: USPubKey,
|
||||
},
|
||||
},
|
||||
}, XSK, "@alice:example.com"),
|
||||
XSK,
|
||||
"@alice:example.com",
|
||||
),
|
||||
},
|
||||
});
|
||||
alice.getKeyBackupVersion = async () => {
|
||||
return {
|
||||
version: "1",
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
auth_data: sign({
|
||||
public_key: "pxEXhg+4vdMf/kFwP4bVawFWdb0EmytL3eFJx++zQ0A",
|
||||
}, XSK, "@alice:example.com"),
|
||||
auth_data: sign(
|
||||
{
|
||||
public_key: "pxEXhg+4vdMf/kFwP4bVawFWdb0EmytL3eFJx++zQ0A",
|
||||
},
|
||||
XSK,
|
||||
"@alice:example.com",
|
||||
),
|
||||
};
|
||||
};
|
||||
alice.setAccountData = async function(name, data) {
|
||||
alice.setAccountData = async function (name, data) {
|
||||
const event = new MatrixEvent({
|
||||
type: name,
|
||||
content: data,
|
||||
@@ -681,8 +696,7 @@ describe("Secrets", function() {
|
||||
|
||||
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==");
|
||||
expect(await alice.getSecret("m.megolm_backup.v1")).toEqual("ey0GB1kB6jhOWgwiBUMIWg==");
|
||||
alice.stopClient();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,15 +17,17 @@ import { MatrixClient } from "../../../../src/client";
|
||||
import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel";
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
|
||||
describe("InRoomChannel tests", function() {
|
||||
describe("InRoomChannel tests", function () {
|
||||
const ALICE = "@alice:hs.tld";
|
||||
const BOB = "@bob:hs.tld";
|
||||
const MALORY = "@malory:hs.tld";
|
||||
const client = {
|
||||
getUserId() { return ALICE; },
|
||||
getUserId() {
|
||||
return ALICE;
|
||||
},
|
||||
} as unknown as MatrixClient;
|
||||
|
||||
it("getEventType only returns .request for a message with a msgtype", function() {
|
||||
it("getEventType only returns .request for a message with a msgtype", function () {
|
||||
const invalidEvent = new MatrixEvent({
|
||||
type: "m.key.verification.request",
|
||||
});
|
||||
@@ -34,34 +36,29 @@ describe("InRoomChannel tests", function() {
|
||||
type: "m.room.message",
|
||||
content: { msgtype: "m.key.verification.request" },
|
||||
});
|
||||
expect(InRoomChannel.getEventType(validEvent)).
|
||||
toStrictEqual("m.key.verification.request");
|
||||
expect(InRoomChannel.getEventType(validEvent)).toStrictEqual("m.key.verification.request");
|
||||
const validFooEvent = new MatrixEvent({ type: "m.foo" });
|
||||
expect(InRoomChannel.getEventType(validFooEvent)).
|
||||
toStrictEqual("m.foo");
|
||||
expect(InRoomChannel.getEventType(validFooEvent)).toStrictEqual("m.foo");
|
||||
});
|
||||
|
||||
it("getEventType should return m.room.message for messages", function() {
|
||||
it("getEventType should return m.room.message for messages", function () {
|
||||
const messageEvent = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
content: { msgtype: "m.text" },
|
||||
});
|
||||
// XXX: The event type doesn't matter too much, just as long as it's not a verification event
|
||||
expect(InRoomChannel.getEventType(messageEvent)).
|
||||
toStrictEqual("m.room.message");
|
||||
expect(InRoomChannel.getEventType(messageEvent)).toStrictEqual("m.room.message");
|
||||
});
|
||||
|
||||
it("getEventType should return actual type for non-message events", function() {
|
||||
it("getEventType should return actual type for non-message events", function () {
|
||||
const event = new MatrixEvent({
|
||||
type: "m.room.member",
|
||||
content: { },
|
||||
content: {},
|
||||
});
|
||||
expect(InRoomChannel.getEventType(event)).
|
||||
toStrictEqual("m.room.member");
|
||||
expect(InRoomChannel.getEventType(event)).toStrictEqual("m.room.member");
|
||||
});
|
||||
|
||||
it("getOtherPartyUserId should not return anything for a request not " +
|
||||
"directed at me", function() {
|
||||
it("getOtherPartyUserId should not return anything for a request not " + "directed at me", function () {
|
||||
const event = new MatrixEvent({
|
||||
sender: BOB,
|
||||
type: "m.room.message",
|
||||
@@ -70,29 +67,25 @@ describe("InRoomChannel tests", function() {
|
||||
expect(InRoomChannel.getOtherPartyUserId(event, client)).toStrictEqual(undefined);
|
||||
});
|
||||
|
||||
it("getOtherPartyUserId should not return anything an event that is not of a valid " +
|
||||
"request type", function() {
|
||||
it("getOtherPartyUserId should not return anything an event that is not of a valid " + "request type", function () {
|
||||
// invalid because this should be a room message with msgtype
|
||||
const invalidRequest = new MatrixEvent({
|
||||
sender: BOB,
|
||||
type: "m.key.verification.request",
|
||||
content: { to: ALICE },
|
||||
});
|
||||
expect(InRoomChannel.getOtherPartyUserId(invalidRequest, client))
|
||||
.toStrictEqual(undefined);
|
||||
expect(InRoomChannel.getOtherPartyUserId(invalidRequest, client)).toStrictEqual(undefined);
|
||||
const startEvent = new MatrixEvent({
|
||||
sender: BOB,
|
||||
type: "m.key.verification.start",
|
||||
content: { to: ALICE },
|
||||
});
|
||||
expect(InRoomChannel.getOtherPartyUserId(startEvent, client))
|
||||
.toStrictEqual(undefined);
|
||||
expect(InRoomChannel.getOtherPartyUserId(startEvent, client)).toStrictEqual(undefined);
|
||||
const fooEvent = new MatrixEvent({
|
||||
sender: BOB,
|
||||
type: "m.foo",
|
||||
content: { to: ALICE },
|
||||
});
|
||||
expect(InRoomChannel.getOtherPartyUserId(fooEvent, client))
|
||||
.toStrictEqual(undefined);
|
||||
expect(InRoomChannel.getOtherPartyUserId(fooEvent, client)).toStrictEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,13 +19,13 @@ import { logger } from "../../../../src/logger";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
describe("QR code verification", function() {
|
||||
describe("QR code verification", function () {
|
||||
if (!global.Olm) {
|
||||
logger.warn('Not running device verification tests: libolm not present');
|
||||
logger.warn("Not running device verification tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
beforeAll(function () {
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
|
||||
@@ -18,23 +18,23 @@ import "../../../olm-loader";
|
||||
import { CryptoEvent, verificationMethods } from "../../../../src/crypto";
|
||||
import { logger } from "../../../../src/logger";
|
||||
import { SAS } from "../../../../src/crypto/verification/SAS";
|
||||
import { makeTestClients } from './util';
|
||||
import { makeTestClients } from "./util";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("verification request integration tests with crypto layer", function() {
|
||||
describe("verification request integration tests with crypto layer", function () {
|
||||
if (!global.Olm) {
|
||||
logger.warn('Not running device verification unit tests: libolm not present');
|
||||
logger.warn("Not running device verification unit tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
beforeAll(function () {
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
it("should request and accept a verification", async function() {
|
||||
it("should request and accept a verification", async function () {
|
||||
const [[alice, bob], clearTestClientTimeouts] = await makeTestClients(
|
||||
[
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
@@ -44,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: [],
|
||||
@@ -66,7 +66,7 @@ describe("verification request integration tests with crypto layer", function()
|
||||
bobVerifier.endTimer();
|
||||
});
|
||||
const aliceRequest = await alice.client.requestVerification("@bob:example.com");
|
||||
await aliceRequest.waitFor(r => r.started);
|
||||
await aliceRequest.waitFor((r) => r.started);
|
||||
const aliceVerifier = aliceRequest.verifier;
|
||||
expect(aliceVerifier).toBeInstanceOf(SAS);
|
||||
|
||||
|
||||
@@ -15,15 +15,15 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import "../../../olm-loader";
|
||||
import { makeTestClients } from './util';
|
||||
import { makeTestClients } from "./util";
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import { ISasEvent, SAS, SasEvent } from "../../../../src/crypto/verification/SAS";
|
||||
import { DeviceInfo } from "../../../../src/crypto/deviceinfo";
|
||||
import { DeviceInfo, IDevice } 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 as Verification, VerificationBase } from "../../../../src/crypto/verification/Base";
|
||||
import { 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";
|
||||
@@ -31,43 +31,45 @@ import { TestClient } from "../../../TestClient";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
let ALICE_DEVICES;
|
||||
let BOB_DEVICES;
|
||||
let ALICE_DEVICES: Record<string, IDevice>;
|
||||
let BOB_DEVICES: Record<string, IDevice>;
|
||||
|
||||
describe("SAS verification", function() {
|
||||
describe("SAS verification", function () {
|
||||
if (!global.Olm) {
|
||||
logger.warn('Not running device verification unit tests: libolm not present');
|
||||
logger.warn("Not running device verification unit tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
beforeAll(function () {
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
it("should error on an unexpected event", async function() {
|
||||
it("should error on an unexpected event", async function () {
|
||||
//channel, baseApis, userId, deviceId, startEvent, request
|
||||
const request = {
|
||||
onVerifierCancelled: function() {},
|
||||
onVerifierCancelled: function () {},
|
||||
} as VerificationRequest;
|
||||
const channel = {
|
||||
send: function() {
|
||||
send: function () {
|
||||
return Promise.resolve();
|
||||
},
|
||||
} as unknown as IVerificationChannel;
|
||||
const mockClient = {} as unknown as MatrixClient;
|
||||
const event = new MatrixEvent({ type: 'test' });
|
||||
const event = new MatrixEvent({ type: "test" });
|
||||
const sas = new SAS(channel, mockClient, "@alice:example.com", "ABCDEFG", event, request);
|
||||
sas.handleEvent(new MatrixEvent({
|
||||
sender: "@alice:example.com",
|
||||
type: "es.inquisition",
|
||||
content: {},
|
||||
}));
|
||||
sas.handleEvent(
|
||||
new MatrixEvent({
|
||||
sender: "@alice:example.com",
|
||||
type: "es.inquisition",
|
||||
content: {},
|
||||
}),
|
||||
);
|
||||
const spy = jest.fn();
|
||||
await sas.verify().catch(spy);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
|
||||
// Cancel the SAS for cleanup (we started a verification, so abort)
|
||||
sas.cancel(new Error('error'));
|
||||
sas.cancel(new Error("error"));
|
||||
});
|
||||
|
||||
describe("verification", () => {
|
||||
@@ -75,7 +77,7 @@ describe("SAS verification", function() {
|
||||
let bob: TestClient;
|
||||
let aliceSasEvent: ISasEvent | null;
|
||||
let bobSasEvent: ISasEvent | null;
|
||||
let aliceVerifier: Verification<any, any>;
|
||||
let aliceVerifier: SAS;
|
||||
let bobPromise: Promise<VerificationBase<any, any>>;
|
||||
let clearTestClientTimeouts: () => void;
|
||||
|
||||
@@ -95,38 +97,34 @@ describe("SAS verification", function() {
|
||||
|
||||
ALICE_DEVICES = {
|
||||
Osborne2: {
|
||||
user_id: "@alice:example.com",
|
||||
device_id: "Osborne2",
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
keys: {
|
||||
"ed25519:Osborne2": aliceDevice.deviceEd25519Key,
|
||||
"curve25519:Osborne2": aliceDevice.deviceCurve25519Key,
|
||||
"ed25519:Osborne2": aliceDevice.deviceEd25519Key!,
|
||||
"curve25519:Osborne2": aliceDevice.deviceCurve25519Key!,
|
||||
},
|
||||
verified: DeviceInfo.DeviceVerification.UNVERIFIED,
|
||||
known: false,
|
||||
},
|
||||
};
|
||||
|
||||
BOB_DEVICES = {
|
||||
Dynabook: {
|
||||
user_id: "@bob:example.com",
|
||||
device_id: "Dynabook",
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
keys: {
|
||||
"ed25519:Dynabook": bobDevice.deviceEd25519Key,
|
||||
"curve25519:Dynabook": bobDevice.deviceCurve25519Key,
|
||||
"ed25519:Dynabook": bobDevice.deviceEd25519Key!,
|
||||
"curve25519:Dynabook": bobDevice.deviceCurve25519Key!,
|
||||
},
|
||||
verified: DeviceInfo.DeviceVerification.UNVERIFIED,
|
||||
known: false,
|
||||
},
|
||||
};
|
||||
|
||||
alice.client.crypto!.deviceList.storeDevicesForUser(
|
||||
"@bob:example.com", BOB_DEVICES,
|
||||
);
|
||||
alice.client.crypto!.deviceList.storeDevicesForUser("@bob:example.com", BOB_DEVICES);
|
||||
alice.client.downloadKeys = () => {
|
||||
return Promise.resolve({});
|
||||
};
|
||||
|
||||
bob.client.crypto!.deviceList.storeDevicesForUser(
|
||||
"@alice:example.com", ALICE_DEVICES,
|
||||
);
|
||||
bob.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", ALICE_DEVICES);
|
||||
bob.client.downloadKeys = () => {
|
||||
return Promise.resolve({});
|
||||
};
|
||||
@@ -135,8 +133,8 @@ describe("SAS verification", function() {
|
||||
bobSasEvent = null;
|
||||
|
||||
bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => {
|
||||
bob.client.on(CryptoEvent.VerificationRequest, request => {
|
||||
request.verifier!.on("show_sas", (e) => {
|
||||
bob.client.on(CryptoEvent.VerificationRequest, (request) => {
|
||||
(<SAS>request.verifier!).on(SasEvent.ShowSas, (e) => {
|
||||
if (!e.sas.emoji || !e.sas.decimal) {
|
||||
e.cancel();
|
||||
} else if (!aliceSasEvent) {
|
||||
@@ -157,8 +155,10 @@ describe("SAS verification", function() {
|
||||
});
|
||||
|
||||
aliceVerifier = alice.client.beginKeyVerification(
|
||||
verificationMethods.SAS, bob.client.getUserId()!, bob.deviceId!,
|
||||
);
|
||||
verificationMethods.SAS,
|
||||
bob.client.getUserId()!,
|
||||
bob.deviceId!,
|
||||
) as SAS;
|
||||
aliceVerifier.on(SasEvent.ShowSas, (e) => {
|
||||
if (!e.sas.emoji || !e.sas.decimal) {
|
||||
e.cancel();
|
||||
@@ -177,10 +177,7 @@ describe("SAS verification", function() {
|
||||
});
|
||||
});
|
||||
afterEach(async () => {
|
||||
await Promise.all([
|
||||
alice.stop(),
|
||||
bob.stop(),
|
||||
]);
|
||||
await Promise.all([alice.stop(), bob.stop()]);
|
||||
|
||||
clearTestClientTimeouts();
|
||||
});
|
||||
@@ -189,23 +186,21 @@ describe("SAS verification", function() {
|
||||
let macMethod;
|
||||
let keyAgreement;
|
||||
const origSendToDevice = bob.client.sendToDevice.bind(bob.client);
|
||||
bob.client.sendToDevice = function(type, map) {
|
||||
bob.client.sendToDevice = function (type, map) {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!]
|
||||
.message_authentication_code;
|
||||
keyAgreement = map[alice.client.getUserId()!][alice.client.deviceId!]
|
||||
.key_agreement_protocol;
|
||||
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!].message_authentication_code;
|
||||
keyAgreement = map[alice.client.getUserId()!][alice.client.deviceId!].key_agreement_protocol;
|
||||
}
|
||||
return origSendToDevice(type, map);
|
||||
};
|
||||
|
||||
alice.httpBackend.when('POST', '/keys/query').respond(200, {
|
||||
alice.httpBackend.when("POST", "/keys/query").respond(200, {
|
||||
failures: {},
|
||||
device_keys: {
|
||||
"@bob:example.com": BOB_DEVICES,
|
||||
},
|
||||
});
|
||||
bob.httpBackend.when('POST', '/keys/query').respond(200, {
|
||||
bob.httpBackend.when("POST", "/keys/query").respond(200, {
|
||||
failures: {},
|
||||
device_keys: {
|
||||
"@alice:example.com": ALICE_DEVICES,
|
||||
@@ -224,11 +219,9 @@ describe("SAS verification", function() {
|
||||
expect(keyAgreement).toBe("curve25519-hkdf-sha256");
|
||||
|
||||
// make sure Alice and Bob verified each other
|
||||
const bobDevice
|
||||
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
|
||||
const bobDevice = await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
|
||||
expect(bobDevice?.isVerified()).toBeTruthy();
|
||||
const aliceDevice
|
||||
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
|
||||
const aliceDevice = await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
|
||||
expect(aliceDevice?.isVerified()).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -244,27 +237,27 @@ describe("SAS verification", function() {
|
||||
// has, since it is the same object. If this does not
|
||||
// happen, the verification will fail due to a hash
|
||||
// commitment mismatch.
|
||||
map[bob.client.getUserId()!][bob.client.deviceId!]
|
||||
.message_authentication_codes = ['hkdf-hmac-sha256'];
|
||||
map[bob.client.getUserId()!][bob.client.deviceId!].message_authentication_codes = [
|
||||
"hkdf-hmac-sha256",
|
||||
];
|
||||
}
|
||||
return aliceOrigSendToDevice(type, map);
|
||||
};
|
||||
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
|
||||
bob.client.sendToDevice = (type, map) => {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!]
|
||||
.message_authentication_code;
|
||||
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!].message_authentication_code;
|
||||
}
|
||||
return bobOrigSendToDevice(type, map);
|
||||
};
|
||||
|
||||
alice.httpBackend.when('POST', '/keys/query').respond(200, {
|
||||
alice.httpBackend.when("POST", "/keys/query").respond(200, {
|
||||
failures: {},
|
||||
device_keys: {
|
||||
"@bob:example.com": BOB_DEVICES,
|
||||
},
|
||||
});
|
||||
bob.httpBackend.when('POST', '/keys/query').respond(200, {
|
||||
bob.httpBackend.when("POST", "/keys/query").respond(200, {
|
||||
failures: {},
|
||||
device_keys: {
|
||||
"@alice:example.com": ALICE_DEVICES,
|
||||
@@ -280,11 +273,9 @@ describe("SAS verification", function() {
|
||||
|
||||
expect(macMethod).toBe("hkdf-hmac-sha256");
|
||||
|
||||
const bobDevice
|
||||
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
|
||||
const bobDevice = await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
|
||||
expect(bobDevice!.isVerified()).toBeTruthy();
|
||||
const aliceDevice
|
||||
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
|
||||
const aliceDevice = await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
|
||||
expect(aliceDevice!.isVerified()).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -300,27 +291,25 @@ describe("SAS verification", function() {
|
||||
// has, since it is the same object. If this does not
|
||||
// happen, the verification will fail due to a hash
|
||||
// commitment mismatch.
|
||||
map[bob.client.getUserId()!][bob.client.deviceId!]
|
||||
.message_authentication_codes = ['hmac-sha256'];
|
||||
map[bob.client.getUserId()!][bob.client.deviceId!].message_authentication_codes = ["hmac-sha256"];
|
||||
}
|
||||
return aliceOrigSendToDevice(type, map);
|
||||
};
|
||||
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
|
||||
bob.client.sendToDevice = (type, map) => {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!]
|
||||
.message_authentication_code;
|
||||
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!].message_authentication_code;
|
||||
}
|
||||
return bobOrigSendToDevice(type, map);
|
||||
};
|
||||
|
||||
alice.httpBackend.when('POST', '/keys/query').respond(200, {
|
||||
alice.httpBackend.when("POST", "/keys/query").respond(200, {
|
||||
failures: {},
|
||||
device_keys: {
|
||||
"@bob:example.com": BOB_DEVICES,
|
||||
},
|
||||
});
|
||||
bob.httpBackend.when('POST', '/keys/query').respond(200, {
|
||||
bob.httpBackend.when("POST", "/keys/query").respond(200, {
|
||||
failures: {},
|
||||
device_keys: {
|
||||
"@alice:example.com": ALICE_DEVICES,
|
||||
@@ -336,41 +325,33 @@ describe("SAS verification", function() {
|
||||
|
||||
expect(macMethod).toBe("hmac-sha256");
|
||||
|
||||
const bobDevice
|
||||
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
|
||||
const bobDevice = await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
|
||||
expect(bobDevice?.isVerified()).toBeTruthy();
|
||||
const aliceDevice
|
||||
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
|
||||
const aliceDevice = await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
|
||||
expect(aliceDevice?.isVerified()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should verify a cross-signing key", async () => {
|
||||
alice.httpBackend.when('POST', '/keys/device_signing/upload').respond(
|
||||
200, {},
|
||||
);
|
||||
alice.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {});
|
||||
alice.httpBackend.when("POST", "/keys/device_signing/upload").respond(200, {});
|
||||
alice.httpBackend.when("POST", "/keys/signatures/upload").respond(200, {});
|
||||
alice.httpBackend.flush(undefined, 2);
|
||||
await resetCrossSigningKeys(alice.client);
|
||||
bob.httpBackend.when('POST', '/keys/device_signing/upload').respond(200, {});
|
||||
bob.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {});
|
||||
bob.httpBackend.when("POST", "/keys/device_signing/upload").respond(200, {});
|
||||
bob.httpBackend.when("POST", "/keys/signatures/upload").respond(200, {});
|
||||
bob.httpBackend.flush(undefined, 2);
|
||||
|
||||
await resetCrossSigningKeys(bob.client);
|
||||
|
||||
bob.client.crypto!.deviceList.storeCrossSigningForUser(
|
||||
"@alice:example.com", {
|
||||
keys: alice.client.crypto!.crossSigningInfo.keys,
|
||||
crossSigningVerifiedBefore: false,
|
||||
firstUse: true,
|
||||
},
|
||||
);
|
||||
bob.client.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
keys: alice.client.crypto!.crossSigningInfo.keys,
|
||||
crossSigningVerifiedBefore: false,
|
||||
firstUse: true,
|
||||
});
|
||||
|
||||
const verifyProm = Promise.all([
|
||||
aliceVerifier.verify(),
|
||||
bobPromise.then((verifier) => {
|
||||
bob.httpBackend.when(
|
||||
'POST', '/keys/signatures/upload',
|
||||
).respond(200, {});
|
||||
bob.httpBackend.when("POST", "/keys/signatures/upload").respond(200, {});
|
||||
bob.httpBackend.flush(undefined, 1, 2000);
|
||||
return verifier.verify();
|
||||
}),
|
||||
@@ -378,9 +359,7 @@ describe("SAS verification", function() {
|
||||
|
||||
await verifyProm;
|
||||
|
||||
const bobDeviceTrust = alice.client.checkDeviceTrust(
|
||||
"@bob:example.com", "Dynabook",
|
||||
);
|
||||
const bobDeviceTrust = alice.client.checkDeviceTrust("@bob:example.com", "Dynabook");
|
||||
expect(bobDeviceTrust.isLocallyVerified()).toBeTruthy();
|
||||
expect(bobDeviceTrust.isCrossSigningVerified()).toBeFalsy();
|
||||
|
||||
@@ -388,15 +367,13 @@ describe("SAS verification", function() {
|
||||
expect(aliceTrust.isCrossSigningVerified()).toBeTruthy();
|
||||
expect(aliceTrust.isTofu()).toBeTruthy();
|
||||
|
||||
const aliceDeviceTrust = bob.client.checkDeviceTrust(
|
||||
"@alice:example.com", "Osborne2",
|
||||
);
|
||||
const aliceDeviceTrust = bob.client.checkDeviceTrust("@alice:example.com", "Osborne2");
|
||||
expect(aliceDeviceTrust.isLocallyVerified()).toBeTruthy();
|
||||
expect(aliceDeviceTrust.isCrossSigningVerified()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it("should send a cancellation message on error", async function() {
|
||||
it("should send a cancellation message on error", async function () {
|
||||
const [[alice, bob], clearTestClientTimeouts] = await makeTestClients(
|
||||
[
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
@@ -412,8 +389,8 @@ describe("SAS verification", function() {
|
||||
bob.client.downloadKeys = jest.fn().mockResolvedValue({});
|
||||
|
||||
const bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => {
|
||||
bob.client.on(CryptoEvent.VerificationRequest, request => {
|
||||
request.verifier!.on("show_sas", (e) => {
|
||||
bob.client.on(CryptoEvent.VerificationRequest, (request) => {
|
||||
(<SAS>request.verifier!).on(SasEvent.ShowSas, (e) => {
|
||||
e.mismatch();
|
||||
});
|
||||
resolve(request.verifier!);
|
||||
@@ -421,7 +398,9 @@ describe("SAS verification", function() {
|
||||
});
|
||||
|
||||
const aliceVerifier = alice.client.beginKeyVerification(
|
||||
verificationMethods.SAS, bob.client.getUserId()!, bob.client.deviceId!,
|
||||
verificationMethods.SAS,
|
||||
bob.client.getUserId()!,
|
||||
bob.client.deviceId!,
|
||||
);
|
||||
|
||||
const aliceSpy = jest.fn();
|
||||
@@ -432,26 +411,24 @@ describe("SAS verification", function() {
|
||||
]);
|
||||
expect(aliceSpy).toHaveBeenCalled();
|
||||
expect(bobSpy).toHaveBeenCalled();
|
||||
expect(alice.client.setDeviceVerified)
|
||||
.not.toHaveBeenCalled();
|
||||
expect(bob.client.setDeviceVerified)
|
||||
.not.toHaveBeenCalled();
|
||||
expect(alice.client.setDeviceVerified).not.toHaveBeenCalled();
|
||||
expect(bob.client.setDeviceVerified).not.toHaveBeenCalled();
|
||||
|
||||
alice.stop();
|
||||
bob.stop();
|
||||
clearTestClientTimeouts();
|
||||
});
|
||||
|
||||
describe("verification in DM", function() {
|
||||
let alice;
|
||||
let bob;
|
||||
let aliceSasEvent;
|
||||
let bobSasEvent;
|
||||
let aliceVerifier;
|
||||
let bobPromise;
|
||||
let clearTestClientTimeouts;
|
||||
describe("verification in DM", function () {
|
||||
let alice: TestClient;
|
||||
let bob: TestClient;
|
||||
let aliceSasEvent: ISasEvent | null;
|
||||
let bobSasEvent: ISasEvent | null;
|
||||
let aliceVerifier: SAS;
|
||||
let bobPromise: Promise<void>;
|
||||
let clearTestClientTimeouts: Function;
|
||||
|
||||
beforeEach(async function() {
|
||||
beforeEach(async function () {
|
||||
[[alice, bob], clearTestClientTimeouts] = await makeTestClients(
|
||||
[
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
@@ -477,7 +454,7 @@ describe("SAS verification", function() {
|
||||
);
|
||||
};
|
||||
alice.client.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
return Promise.resolve({});
|
||||
};
|
||||
|
||||
bob.client.crypto!.setDeviceVerification = jest.fn();
|
||||
@@ -495,16 +472,16 @@ describe("SAS verification", function() {
|
||||
return "bob+base64+ed25519+key";
|
||||
};
|
||||
bob.client.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
return Promise.resolve({});
|
||||
};
|
||||
|
||||
aliceSasEvent = null;
|
||||
bobSasEvent = null;
|
||||
|
||||
bobPromise = new Promise<void>((resolve, reject) => {
|
||||
bob.client.on("crypto.verification.request", async (request) => {
|
||||
const verifier = request.beginKeyVerification(SAS.NAME);
|
||||
verifier.on("show_sas", (e) => {
|
||||
bob.client.on(CryptoEvent.VerificationRequest, async (request) => {
|
||||
const verifier = request.beginKeyVerification(SAS.NAME) as SAS;
|
||||
verifier.on(SasEvent.ShowSas, (e) => {
|
||||
if (!e.sas.emoji || !e.sas.decimal) {
|
||||
e.cancel();
|
||||
} else if (!aliceSasEvent) {
|
||||
@@ -525,12 +502,10 @@ describe("SAS verification", function() {
|
||||
});
|
||||
});
|
||||
|
||||
const aliceRequest = await alice.client.requestVerificationDM(
|
||||
bob.client.getUserId(), "!room_id",
|
||||
);
|
||||
await aliceRequest.waitFor(r => r.started);
|
||||
aliceVerifier = aliceRequest.verifier;
|
||||
aliceVerifier.on("show_sas", (e) => {
|
||||
const aliceRequest = await alice.client.requestVerificationDM(bob.client.getUserId()!, "!room_id");
|
||||
await aliceRequest.waitFor((r) => r.started);
|
||||
aliceVerifier = aliceRequest.verifier! as SAS;
|
||||
aliceVerifier.on(SasEvent.ShowSas, (e) => {
|
||||
if (!e.sas.emoji || !e.sas.decimal) {
|
||||
e.cancel();
|
||||
} else if (!bobSasEvent) {
|
||||
@@ -547,40 +522,32 @@ describe("SAS verification", function() {
|
||||
}
|
||||
});
|
||||
});
|
||||
afterEach(async function() {
|
||||
await Promise.all([
|
||||
alice.stop(),
|
||||
bob.stop(),
|
||||
]);
|
||||
afterEach(async function () {
|
||||
await Promise.all([alice.stop(), bob.stop()]);
|
||||
|
||||
clearTestClientTimeouts();
|
||||
});
|
||||
|
||||
it("should verify a key", async function() {
|
||||
await Promise.all([
|
||||
aliceVerifier.verify(),
|
||||
bobPromise,
|
||||
]);
|
||||
it("should verify a key", async function () {
|
||||
await Promise.all([aliceVerifier.verify(), bobPromise]);
|
||||
|
||||
// make sure Alice and Bob verified each other
|
||||
expect(alice.client.crypto!.setDeviceVerification)
|
||||
.toHaveBeenCalledWith(
|
||||
bob.client.getUserId(),
|
||||
bob.client.deviceId,
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
{ "ed25519:Dynabook": "bob+base64+ed25519+key" },
|
||||
);
|
||||
expect(bob.client.crypto!.setDeviceVerification)
|
||||
.toHaveBeenCalledWith(
|
||||
alice.client.getUserId(),
|
||||
alice.client.deviceId,
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
{ "ed25519:Osborne2": "alice+base64+ed25519+key" },
|
||||
);
|
||||
expect(alice.client.crypto!.setDeviceVerification).toHaveBeenCalledWith(
|
||||
bob.client.getUserId(),
|
||||
bob.client.deviceId,
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
{ "ed25519:Dynabook": "bob+base64+ed25519+key" },
|
||||
);
|
||||
expect(bob.client.crypto!.setDeviceVerification).toHaveBeenCalledWith(
|
||||
alice.client.getUserId(),
|
||||
alice.client.deviceId,
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
{ "ed25519:Osborne2": "alice+base64+ed25519+key" },
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,28 +14,26 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import '../../../olm-loader';
|
||||
import { MatrixClient, MatrixEvent } from '../../../../src/matrix';
|
||||
import "../../../olm-loader";
|
||||
import { MatrixClient, MatrixEvent } from "../../../../src/matrix";
|
||||
import { encodeBase64 } from "../../../../src/crypto/olmlib";
|
||||
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';
|
||||
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();
|
||||
|
||||
// Private key for tests only
|
||||
const testKey = new Uint8Array([
|
||||
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82,
|
||||
0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef,
|
||||
0xae, 0xb1, 0x05, 0xc1, 0xe7, 0x62, 0x78, 0xa6,
|
||||
0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
|
||||
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82, 0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef, 0xae, 0xb1, 0x05,
|
||||
0xc1, 0xe7, 0x62, 0x78, 0xa6, 0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
|
||||
]);
|
||||
const testKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
|
||||
|
||||
describe("self-verifications", () => {
|
||||
beforeAll(function() {
|
||||
beforeAll(function () {
|
||||
return global.Olm.init();
|
||||
});
|
||||
|
||||
@@ -47,26 +45,22 @@ describe("self-verifications", () => {
|
||||
storeCrossSigningKeyCache: jest.fn(),
|
||||
};
|
||||
|
||||
const crossSigningInfo = new CrossSigningInfo(
|
||||
userId,
|
||||
{},
|
||||
cacheCallbacks,
|
||||
);
|
||||
const crossSigningInfo = new CrossSigningInfo(userId, {}, cacheCallbacks);
|
||||
crossSigningInfo.keys = {
|
||||
master: {
|
||||
keys: { X: testKeyPub },
|
||||
usage: [],
|
||||
user_id: 'user-id',
|
||||
user_id: "user-id",
|
||||
},
|
||||
self_signing: {
|
||||
keys: { X: testKeyPub },
|
||||
usage: [],
|
||||
user_id: 'user-id',
|
||||
user_id: "user-id",
|
||||
},
|
||||
user_signing: {
|
||||
keys: { X: testKeyPub },
|
||||
usage: [],
|
||||
user_id: 'user-id',
|
||||
user_id: "user-id",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -114,18 +108,15 @@ describe("self-verifications", () => {
|
||||
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls.length).toBe(3);
|
||||
expect(secretStorage.request.mock.calls.length).toBe(4);
|
||||
|
||||
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[0][1])
|
||||
.toEqual(testKey);
|
||||
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[1][1])
|
||||
.toEqual(testKey);
|
||||
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[0][1]).toEqual(testKey);
|
||||
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[1][1]).toEqual(testKey);
|
||||
|
||||
expect(storeSessionBackupPrivateKey.mock.calls[0][0])
|
||||
.toEqual(testKey);
|
||||
expect(storeSessionBackupPrivateKey.mock.calls[0][0]).toEqual(testKey);
|
||||
|
||||
expect(restoreKeyBackupWithCache).toHaveBeenCalled();
|
||||
|
||||
expect(result).toBeInstanceOf(Array);
|
||||
expect(result[0][0]).toBe(testKeyPub);
|
||||
expect(result[1][0]).toBe(testKeyPub);
|
||||
expect(result![0][0]).toBe(testKeyPub);
|
||||
expect(result![1][0]).toBe(testKeyPub);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,43 +15,51 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { TestClient } from '../../../TestClient';
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import { TestClient } from "../../../TestClient";
|
||||
import { IContent, MatrixEvent } from "../../../../src/models/event";
|
||||
import { IRoomTimelineData } from "../../../../src/models/event-timeline-set";
|
||||
import { Room, RoomEvent } from "../../../../src/models/room";
|
||||
import { logger } from '../../../../src/logger';
|
||||
import { MatrixClient, ClientEvent } from '../../../../src/client';
|
||||
import { logger } from "../../../../src/logger";
|
||||
import { MatrixClient, ClientEvent, ICreateClientOpts } from "../../../../src/client";
|
||||
|
||||
export async function makeTestClients(userInfos, options): Promise<[TestClient[], () => void]> {
|
||||
interface UserInfo {
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
}
|
||||
|
||||
export async function makeTestClients(
|
||||
userInfos: UserInfo[],
|
||||
options: Partial<ICreateClientOpts>,
|
||||
): Promise<[TestClient[], () => void]> {
|
||||
const clients: TestClient[] = [];
|
||||
const timeouts: ReturnType<typeof setTimeout>[] = [];
|
||||
const clientMap: Record<string, Record<string, MatrixClient>> = {};
|
||||
const makeSendToDevice = (matrixClient: MatrixClient): MatrixClient['sendToDevice'] => async (type, map) => {
|
||||
// logger.log(this.getUserId(), "sends", type, map);
|
||||
for (const [userId, devMap] of Object.entries(map)) {
|
||||
if (userId in clientMap) {
|
||||
for (const [deviceId, msg] of Object.entries(devMap)) {
|
||||
if (deviceId in clientMap[userId]) {
|
||||
const event = new MatrixEvent({
|
||||
sender: matrixClient.getUserId()!,
|
||||
type: type,
|
||||
content: msg,
|
||||
});
|
||||
const client = clientMap[userId][deviceId];
|
||||
const decryptionPromise = event.isEncrypted() ?
|
||||
event.attemptDecryption(client.crypto!) :
|
||||
Promise.resolve();
|
||||
const makeSendToDevice =
|
||||
(matrixClient: MatrixClient): MatrixClient["sendToDevice"] =>
|
||||
async (type, map) => {
|
||||
// logger.log(this.getUserId(), "sends", type, map);
|
||||
for (const [userId, devMap] of Object.entries(map)) {
|
||||
if (userId in clientMap) {
|
||||
for (const [deviceId, msg] of Object.entries(devMap)) {
|
||||
if (deviceId in clientMap[userId]) {
|
||||
const event = new MatrixEvent({
|
||||
sender: matrixClient.getUserId()!,
|
||||
type: type,
|
||||
content: msg,
|
||||
});
|
||||
const client = clientMap[userId][deviceId];
|
||||
const decryptionPromise = event.isEncrypted()
|
||||
? event.attemptDecryption(client.crypto!)
|
||||
: Promise.resolve();
|
||||
|
||||
decryptionPromise.then(
|
||||
() => client.emit(ClientEvent.ToDeviceEvent, event),
|
||||
);
|
||||
decryptionPromise.then(() => client.emit(ClientEvent.ToDeviceEvent, event));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
};
|
||||
const makeSendEvent = (matrixClient: MatrixClient) => (room, type, content) => {
|
||||
return {};
|
||||
};
|
||||
const makeSendEvent = (matrixClient: MatrixClient) => (room: string, type: string, content: IContent) => {
|
||||
// make up a unique ID as the event ID
|
||||
const eventId = "$" + matrixClient.makeTxnId();
|
||||
const rawEvent = {
|
||||
@@ -63,15 +71,17 @@ export async function makeTestClients(userInfos, options): Promise<[TestClient[]
|
||||
origin_server_ts: Date.now(),
|
||||
};
|
||||
const event = new MatrixEvent(rawEvent);
|
||||
const remoteEcho = new MatrixEvent(Object.assign({}, rawEvent, {
|
||||
unsigned: {
|
||||
transaction_id: matrixClient.makeTxnId(),
|
||||
},
|
||||
}));
|
||||
const remoteEcho = new MatrixEvent(
|
||||
Object.assign({}, rawEvent, {
|
||||
unsigned: {
|
||||
transaction_id: matrixClient.makeTxnId(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
for (const tc of clients) {
|
||||
const room = new Room('test', tc.client, tc.client.getUserId()!);
|
||||
const room = new Room("test", tc.client, tc.client.getUserId()!);
|
||||
const roomTimelineData = {} as unknown as IRoomTimelineData;
|
||||
if (tc.client === matrixClient) {
|
||||
logger.log("sending remote echo!!");
|
||||
@@ -88,22 +98,23 @@ export async function makeTestClients(userInfos, options): Promise<[TestClient[]
|
||||
};
|
||||
|
||||
for (const userInfo of userInfos) {
|
||||
let keys = {};
|
||||
let keys: Record<string, Uint8Array> = {};
|
||||
if (!options) options = {};
|
||||
if (!options.cryptoCallbacks) options.cryptoCallbacks = {};
|
||||
if (!options.cryptoCallbacks.saveCrossSigningKeys) {
|
||||
options.cryptoCallbacks.saveCrossSigningKeys = k => { keys = k; };
|
||||
options.cryptoCallbacks.getCrossSigningKey = typ => keys[typ];
|
||||
options.cryptoCallbacks.saveCrossSigningKeys = (k) => {
|
||||
keys = k;
|
||||
};
|
||||
// @ts-ignore tsc getting confused by overloads
|
||||
options.cryptoCallbacks.getCrossSigningKey = (typ) => keys[typ];
|
||||
}
|
||||
const testClient = new TestClient(
|
||||
userInfo.userId, userInfo.deviceId, undefined, undefined,
|
||||
options,
|
||||
);
|
||||
const testClient = new TestClient(userInfo.userId, userInfo.deviceId, undefined, undefined, options);
|
||||
if (!(userInfo.userId in clientMap)) {
|
||||
clientMap[userInfo.userId] = {};
|
||||
}
|
||||
clientMap[userInfo.userId][userInfo.deviceId] = testClient.client;
|
||||
testClient.client.sendToDevice = makeSendToDevice(testClient.client);
|
||||
// @ts-ignore tsc getting confused by overloads
|
||||
testClient.client.sendEvent = makeSendEvent(testClient.client);
|
||||
clients.push(testClient);
|
||||
}
|
||||
|
||||
@@ -13,12 +13,15 @@ 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 { VerificationRequest, READY_TYPE, START_TYPE, DONE_TYPE } from
|
||||
"../../../../src/crypto/verification/request/VerificationRequest";
|
||||
import {
|
||||
VerificationRequest,
|
||||
READY_TYPE,
|
||||
START_TYPE,
|
||||
DONE_TYPE,
|
||||
} from "../../../../src/crypto/verification/request/VerificationRequest";
|
||||
import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel";
|
||||
import { ToDeviceChannel } from
|
||||
"../../../../src/crypto/verification/request/ToDeviceChannel";
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import { ToDeviceChannel } from "../../../../src/crypto/verification/request/ToDeviceChannel";
|
||||
import { IContent, MatrixEvent } from "../../../../src/models/event";
|
||||
import { MatrixClient } from "../../../../src/client";
|
||||
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
|
||||
import { VerificationBase } from "../../../../src/crypto/verification/Base";
|
||||
@@ -30,26 +33,32 @@ type MockClient = MatrixClient & {
|
||||
function makeMockClient(userId: string, deviceId: string): MockClient {
|
||||
let counter = 1;
|
||||
let events: MatrixEvent[] = [];
|
||||
const deviceEvents = {};
|
||||
const deviceEvents: Record<string, Record<string, MatrixEvent[]>> = {};
|
||||
return {
|
||||
getUserId() { return userId; },
|
||||
getDeviceId() { return deviceId; },
|
||||
getUserId() {
|
||||
return userId;
|
||||
},
|
||||
getDeviceId() {
|
||||
return deviceId;
|
||||
},
|
||||
|
||||
sendEvent(roomId, type, content) {
|
||||
sendEvent(roomId: string, type: string, content: IContent) {
|
||||
counter = counter + 1;
|
||||
const eventId = `$${userId}-${deviceId}-${counter}`;
|
||||
events.push(new MatrixEvent({
|
||||
sender: userId,
|
||||
event_id: eventId,
|
||||
room_id: roomId,
|
||||
type,
|
||||
content,
|
||||
origin_server_ts: Date.now(),
|
||||
}));
|
||||
events.push(
|
||||
new MatrixEvent({
|
||||
sender: userId,
|
||||
event_id: eventId,
|
||||
room_id: roomId,
|
||||
type,
|
||||
content,
|
||||
origin_server_ts: Date.now(),
|
||||
}),
|
||||
);
|
||||
return Promise.resolve({ event_id: eventId });
|
||||
},
|
||||
|
||||
sendToDevice(type, msgMap) {
|
||||
sendToDevice(type: string, msgMap: Record<string, Record<string, IContent>>) {
|
||||
for (const userId of Object.keys(msgMap)) {
|
||||
const deviceMap = msgMap[userId];
|
||||
for (const deviceId of Object.keys(deviceMap)) {
|
||||
@@ -84,7 +93,7 @@ function makeMockClient(userId: string, deviceId: string): MockClient {
|
||||
}
|
||||
|
||||
const MOCK_METHOD = "mock-verify";
|
||||
class MockVerifier extends VerificationBase<'', any> {
|
||||
class MockVerifier extends VerificationBase<"", any> {
|
||||
public _channel;
|
||||
public _startEvent;
|
||||
constructor(
|
||||
@@ -111,7 +120,7 @@ class MockVerifier extends VerificationBase<'', any> {
|
||||
}
|
||||
}
|
||||
|
||||
async handleEvent(event) {
|
||||
async handleEvent(event: MatrixEvent) {
|
||||
if (event.getType() === DONE_TYPE && !this._startEvent) {
|
||||
await this._channel.send(DONE_TYPE, {});
|
||||
}
|
||||
@@ -122,12 +131,14 @@ class MockVerifier extends VerificationBase<'', any> {
|
||||
}
|
||||
}
|
||||
|
||||
function makeRemoteEcho(event) {
|
||||
return new MatrixEvent(Object.assign({}, event.event, {
|
||||
unsigned: {
|
||||
transaction_id: "abc",
|
||||
},
|
||||
}));
|
||||
function makeRemoteEcho(event: MatrixEvent) {
|
||||
return new MatrixEvent(
|
||||
Object.assign({}, event.event, {
|
||||
unsigned: {
|
||||
transaction_id: "abc",
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function distributeEvent(
|
||||
@@ -135,33 +146,26 @@ async function distributeEvent(
|
||||
theirRequest: VerificationRequest,
|
||||
event: MatrixEvent,
|
||||
): Promise<void> {
|
||||
await ownRequest.channel.handleEvent(
|
||||
makeRemoteEcho(event),
|
||||
ownRequest,
|
||||
true,
|
||||
);
|
||||
await ownRequest.channel.handleEvent(makeRemoteEcho(event), ownRequest, true);
|
||||
await theirRequest.channel.handleEvent(event, theirRequest, true);
|
||||
}
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("verification request unit tests", function() {
|
||||
it("transition from UNSENT to DONE through happy path", async function() {
|
||||
describe("verification request unit tests", function () {
|
||||
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");
|
||||
const verificationMethods = new Map(
|
||||
[[MOCK_METHOD, MockVerifier]],
|
||||
) as unknown as Map<string, typeof VerificationBase>;
|
||||
const verificationMethods = new Map([[MOCK_METHOD, MockVerifier]]) as unknown as Map<
|
||||
string,
|
||||
typeof VerificationBase
|
||||
>;
|
||||
const aliceRequest = new VerificationRequest(
|
||||
new InRoomChannel(alice, "!room", bob.getUserId()!),
|
||||
verificationMethods,
|
||||
alice,
|
||||
);
|
||||
const bobRequest = new VerificationRequest(
|
||||
new InRoomChannel(bob, "!room"),
|
||||
verificationMethods,
|
||||
bob,
|
||||
);
|
||||
const bobRequest = new VerificationRequest(new InRoomChannel(bob, "!room"), verificationMethods, bob);
|
||||
expect(aliceRequest.invalid).toBe(true);
|
||||
expect(bobRequest.invalid).toBe(true);
|
||||
|
||||
@@ -199,23 +203,23 @@ describe("verification request unit tests", function() {
|
||||
expect(bobRequest.done).toBe(true);
|
||||
});
|
||||
|
||||
it("methods only contains common methods", async function() {
|
||||
it("methods only contains common methods", async function () {
|
||||
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
||||
const bob = makeMockClient("@bob:matrix.tld", "device1");
|
||||
const aliceVerificationMethods = new Map(
|
||||
[["c", function() {}], ["a", function() {}]],
|
||||
) as unknown as Map<string, typeof VerificationBase>;
|
||||
const bobVerificationMethods = new Map(
|
||||
[["c", function() {}], ["b", function() {}]],
|
||||
) as unknown as Map<string, typeof VerificationBase>;
|
||||
const aliceVerificationMethods = new Map([
|
||||
["c", function () {}],
|
||||
["a", function () {}],
|
||||
]) as unknown as Map<string, typeof VerificationBase>;
|
||||
const bobVerificationMethods = new Map([
|
||||
["c", function () {}],
|
||||
["b", function () {}],
|
||||
]) as unknown as Map<string, typeof VerificationBase>;
|
||||
const aliceRequest = new VerificationRequest(
|
||||
new InRoomChannel(alice, "!room", bob.getUserId()!),
|
||||
aliceVerificationMethods, alice);
|
||||
const bobRequest = new VerificationRequest(
|
||||
new InRoomChannel(bob, "!room"),
|
||||
bobVerificationMethods,
|
||||
bob,
|
||||
aliceVerificationMethods,
|
||||
alice,
|
||||
);
|
||||
const bobRequest = new VerificationRequest(new InRoomChannel(bob, "!room"), bobVerificationMethods, bob);
|
||||
await aliceRequest.sendRequest();
|
||||
const [requestEvent] = alice.popEvents();
|
||||
await distributeEvent(aliceRequest, bobRequest, requestEvent);
|
||||
@@ -226,7 +230,7 @@ describe("verification request unit tests", function() {
|
||||
expect(bobRequest.methods).toStrictEqual(["c"]);
|
||||
});
|
||||
|
||||
it("other client accepting request puts it in observeOnly mode", async function() {
|
||||
it("other client accepting request puts it in observeOnly mode", async function () {
|
||||
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
||||
const bob1 = makeMockClient("@bob:matrix.tld", "device1");
|
||||
const bob2 = makeMockClient("@bob:matrix.tld", "device2");
|
||||
@@ -237,16 +241,8 @@ describe("verification request unit tests", function() {
|
||||
);
|
||||
await aliceRequest.sendRequest();
|
||||
const [requestEvent] = alice.popEvents();
|
||||
const bob1Request = new VerificationRequest(
|
||||
new InRoomChannel(bob1, "!room"),
|
||||
new Map(),
|
||||
bob1,
|
||||
);
|
||||
const bob2Request = new VerificationRequest(
|
||||
new InRoomChannel(bob2, "!room"),
|
||||
new Map(),
|
||||
bob2,
|
||||
);
|
||||
const bob1Request = new VerificationRequest(new InRoomChannel(bob1, "!room"), new Map(), bob1);
|
||||
const bob2Request = new VerificationRequest(new InRoomChannel(bob2, "!room"), new Map(), bob2);
|
||||
|
||||
await bob1Request.channel.handleEvent(requestEvent, bob1Request, true);
|
||||
await bob2Request.channel.handleEvent(requestEvent, bob2Request, true);
|
||||
@@ -258,12 +254,13 @@ describe("verification request unit tests", function() {
|
||||
expect(bob2Request.observeOnly).toBe(true);
|
||||
});
|
||||
|
||||
it("verify own device with to_device messages", async function() {
|
||||
it("verify own device with to_device messages", async function () {
|
||||
const bob1 = makeMockClient("@bob:matrix.tld", "device1");
|
||||
const bob2 = makeMockClient("@bob:matrix.tld", "device2");
|
||||
const verificationMethods = new Map(
|
||||
[[MOCK_METHOD, MockVerifier]],
|
||||
) as unknown as Map<string, typeof VerificationBase>;
|
||||
const verificationMethods = new Map([[MOCK_METHOD, MockVerifier]]) as unknown as Map<
|
||||
string,
|
||||
typeof VerificationBase
|
||||
>;
|
||||
const bob1Request = new VerificationRequest(
|
||||
new ToDeviceChannel(
|
||||
bob1,
|
||||
@@ -300,7 +297,7 @@ describe("verification request unit tests", function() {
|
||||
expect(bob2Request.done).toBe(true);
|
||||
});
|
||||
|
||||
it("request times out after 10 minutes", async function() {
|
||||
it("request times out after 10 minutes", async function () {
|
||||
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
||||
const bob = makeMockClient("@bob:matrix.tld", "device1");
|
||||
const aliceRequest = new VerificationRequest(
|
||||
@@ -318,7 +315,7 @@ describe("verification request unit tests", function() {
|
||||
expect(aliceRequest._cancellingUserId).toBe(alice.getUserId());
|
||||
});
|
||||
|
||||
it("request times out 2 minutes after receipt", async function() {
|
||||
it("request times out 2 minutes after receipt", async function () {
|
||||
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
||||
const bob = makeMockClient("@bob:matrix.tld", "device1");
|
||||
const aliceRequest = new VerificationRequest(
|
||||
@@ -328,11 +325,7 @@ describe("verification request unit tests", function() {
|
||||
);
|
||||
await aliceRequest.sendRequest();
|
||||
const [requestEvent] = alice.popEvents();
|
||||
const bobRequest = new VerificationRequest(
|
||||
new InRoomChannel(bob, "!room"),
|
||||
new Map(),
|
||||
bob,
|
||||
);
|
||||
const bobRequest = new VerificationRequest(new InRoomChannel(bob, "!room"), new Map(), bob);
|
||||
|
||||
await bobRequest.channel.handleEvent(requestEvent, bobRequest, true);
|
||||
|
||||
|
||||
+24
-20
@@ -23,13 +23,7 @@ limitations under the License.
|
||||
// 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 { WidgetApi, WidgetApiToWidgetAction, MatrixCapabilities, ITurnServer, IRoomEvent } from "matrix-widget-api";
|
||||
|
||||
import { createRoomWidgetClient, MsgType } from "../../src/matrix";
|
||||
import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "../../src/client";
|
||||
@@ -88,7 +82,9 @@ describe("RoomWidgetClient", () => {
|
||||
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",
|
||||
"org.matrix.rageshake_request",
|
||||
{ request_id: 123 },
|
||||
"!1:example.org",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -105,8 +101,8 @@ describe("RoomWidgetClient", () => {
|
||||
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));
|
||||
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 } }),
|
||||
@@ -118,7 +114,12 @@ describe("RoomWidgetClient", () => {
|
||||
// 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]);
|
||||
expect(
|
||||
room!
|
||||
.getLiveTimeline()
|
||||
.getEvents()
|
||||
.map((e) => e.getEffectiveEvent()),
|
||||
).toEqual([event]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -157,7 +158,10 @@ describe("RoomWidgetClient", () => {
|
||||
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",
|
||||
"org.example.foo",
|
||||
"bar",
|
||||
{ hello: "world" },
|
||||
"!1:example.org",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -166,8 +170,8 @@ describe("RoomWidgetClient", () => {
|
||||
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));
|
||||
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 } }),
|
||||
@@ -179,7 +183,7 @@ describe("RoomWidgetClient", () => {
|
||||
// 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);
|
||||
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
|
||||
});
|
||||
|
||||
it("backfills", async () => {
|
||||
@@ -195,7 +199,7 @@ describe("RoomWidgetClient", () => {
|
||||
|
||||
const room = client.getRoom("!1:example.org");
|
||||
expect(room).not.toBeNull();
|
||||
expect(room!.currentState.getStateEvents("org.example.foo", "bar").getEffectiveEvent()).toEqual(event);
|
||||
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -260,8 +264,8 @@ describe("RoomWidgetClient", () => {
|
||||
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));
|
||||
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 } }),
|
||||
@@ -308,7 +312,7 @@ describe("RoomWidgetClient", () => {
|
||||
};
|
||||
|
||||
let emitServer2: () => void;
|
||||
const getServer2 = new Promise<ITurnServer>(resolve => emitServer2 = () => resolve(server2));
|
||||
const getServer2 = new Promise<ITurnServer>((resolve) => (emitServer2 = () => resolve(server2)));
|
||||
widgetApi.getTurnServers.mockImplementation(async function* () {
|
||||
yield server1;
|
||||
yield await getServer2;
|
||||
@@ -321,7 +325,7 @@ describe("RoomWidgetClient", () => {
|
||||
expect(client.getTurnServers()).toEqual([clientServer1]);
|
||||
|
||||
// Subsequent servers arrive asynchronously and should emit an event
|
||||
const emittedServer = new Promise<IClientTurnServer[]>(resolve =>
|
||||
const emittedServer = new Promise<IClientTurnServer[]>((resolve) =>
|
||||
client.once(ClientEvent.TurnServers, resolve),
|
||||
);
|
||||
emitServer2!();
|
||||
|
||||
@@ -18,7 +18,7 @@ import { MatrixClient, MatrixEvent, MatrixEventEvent, MatrixScheduler, Room } fr
|
||||
import { eventMapperFor } from "../../src/event-mapper";
|
||||
import { IStore } from "../../src/store";
|
||||
|
||||
describe("eventMapperFor", function() {
|
||||
describe("eventMapperFor", function () {
|
||||
let rooms: Room[] = [];
|
||||
|
||||
const userId = "@test:example.org";
|
||||
@@ -29,10 +29,10 @@ describe("eventMapperFor", function() {
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
accessToken: "my.access.token",
|
||||
fetchFn: function() {} as any, // NOP
|
||||
fetchFn: function () {} as any, // NOP
|
||||
store: {
|
||||
getRoom(roomId: string): Room | null {
|
||||
return rooms.find(r => r.roomId === roomId) ?? null;
|
||||
return rooms.find((r) => r.roomId === roomId) ?? null;
|
||||
},
|
||||
} as IStore,
|
||||
scheduler: {
|
||||
|
||||
@@ -24,13 +24,16 @@ import {
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
RelationType,
|
||||
Room,
|
||||
} from '../../src';
|
||||
import { Thread } from "../../src/models/thread";
|
||||
RoomEvent,
|
||||
} from "../../src";
|
||||
import { FeatureSupport, Thread } from "../../src/models/thread";
|
||||
import { ReEmitter } from "../../src/ReEmitter";
|
||||
import { eventMapperFor } from "../../src/event-mapper";
|
||||
|
||||
describe('EventTimelineSet', () => {
|
||||
const roomId = '!foo:bar';
|
||||
describe("EventTimelineSet", () => {
|
||||
const roomId = "!foo:bar";
|
||||
const userA = "@alice:bar";
|
||||
|
||||
let room: Room;
|
||||
@@ -42,7 +45,7 @@ describe('EventTimelineSet', () => {
|
||||
let replyEvent: MatrixEvent;
|
||||
|
||||
const itShouldReturnTheRelatedEvents = () => {
|
||||
it('should return the related events', () => {
|
||||
it("should return the related events", () => {
|
||||
eventTimelineSet.relations.aggregateChildEvent(messageEvent);
|
||||
const relations = eventTimelineSet.relations.getChildEventsForEvent(
|
||||
messageEvent.getId()!,
|
||||
@@ -55,45 +58,49 @@ describe('EventTimelineSet', () => {
|
||||
});
|
||||
};
|
||||
|
||||
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(),
|
||||
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",
|
||||
},
|
||||
},
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
},
|
||||
}, room.client);
|
||||
room.client,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
client = utils.mock(MatrixClient, 'MatrixClient');
|
||||
client.reEmitter = utils.mock(ReEmitter, 'ReEmitter');
|
||||
client = utils.mock(MatrixClient, "MatrixClient");
|
||||
client.reEmitter = utils.mock(ReEmitter, "ReEmitter");
|
||||
room = new Room(roomId, client, userA);
|
||||
eventTimelineSet = new EventTimelineSet(room);
|
||||
eventTimeline = new EventTimeline(eventTimelineSet);
|
||||
messageEvent = utils.mkMessage({
|
||||
room: roomId,
|
||||
user: userA,
|
||||
msg: 'Hi!',
|
||||
msg: "Hi!",
|
||||
event: true,
|
||||
});
|
||||
replyEvent = utils.mkReplyMessage({
|
||||
room: roomId,
|
||||
user: userA,
|
||||
msg: 'Hoo!',
|
||||
msg: "Hoo!",
|
||||
event: true,
|
||||
replyToMessage: messageEvent,
|
||||
});
|
||||
});
|
||||
|
||||
describe('addLiveEvent', () => {
|
||||
describe("addLiveEvent", () => {
|
||||
it("Adds event to the live timeline in the timeline set", () => {
|
||||
const liveTimeline = eventTimelineSet.getLiveTimeline();
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
@@ -111,7 +118,10 @@ describe('EventTimelineSet', () => {
|
||||
|
||||
// make a duplicate
|
||||
const duplicateMessageEvent = utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "dupe", event: true,
|
||||
room: roomId,
|
||||
user: userA,
|
||||
msg: "dupe",
|
||||
event: true,
|
||||
});
|
||||
duplicateMessageEvent.event.event_id = messageEvent.getId();
|
||||
|
||||
@@ -133,7 +143,7 @@ describe('EventTimelineSet', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('addEventToTimeline', () => {
|
||||
describe("addEventToTimeline", () => {
|
||||
let thread: Thread;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -153,19 +163,10 @@ describe('EventTimelineSet', () => {
|
||||
it("Make sure legacy overload passing options directly as parameters still works", () => {
|
||||
const liveTimeline = eventTimelineSet.getLiveTimeline();
|
||||
expect(() => {
|
||||
eventTimelineSet.addEventToTimeline(
|
||||
messageEvent,
|
||||
liveTimeline,
|
||||
true,
|
||||
);
|
||||
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, true);
|
||||
}).not.toThrow();
|
||||
expect(() => {
|
||||
eventTimelineSet.addEventToTimeline(
|
||||
messageEvent,
|
||||
liveTimeline,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, true, false);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
@@ -204,8 +205,90 @@ describe('EventTimelineSet', () => {
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
});
|
||||
|
||||
describe('non-room timeline', () => {
|
||||
it('Adds event to timeline', () => {
|
||||
it("should allow edits to be added to thread timeline", async () => {
|
||||
jest.spyOn(client, "supportsExperimentalThreads").mockReturnValue(true);
|
||||
jest.spyOn(client, "getEventMapper").mockReturnValue(eventMapperFor(client, {}));
|
||||
Thread.hasServerSideSupport = FeatureSupport.Stable;
|
||||
|
||||
const sender = "@alice:matrix.org";
|
||||
|
||||
const root = utils.mkEvent({
|
||||
event: true,
|
||||
content: {
|
||||
body: "Thread root",
|
||||
},
|
||||
type: EventType.RoomMessage,
|
||||
sender,
|
||||
});
|
||||
room.addLiveEvents([root]);
|
||||
|
||||
const threadReply = utils.mkEvent({
|
||||
event: true,
|
||||
content: {
|
||||
"body": "Thread reply",
|
||||
"m.relates_to": {
|
||||
event_id: root.getId()!,
|
||||
rel_type: RelationType.Thread,
|
||||
},
|
||||
},
|
||||
type: EventType.RoomMessage,
|
||||
sender,
|
||||
});
|
||||
|
||||
root.setUnsigned({
|
||||
"m.relations": {
|
||||
[RelationType.Thread]: {
|
||||
count: 1,
|
||||
latest_event: {
|
||||
content: threadReply.getContent(),
|
||||
origin_server_ts: 5,
|
||||
room_id: room.roomId,
|
||||
sender,
|
||||
type: EventType.RoomMessage,
|
||||
event_id: threadReply.getId()!,
|
||||
user_id: sender,
|
||||
age: 1,
|
||||
},
|
||||
current_user_participated: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const editToThreadReply = utils.mkEvent({
|
||||
event: true,
|
||||
content: {
|
||||
"body": " * edit",
|
||||
"m.new_content": {
|
||||
"body": "edit",
|
||||
"msgtype": "m.text",
|
||||
"org.matrix.msc1767.text": "edit",
|
||||
},
|
||||
"m.relates_to": {
|
||||
event_id: threadReply.getId()!,
|
||||
rel_type: RelationType.Replace,
|
||||
},
|
||||
},
|
||||
type: EventType.RoomMessage,
|
||||
sender,
|
||||
});
|
||||
|
||||
jest.spyOn(client, "paginateEventTimeline").mockImplementation(async () => {
|
||||
thread.timelineSet.getLiveTimeline().addEvent(threadReply, { toStartOfTimeline: true });
|
||||
return true;
|
||||
});
|
||||
jest.spyOn(client, "relations").mockResolvedValue({
|
||||
events: [],
|
||||
});
|
||||
|
||||
const thread = room.createThread(root.getId()!, root, [threadReply, editToThreadReply], false);
|
||||
thread.once(RoomEvent.TimelineReset, () => {
|
||||
const lastEvent = thread.timeline.at(-1)!;
|
||||
expect(lastEvent.getContent().body).toBe(" * edit");
|
||||
});
|
||||
});
|
||||
|
||||
describe("non-room timeline", () => {
|
||||
it("Adds event to timeline", () => {
|
||||
const nonRoomEventTimelineSet = new EventTimelineSet(
|
||||
// This is what we're specifically testing against, a timeline
|
||||
// without a `room` defined
|
||||
@@ -222,24 +305,16 @@ describe('EventTimelineSet', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('aggregateRelations', () => {
|
||||
describe('with unencrypted events', () => {
|
||||
describe("aggregateRelations", () => {
|
||||
describe("with unencrypted events", () => {
|
||||
beforeEach(() => {
|
||||
eventTimelineSet.addEventsToTimeline(
|
||||
[
|
||||
messageEvent,
|
||||
replyEvent,
|
||||
],
|
||||
true,
|
||||
eventTimeline,
|
||||
'foo',
|
||||
);
|
||||
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, eventTimeline, "foo");
|
||||
});
|
||||
|
||||
itShouldReturnTheRelatedEvents();
|
||||
});
|
||||
|
||||
describe('with events to be decrypted', () => {
|
||||
describe("with events to be decrypted", () => {
|
||||
let messageEventShouldAttemptDecryptionSpy: jest.SpyInstance;
|
||||
let messageEventIsDecryptionFailureSpy: jest.SpyInstance;
|
||||
|
||||
@@ -247,26 +322,18 @@ describe('EventTimelineSet', () => {
|
||||
let replyEventIsDecryptionFailureSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
messageEventShouldAttemptDecryptionSpy = jest.spyOn(messageEvent, 'shouldAttemptDecryption');
|
||||
messageEventShouldAttemptDecryptionSpy = jest.spyOn(messageEvent, "shouldAttemptDecryption");
|
||||
messageEventShouldAttemptDecryptionSpy.mockReturnValue(true);
|
||||
messageEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, 'isDecryptionFailure');
|
||||
messageEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, "isDecryptionFailure");
|
||||
|
||||
replyEventShouldAttemptDecryptionSpy = jest.spyOn(replyEvent, 'shouldAttemptDecryption');
|
||||
replyEventShouldAttemptDecryptionSpy = jest.spyOn(replyEvent, "shouldAttemptDecryption");
|
||||
replyEventShouldAttemptDecryptionSpy.mockReturnValue(true);
|
||||
replyEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, 'isDecryptionFailure');
|
||||
replyEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, "isDecryptionFailure");
|
||||
|
||||
eventTimelineSet.addEventsToTimeline(
|
||||
[
|
||||
messageEvent,
|
||||
replyEvent,
|
||||
],
|
||||
true,
|
||||
eventTimeline,
|
||||
'foo',
|
||||
);
|
||||
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, eventTimeline, "foo");
|
||||
});
|
||||
|
||||
it('should not return the related events', () => {
|
||||
it("should not return the related events", () => {
|
||||
eventTimelineSet.relations.aggregateChildEvent(messageEvent);
|
||||
const relations = eventTimelineSet.relations.getChildEventsForEvent(
|
||||
messageEvent.getId()!,
|
||||
@@ -276,7 +343,7 @@ describe('EventTimelineSet', () => {
|
||||
expect(relations).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('after decryption', () => {
|
||||
describe("after decryption", () => {
|
||||
beforeEach(() => {
|
||||
// simulate decryption failure once
|
||||
messageEventIsDecryptionFailureSpy.mockReturnValue(true);
|
||||
@@ -302,22 +369,26 @@ describe('EventTimelineSet', () => {
|
||||
});
|
||||
|
||||
describe("canContain", () => {
|
||||
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()!,
|
||||
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",
|
||||
},
|
||||
},
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
},
|
||||
}, room.client);
|
||||
room.client,
|
||||
);
|
||||
|
||||
let thread: Thread;
|
||||
|
||||
|
||||
+210
-167
@@ -1,4 +1,4 @@
|
||||
import { mocked } from 'jest-mock';
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { Direction, EventTimeline } from "../../src/models/event-timeline";
|
||||
@@ -8,7 +8,7 @@ import { Room } from "../../src/models/room";
|
||||
import { RoomMember } from "../../src/models/room-member";
|
||||
import { EventTimelineSet } from "../../src/models/event-timeline-set";
|
||||
|
||||
describe("EventTimeline", function() {
|
||||
describe("EventTimeline", function () {
|
||||
const roomId = "!foo:bar";
|
||||
const userA = "@alice:bar";
|
||||
const userB = "@bertha:bar";
|
||||
@@ -19,7 +19,7 @@ describe("EventTimeline", function() {
|
||||
const getTimeline = (): EventTimeline => {
|
||||
const room = new Room(roomId, mockClient, userA);
|
||||
const timelineSet = new EventTimelineSet(room);
|
||||
jest.spyOn(room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet);
|
||||
jest.spyOn(room, "getUnfilteredTimelineSet").mockReturnValue(timelineSet);
|
||||
|
||||
const timeline = new EventTimeline(timelineSet);
|
||||
// We manually stub the methods we'll be mocking out later instead of mocking the whole module
|
||||
@@ -31,29 +31,34 @@ describe("EventTimeline", function() {
|
||||
return timeline;
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
// reset any RoomState mocks
|
||||
jest.resetAllMocks();
|
||||
|
||||
timeline = getTimeline();
|
||||
});
|
||||
|
||||
describe("construction", function() {
|
||||
it("getRoomId should get room id", function() {
|
||||
describe("construction", function () {
|
||||
it("getRoomId should get room id", function () {
|
||||
const v = timeline.getRoomId();
|
||||
expect(v).toEqual(roomId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("initialiseState", function() {
|
||||
it("should copy state events to start and end state", function() {
|
||||
describe("initialiseState", function () {
|
||||
it("should copy state events to start and end state", function () {
|
||||
const events = [
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "invite", user: userB, skey: userA,
|
||||
room: roomId,
|
||||
mship: "invite",
|
||||
user: userB,
|
||||
skey: userA,
|
||||
event: true,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: userB,
|
||||
type: "m.room.name",
|
||||
room: roomId,
|
||||
user: userB,
|
||||
event: true,
|
||||
content: { name: "New room" },
|
||||
}),
|
||||
@@ -61,49 +66,51 @@ describe("EventTimeline", function() {
|
||||
timeline.initialiseState(events);
|
||||
// @ts-ignore private prop
|
||||
const timelineStartState = timeline.startState!;
|
||||
expect(mocked(timelineStartState).setStateEvents).toHaveBeenCalledWith(
|
||||
events,
|
||||
{ timelineWasEmpty: undefined },
|
||||
);
|
||||
expect(mocked(timelineStartState).setStateEvents).toHaveBeenCalledWith(events, {
|
||||
timelineWasEmpty: undefined,
|
||||
});
|
||||
// @ts-ignore private prop
|
||||
const timelineEndState = timeline.endState!;
|
||||
expect(mocked(timelineEndState).setStateEvents).toHaveBeenCalledWith(
|
||||
events,
|
||||
{ timelineWasEmpty: undefined },
|
||||
);
|
||||
expect(mocked(timelineEndState).setStateEvents).toHaveBeenCalledWith(events, {
|
||||
timelineWasEmpty: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("should raise an exception if called after events are added", function() {
|
||||
const event =
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "Adam stole the plushies",
|
||||
event: true,
|
||||
});
|
||||
it("should raise an exception if called after events are added", function () {
|
||||
const event = utils.mkMessage({
|
||||
room: roomId,
|
||||
user: userA,
|
||||
msg: "Adam stole the plushies",
|
||||
event: true,
|
||||
});
|
||||
|
||||
const state = [
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "invite", user: userB, skey: userA,
|
||||
room: roomId,
|
||||
mship: "invite",
|
||||
user: userB,
|
||||
skey: userA,
|
||||
event: true,
|
||||
}),
|
||||
];
|
||||
|
||||
expect(function() {
|
||||
expect(function () {
|
||||
timeline.initialiseState(state);
|
||||
}).not.toThrow();
|
||||
timeline.addEvent(event, { toStartOfTimeline: false });
|
||||
expect(function() {
|
||||
expect(function () {
|
||||
timeline.initialiseState(state);
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("paginationTokens", function() {
|
||||
it("pagination tokens should start null", function() {
|
||||
describe("paginationTokens", function () {
|
||||
it("pagination tokens should start null", function () {
|
||||
expect(timeline.getPaginationToken(EventTimeline.BACKWARDS)).toBe(null);
|
||||
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");
|
||||
@@ -121,13 +128,13 @@ describe("EventTimeline", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("neighbouringTimelines", function() {
|
||||
it("neighbouring timelines should start null", function() {
|
||||
describe("neighbouringTimelines", function () {
|
||||
it("neighbouring timelines should start null", function () {
|
||||
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(null);
|
||||
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS)).toBe(null);
|
||||
});
|
||||
|
||||
it("setNeighbouringTimeline should set neighbour", function() {
|
||||
it("setNeighbouringTimeline should set neighbour", function () {
|
||||
const prev = getTimeline();
|
||||
const next = getTimeline();
|
||||
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
|
||||
@@ -136,42 +143,44 @@ describe("EventTimeline", function() {
|
||||
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS)).toBe(next);
|
||||
});
|
||||
|
||||
it("setNeighbouringTimeline should throw if called twice", function() {
|
||||
it("setNeighbouringTimeline should throw if called twice", function () {
|
||||
const prev = getTimeline();
|
||||
const next = getTimeline();
|
||||
expect(function() {
|
||||
expect(function () {
|
||||
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
|
||||
}).not.toThrow();
|
||||
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS))
|
||||
.toBe(prev);
|
||||
expect(function() {
|
||||
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(prev);
|
||||
expect(function () {
|
||||
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
|
||||
}).toThrow();
|
||||
|
||||
expect(function() {
|
||||
expect(function () {
|
||||
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
|
||||
}).not.toThrow();
|
||||
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS))
|
||||
.toBe(next);
|
||||
expect(function() {
|
||||
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS)).toBe(next);
|
||||
expect(function () {
|
||||
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("addEvent", function() {
|
||||
describe("addEvent", function () {
|
||||
const events = [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "hungry hungry hungry",
|
||||
room: roomId,
|
||||
user: userA,
|
||||
msg: "hungry hungry hungry",
|
||||
event: true,
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userB, msg: "nom nom nom",
|
||||
room: roomId,
|
||||
user: userB,
|
||||
msg: "nom nom nom",
|
||||
event: true,
|
||||
}),
|
||||
];
|
||||
|
||||
it("should be able to add events to the end", function() {
|
||||
it("should be able to add events to the end", function () {
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: false });
|
||||
const initialIndex = timeline.getBaseIndex();
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: false });
|
||||
@@ -181,7 +190,7 @@ describe("EventTimeline", function() {
|
||||
expect(timeline.getEvents()[1]).toEqual(events[1]);
|
||||
});
|
||||
|
||||
it("should be able to add events to the start", function() {
|
||||
it("should be able to add events to the start", function () {
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: true });
|
||||
const initialIndex = timeline.getBaseIndex();
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: true });
|
||||
@@ -191,7 +200,7 @@ describe("EventTimeline", function() {
|
||||
expect(timeline.getEvents()[1]).toEqual(events[0]);
|
||||
});
|
||||
|
||||
it("should set event.sender for new and old events", function() {
|
||||
it("should set event.sender for new and old events", function () {
|
||||
const sentinel = new RoomMember(roomId, userA);
|
||||
sentinel.name = "Alice";
|
||||
sentinel.membership = "join";
|
||||
@@ -200,27 +209,31 @@ describe("EventTimeline", function() {
|
||||
sentinel.name = "Old Alice";
|
||||
sentinel.membership = "join";
|
||||
|
||||
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const newEv = utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: userA, event: true,
|
||||
type: "m.room.name",
|
||||
room: roomId,
|
||||
user: userA,
|
||||
event: true,
|
||||
content: { name: "New Room Name" },
|
||||
});
|
||||
const oldEv = utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: userA, event: true,
|
||||
type: "m.room.name",
|
||||
room: roomId,
|
||||
user: userA,
|
||||
event: true,
|
||||
content: { name: "Old Room Name" },
|
||||
});
|
||||
|
||||
@@ -230,128 +243,159 @@ describe("EventTimeline", function() {
|
||||
expect(oldEv.sender).toEqual(oldSentinel);
|
||||
});
|
||||
|
||||
it("should set event.target for new and old m.room.member events",
|
||||
function() {
|
||||
const sentinel = new RoomMember(roomId, userA);
|
||||
sentinel.name = "Alice";
|
||||
sentinel.membership = "join";
|
||||
it("should set event.target for new and old m.room.member events", function () {
|
||||
const sentinel = new RoomMember(roomId, userA);
|
||||
sentinel.name = "Alice";
|
||||
sentinel.membership = "join";
|
||||
|
||||
const oldSentinel = new RoomMember(roomId, userA);
|
||||
sentinel.name = "Old Alice";
|
||||
sentinel.membership = "join";
|
||||
const oldSentinel = new RoomMember(roomId, userA);
|
||||
sentinel.name = "Old Alice";
|
||||
sentinel.membership = "join";
|
||||
|
||||
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const newEv = utils.mkMembership({
|
||||
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
|
||||
});
|
||||
const oldEv = utils.mkMembership({
|
||||
room: roomId, mship: "ban", user: userB, skey: userA, event: true,
|
||||
});
|
||||
timeline.addEvent(newEv, { toStartOfTimeline: false });
|
||||
expect(newEv.target).toEqual(sentinel);
|
||||
timeline.addEvent(oldEv, { toStartOfTimeline: true });
|
||||
expect(oldEv.target).toEqual(oldSentinel);
|
||||
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
it("should call setStateEvents on the right RoomState with the right " +
|
||||
"forwardLooking value for new events", function() {
|
||||
const events = [
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: userB, event: true,
|
||||
content: {
|
||||
name: "New room",
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: false });
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: false });
|
||||
|
||||
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).
|
||||
toHaveBeenCalledWith([events[0]], { timelineWasEmpty: undefined });
|
||||
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).
|
||||
not.toHaveBeenCalled();
|
||||
const newEv = utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "invite",
|
||||
user: userB,
|
||||
skey: userA,
|
||||
event: true,
|
||||
});
|
||||
const oldEv = utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "ban",
|
||||
user: userB,
|
||||
skey: userA,
|
||||
event: true,
|
||||
});
|
||||
timeline.addEvent(newEv, { toStartOfTimeline: false });
|
||||
expect(newEv.target).toEqual(sentinel);
|
||||
timeline.addEvent(oldEv, { toStartOfTimeline: true });
|
||||
expect(oldEv.target).toEqual(oldSentinel);
|
||||
});
|
||||
|
||||
it("should call setStateEvents on the right RoomState with the right " +
|
||||
"forwardLooking value for old events", function() {
|
||||
const events = [
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: userB, event: true,
|
||||
content: {
|
||||
name: "New room",
|
||||
},
|
||||
}),
|
||||
];
|
||||
it(
|
||||
"should call setStateEvents on the right RoomState with the right " + "forwardLooking value for new events",
|
||||
function () {
|
||||
const events = [
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "invite",
|
||||
user: userB,
|
||||
skey: userA,
|
||||
event: true,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.name",
|
||||
room: roomId,
|
||||
user: userB,
|
||||
event: true,
|
||||
content: {
|
||||
name: "New room",
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: true });
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: true });
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: false });
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: false });
|
||||
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).
|
||||
toHaveBeenCalledWith([events[0]], { timelineWasEmpty: undefined });
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).
|
||||
toHaveBeenCalledWith([events[1]], { timelineWasEmpty: undefined });
|
||||
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).toHaveBeenCalledWith([events[0]], {
|
||||
timelineWasEmpty: undefined,
|
||||
});
|
||||
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).toHaveBeenCalledWith([events[1]], {
|
||||
timelineWasEmpty: undefined,
|
||||
});
|
||||
|
||||
expect(events[0].forwardLooking).toBe(false);
|
||||
expect(events[1].forwardLooking).toBe(false);
|
||||
expect(events[0].forwardLooking).toBe(true);
|
||||
expect(events[1].forwardLooking).toBe(true);
|
||||
|
||||
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).
|
||||
not.toHaveBeenCalled();
|
||||
});
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
"should call setStateEvents on the right RoomState with the right " + "forwardLooking value for old events",
|
||||
function () {
|
||||
const events = [
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "invite",
|
||||
user: userB,
|
||||
skey: userA,
|
||||
event: true,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.name",
|
||||
room: roomId,
|
||||
user: userB,
|
||||
event: true,
|
||||
content: {
|
||||
name: "New room",
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: true });
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: true });
|
||||
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).toHaveBeenCalledWith([events[0]], {
|
||||
timelineWasEmpty: undefined,
|
||||
});
|
||||
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).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it("Make sure legacy overload passing options directly as parameters still works", () => {
|
||||
expect(() => timeline.addEvent(events[0], { toStartOfTimeline: true })).not.toThrow();
|
||||
// @ts-ignore stateContext is not a valid param
|
||||
expect(() => timeline.addEvent(events[0], { stateContext: new RoomState(roomId) })).not.toThrow();
|
||||
expect(() => timeline.addEvent(events[0],
|
||||
{ toStartOfTimeline: false, roomState: new RoomState(roomId) },
|
||||
)).not.toThrow();
|
||||
expect(() =>
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: false, roomState: new RoomState(roomId) }),
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeEvent", function() {
|
||||
describe("removeEvent", function () {
|
||||
const events = [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "hungry hungry hungry",
|
||||
room: roomId,
|
||||
user: userA,
|
||||
msg: "hungry hungry hungry",
|
||||
event: true,
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userB, msg: "nom nom nom",
|
||||
room: roomId,
|
||||
user: userB,
|
||||
msg: "nom nom nom",
|
||||
event: true,
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userB, msg: "piiie",
|
||||
room: roomId,
|
||||
user: userB,
|
||||
msg: "piiie",
|
||||
event: true,
|
||||
}),
|
||||
];
|
||||
|
||||
it("should remove events", function() {
|
||||
it("should remove events", function () {
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: false });
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: false });
|
||||
expect(timeline.getEvents().length).toEqual(2);
|
||||
@@ -365,7 +409,7 @@ describe("EventTimeline", function() {
|
||||
expect(timeline.getEvents().length).toEqual(0);
|
||||
});
|
||||
|
||||
it("should update baseIndex", function() {
|
||||
it("should update baseIndex", function () {
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: false });
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: true });
|
||||
timeline.addEvent(events[2], { toStartOfTimeline: false });
|
||||
@@ -384,15 +428,14 @@ describe("EventTimeline", function() {
|
||||
// this is basically https://github.com/vector-im/vector-web/issues/937
|
||||
// - removing the last event got baseIndex into such a state that
|
||||
// further addEvent(ev, false) calls made the index increase.
|
||||
it("should not make baseIndex assplode when removing the last event",
|
||||
function() {
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: true });
|
||||
timeline.removeEvent(events[0].getId()!);
|
||||
const initialIndex = timeline.getBaseIndex();
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: false });
|
||||
timeline.addEvent(events[2], { toStartOfTimeline: false });
|
||||
expect(timeline.getBaseIndex()).toEqual(initialIndex);
|
||||
expect(timeline.getEvents().length).toEqual(2);
|
||||
});
|
||||
it("should not make baseIndex assplode when removing the last event", function () {
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: true });
|
||||
timeline.removeEvent(events[0].getId()!);
|
||||
const initialIndex = timeline.getBaseIndex();
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: false });
|
||||
timeline.addEvent(events[2], { toStartOfTimeline: false });
|
||||
expect(timeline.getBaseIndex()).toEqual(initialIndex);
|
||||
expect(timeline.getEvents().length).toEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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 { ExtensibleEventType, IPartialEvent } from "../../../src/@types/extensible_events";
|
||||
import { ExtensibleEvent } from "../../../src/extensible_events_v1/ExtensibleEvent";
|
||||
|
||||
class MockEvent extends ExtensibleEvent<any> {
|
||||
public constructor(wireEvent: IPartialEvent<any>) {
|
||||
super(wireEvent);
|
||||
}
|
||||
|
||||
public serialize(): IPartialEvent<object> {
|
||||
throw new Error("Not implemented for tests");
|
||||
}
|
||||
|
||||
public isEquivalentTo(primaryEventType: ExtensibleEventType): boolean {
|
||||
throw new Error("Not implemented for tests");
|
||||
}
|
||||
}
|
||||
|
||||
describe("ExtensibleEvent", () => {
|
||||
it("should expose the wire event directly", () => {
|
||||
const input: IPartialEvent<any> = { type: "org.example.custom", content: { hello: "world" } };
|
||||
const event = new MockEvent(input);
|
||||
expect(event.wireFormat).toBe(input);
|
||||
expect(event.wireContent).toBe(input.content);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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 {
|
||||
ExtensibleAnyMessageEventContent,
|
||||
IPartialEvent,
|
||||
M_HTML,
|
||||
M_MESSAGE,
|
||||
M_TEXT,
|
||||
} from "../../../src/@types/extensible_events";
|
||||
import { MessageEvent } from "../../../src/extensible_events_v1/MessageEvent";
|
||||
import { InvalidEventError } from "../../../src/extensible_events_v1/InvalidEventError";
|
||||
|
||||
describe("MessageEvent", () => {
|
||||
it("should parse m.text", () => {
|
||||
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
|
||||
type: "org.example.message-like",
|
||||
content: {
|
||||
[M_TEXT.name]: "Text here",
|
||||
},
|
||||
};
|
||||
const message = new MessageEvent(input);
|
||||
expect(message.text).toBe("Text here");
|
||||
expect(message.html).toBeFalsy();
|
||||
expect(message.renderings.length).toBe(1);
|
||||
expect(message.renderings.some((r) => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true);
|
||||
});
|
||||
|
||||
it("should parse m.html", () => {
|
||||
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
|
||||
type: "org.example.message-like",
|
||||
content: {
|
||||
[M_TEXT.name]: "Text here",
|
||||
[M_HTML.name]: "HTML here",
|
||||
},
|
||||
};
|
||||
const message = new MessageEvent(input);
|
||||
expect(message.text).toBe("Text here");
|
||||
expect(message.html).toBe("HTML here");
|
||||
expect(message.renderings.length).toBe(2);
|
||||
expect(message.renderings.some((r) => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true);
|
||||
expect(message.renderings.some((r) => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true);
|
||||
});
|
||||
|
||||
it("should parse m.message", () => {
|
||||
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
|
||||
type: "org.example.message-like",
|
||||
content: {
|
||||
[M_MESSAGE.name]: [
|
||||
{ body: "Text here", mimetype: "text/plain" },
|
||||
{ body: "HTML here", mimetype: "text/html" },
|
||||
{ body: "MD here", mimetype: "text/markdown" },
|
||||
],
|
||||
|
||||
// These should be ignored
|
||||
[M_TEXT.name]: "WRONG Text here",
|
||||
[M_HTML.name]: "WRONG HTML here",
|
||||
},
|
||||
};
|
||||
const message = new MessageEvent(input);
|
||||
expect(message.text).toBe("Text here");
|
||||
expect(message.html).toBe("HTML here");
|
||||
expect(message.renderings.length).toBe(3);
|
||||
expect(message.renderings.some((r) => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true);
|
||||
expect(message.renderings.some((r) => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true);
|
||||
expect(message.renderings.some((r) => r.mimetype === "text/markdown" && r.body === "MD here")).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail to parse missing text", () => {
|
||||
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
|
||||
type: "org.example.message-like",
|
||||
content: {
|
||||
hello: "world",
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new MessageEvent(input)).toThrow(
|
||||
new InvalidEventError("Missing textual representation for event"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail to parse missing plain text in m.message", () => {
|
||||
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
|
||||
type: "org.example.message-like",
|
||||
content: {
|
||||
[M_MESSAGE.name]: [{ body: "HTML here", mimetype: "text/html" }],
|
||||
},
|
||||
};
|
||||
expect(() => new MessageEvent(input)).toThrow(
|
||||
new InvalidEventError("m.message is missing a plain text representation"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail to parse non-array m.message", () => {
|
||||
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
|
||||
type: "org.example.message-like",
|
||||
content: {
|
||||
[M_MESSAGE.name]: "invalid",
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new MessageEvent(input)).toThrow(new InvalidEventError("m.message contents must be an array"));
|
||||
});
|
||||
|
||||
describe("from & serialize", () => {
|
||||
it("should serialize to a legacy fallback", () => {
|
||||
const message = MessageEvent.from("Text here", "HTML here");
|
||||
expect(message.text).toBe("Text here");
|
||||
expect(message.html).toBe("HTML here");
|
||||
expect(message.renderings.length).toBe(2);
|
||||
expect(message.renderings.some((r) => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true);
|
||||
expect(message.renderings.some((r) => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true);
|
||||
|
||||
const serialized = message.serialize();
|
||||
expect(serialized.type).toBe("m.room.message");
|
||||
expect(serialized.content).toMatchObject({
|
||||
[M_MESSAGE.name]: [
|
||||
{ body: "Text here", mimetype: "text/plain" },
|
||||
{ body: "HTML here", mimetype: "text/html" },
|
||||
],
|
||||
body: "Text here",
|
||||
msgtype: "m.text",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "HTML here",
|
||||
});
|
||||
});
|
||||
|
||||
it("should serialize non-html content to a legacy fallback", () => {
|
||||
const message = MessageEvent.from("Text here");
|
||||
expect(message.text).toBe("Text here");
|
||||
expect(message.renderings.length).toBe(1);
|
||||
expect(message.renderings.some((r) => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true);
|
||||
|
||||
const serialized = message.serialize();
|
||||
expect(serialized.type).toBe("m.room.message");
|
||||
expect(serialized.content).toMatchObject({
|
||||
[M_TEXT.name]: "Text here",
|
||||
body: "Text here",
|
||||
msgtype: "m.text",
|
||||
format: undefined,
|
||||
formatted_body: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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 { PollEndEventContent, M_POLL_END } from "../../../src/@types/polls";
|
||||
import { IPartialEvent, REFERENCE_RELATION, M_TEXT } from "../../../src/@types/extensible_events";
|
||||
import { PollEndEvent } from "../../../src/extensible_events_v1/PollEndEvent";
|
||||
import { InvalidEventError } from "../../../src/extensible_events_v1/InvalidEventError";
|
||||
|
||||
describe("PollEndEvent", () => {
|
||||
// Note: throughout these tests we don't really bother testing that
|
||||
// MessageEvent is doing its job. It has its own tests to worry about.
|
||||
|
||||
it("should parse a poll closure", () => {
|
||||
const input: IPartialEvent<PollEndEventContent> = {
|
||||
type: M_POLL_END.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_END.name]: {},
|
||||
[M_TEXT.name]: "Poll closed",
|
||||
},
|
||||
};
|
||||
const event = new PollEndEvent(input);
|
||||
expect(event.pollEventId).toBe("$poll");
|
||||
expect(event.closingMessage.text).toBe("Poll closed");
|
||||
});
|
||||
|
||||
it("should fail to parse a missing relationship", () => {
|
||||
const input: IPartialEvent<PollEndEventContent> = {
|
||||
type: M_POLL_END.name,
|
||||
content: {
|
||||
[M_POLL_END.name]: {},
|
||||
[M_TEXT.name]: "Poll closed",
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new PollEndEvent(input)).toThrow(
|
||||
new InvalidEventError("Relationship must be a reference to an event"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail to parse a missing relationship event ID", () => {
|
||||
const input: IPartialEvent<PollEndEventContent> = {
|
||||
type: M_POLL_END.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
},
|
||||
[M_POLL_END.name]: {},
|
||||
[M_TEXT.name]: "Poll closed",
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new PollEndEvent(input)).toThrow(
|
||||
new InvalidEventError("Relationship must be a reference to an event"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail to parse an improper relationship", () => {
|
||||
const input: IPartialEvent<PollEndEventContent> = {
|
||||
type: M_POLL_END.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: "org.example.not-relationship",
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_END.name]: {},
|
||||
[M_TEXT.name]: "Poll closed",
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new PollEndEvent(input)).toThrow(
|
||||
new InvalidEventError("Relationship must be a reference to an event"),
|
||||
);
|
||||
});
|
||||
|
||||
describe("from & serialize", () => {
|
||||
it("should serialize to a poll end event", () => {
|
||||
const event = PollEndEvent.from("$poll", "Poll closed");
|
||||
expect(event.pollEventId).toBe("$poll");
|
||||
expect(event.closingMessage.text).toBe("Poll closed");
|
||||
|
||||
const serialized = event.serialize();
|
||||
expect(M_POLL_END.matches(serialized.type)).toBe(true);
|
||||
expect(serialized.content).toMatchObject({
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_END.name]: {},
|
||||
[M_TEXT.name]: expect.any(String), // tested by MessageEvent tests
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,277 @@
|
||||
/*
|
||||
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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 { M_TEXT, IPartialEvent, REFERENCE_RELATION } from "../../../src/@types/extensible_events";
|
||||
import {
|
||||
M_POLL_START,
|
||||
M_POLL_KIND_DISCLOSED,
|
||||
PollResponseEventContent,
|
||||
M_POLL_RESPONSE,
|
||||
} from "../../../src/@types/polls";
|
||||
import { PollStartEvent } from "../../../src/extensible_events_v1/PollStartEvent";
|
||||
import { InvalidEventError } from "../../../src/extensible_events_v1/InvalidEventError";
|
||||
import { PollResponseEvent } from "../../../src/extensible_events_v1/PollResponseEvent";
|
||||
|
||||
const SAMPLE_POLL = new PollStartEvent({
|
||||
type: M_POLL_START.name,
|
||||
content: {
|
||||
[M_TEXT.name]: "FALLBACK Question here",
|
||||
[M_POLL_START.name]: {
|
||||
question: { [M_TEXT.name]: "Question here" },
|
||||
kind: M_POLL_KIND_DISCLOSED.name,
|
||||
max_selections: 2,
|
||||
answers: [
|
||||
{ id: "one", [M_TEXT.name]: "ONE" },
|
||||
{ id: "two", [M_TEXT.name]: "TWO" },
|
||||
{ id: "thr", [M_TEXT.name]: "THR" },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe("PollResponseEvent", () => {
|
||||
it("should parse a poll response", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: ["one"],
|
||||
},
|
||||
},
|
||||
};
|
||||
const response = new PollResponseEvent(input);
|
||||
expect(response.spoiled).toBe(false);
|
||||
expect(response.answerIds).toMatchObject(["one"]);
|
||||
expect(response.pollEventId).toBe("$poll");
|
||||
});
|
||||
|
||||
it("should fail to parse a missing relationship", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: ["one"],
|
||||
},
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new PollResponseEvent(input)).toThrow(
|
||||
new InvalidEventError("Relationship must be a reference to an event"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail to parse a missing relationship event ID", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: ["one"],
|
||||
},
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new PollResponseEvent(input)).toThrow(
|
||||
new InvalidEventError("Relationship must be a reference to an event"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail to parse an improper relationship", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: "org.example.not-relationship",
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: ["one"],
|
||||
},
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new PollResponseEvent(input)).toThrow(
|
||||
new InvalidEventError("Relationship must be a reference to an event"),
|
||||
);
|
||||
});
|
||||
|
||||
describe("validateAgainst", () => {
|
||||
it("should spoil the vote when no answers", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {},
|
||||
} as any, // force invalid type
|
||||
};
|
||||
const response = new PollResponseEvent(input);
|
||||
expect(response.spoiled).toBe(true);
|
||||
|
||||
response.validateAgainst(SAMPLE_POLL);
|
||||
expect(response.spoiled).toBe(true);
|
||||
});
|
||||
|
||||
it("should spoil the vote when answers are empty", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
const response = new PollResponseEvent(input);
|
||||
expect(response.spoiled).toBe(true);
|
||||
|
||||
response.validateAgainst(SAMPLE_POLL);
|
||||
expect(response.spoiled).toBe(true);
|
||||
});
|
||||
|
||||
it("should spoil the vote when answers are empty", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
const response = new PollResponseEvent(input);
|
||||
expect(response.spoiled).toBe(true);
|
||||
|
||||
response.validateAgainst(SAMPLE_POLL);
|
||||
expect(response.spoiled).toBe(true);
|
||||
});
|
||||
|
||||
it("should spoil the vote when answers are not strings", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: [1, 2, 3],
|
||||
},
|
||||
} as any, // force invalid type
|
||||
};
|
||||
const response = new PollResponseEvent(input);
|
||||
expect(response.spoiled).toBe(true);
|
||||
|
||||
response.validateAgainst(SAMPLE_POLL);
|
||||
expect(response.spoiled).toBe(true);
|
||||
});
|
||||
|
||||
describe("consumer usage", () => {
|
||||
it("should spoil the vote when invalid answers are given", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: ["A", "B", "C"],
|
||||
},
|
||||
},
|
||||
};
|
||||
const response = new PollResponseEvent(input);
|
||||
expect(response.spoiled).toBe(false); // it won't know better
|
||||
|
||||
response.validateAgainst(SAMPLE_POLL);
|
||||
expect(response.spoiled).toBe(true);
|
||||
});
|
||||
|
||||
it("should truncate answers to the poll max selections", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: ["one", "two", "thr"],
|
||||
},
|
||||
},
|
||||
};
|
||||
const response = new PollResponseEvent(input);
|
||||
expect(response.spoiled).toBe(false); // it won't know better
|
||||
expect(response.answerIds).toMatchObject(["one", "two", "thr"]);
|
||||
|
||||
response.validateAgainst(SAMPLE_POLL);
|
||||
expect(response.spoiled).toBe(false);
|
||||
expect(response.answerIds).toMatchObject(["one", "two"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("from & serialize", () => {
|
||||
it("should serialize to a poll response event", () => {
|
||||
const response = PollResponseEvent.from(["A", "B", "C"], "$poll");
|
||||
expect(response.spoiled).toBe(false);
|
||||
expect(response.answerIds).toMatchObject(["A", "B", "C"]);
|
||||
expect(response.pollEventId).toBe("$poll");
|
||||
|
||||
const serialized = response.serialize();
|
||||
expect(M_POLL_RESPONSE.matches(serialized.type)).toBe(true);
|
||||
expect(serialized.content).toMatchObject({
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: ["A", "B", "C"],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should serialize a spoiled vote", () => {
|
||||
const response = PollResponseEvent.from([], "$poll");
|
||||
expect(response.spoiled).toBe(true);
|
||||
expect(response.answerIds).toMatchObject([]);
|
||||
expect(response.pollEventId).toBe("$poll");
|
||||
|
||||
const serialized = response.serialize();
|
||||
expect(M_POLL_RESPONSE.matches(serialized.type)).toBe(true);
|
||||
expect(serialized.content).toMatchObject({
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,337 @@
|
||||
/*
|
||||
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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 { M_TEXT, IPartialEvent } from "../../../src/@types/extensible_events";
|
||||
import {
|
||||
M_POLL_START,
|
||||
M_POLL_KIND_DISCLOSED,
|
||||
PollAnswer,
|
||||
PollStartEventContent,
|
||||
M_POLL_KIND_UNDISCLOSED,
|
||||
} from "../../../src/@types/polls";
|
||||
import { PollStartEvent, PollAnswerSubevent } from "../../../src/extensible_events_v1/PollStartEvent";
|
||||
import { InvalidEventError } from "../../../src/extensible_events_v1/InvalidEventError";
|
||||
|
||||
describe("PollAnswerSubevent", () => {
|
||||
// Note: throughout these tests we don't really bother testing that
|
||||
// MessageEvent is doing its job. It has its own tests to worry about.
|
||||
|
||||
it("should parse an answer representation", () => {
|
||||
const input: IPartialEvent<PollAnswer> = {
|
||||
type: "org.matrix.sdk.poll.answer",
|
||||
content: {
|
||||
id: "one",
|
||||
[M_TEXT.name]: "ONE",
|
||||
},
|
||||
};
|
||||
const answer = new PollAnswerSubevent(input);
|
||||
expect(answer.id).toBe("one");
|
||||
expect(answer.text).toBe("ONE");
|
||||
});
|
||||
|
||||
it("should fail to parse answers without an ID", () => {
|
||||
const input: IPartialEvent<PollAnswer> = {
|
||||
type: "org.matrix.sdk.poll.answer",
|
||||
content: {
|
||||
[M_TEXT.name]: "ONE",
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new PollAnswerSubevent(input)).toThrow(
|
||||
new InvalidEventError("Answer ID must be a non-empty string"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail to parse answers without text", () => {
|
||||
const input: IPartialEvent<PollAnswer> = {
|
||||
type: "org.matrix.sdk.poll.answer",
|
||||
content: {
|
||||
id: "one",
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new PollAnswerSubevent(input)).toThrow(); // we don't check message - that'll be MessageEvent's problem
|
||||
});
|
||||
|
||||
describe("from & serialize", () => {
|
||||
it("should serialize to a placeholder representation", () => {
|
||||
const answer = PollAnswerSubevent.from("one", "ONE");
|
||||
expect(answer.id).toBe("one");
|
||||
expect(answer.text).toBe("ONE");
|
||||
|
||||
const serialized = answer.serialize();
|
||||
expect(serialized.type).toBe("org.matrix.sdk.poll.answer");
|
||||
expect(serialized.content).toMatchObject({
|
||||
id: "one",
|
||||
[M_TEXT.name]: expect.any(String), // tested by MessageEvent
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("PollStartEvent", () => {
|
||||
// Note: throughout these tests we don't really bother testing that
|
||||
// MessageEvent is doing its job. It has its own tests to worry about.
|
||||
|
||||
it("should parse a poll", () => {
|
||||
const input: IPartialEvent<PollStartEventContent> = {
|
||||
type: M_POLL_START.name,
|
||||
content: {
|
||||
[M_TEXT.name]: "FALLBACK Question here",
|
||||
[M_POLL_START.name]: {
|
||||
question: { [M_TEXT.name]: "Question here" },
|
||||
kind: M_POLL_KIND_DISCLOSED.name,
|
||||
max_selections: 2,
|
||||
answers: [
|
||||
{ id: "one", [M_TEXT.name]: "ONE" },
|
||||
{ id: "two", [M_TEXT.name]: "TWO" },
|
||||
{ id: "thr", [M_TEXT.name]: "THR" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const poll = new PollStartEvent(input);
|
||||
expect(poll.question).toBeDefined();
|
||||
expect(poll.question.text).toBe("Question here");
|
||||
expect(poll.kind).toBe(M_POLL_KIND_DISCLOSED);
|
||||
expect(M_POLL_KIND_DISCLOSED.matches(poll.rawKind)).toBe(true);
|
||||
expect(poll.maxSelections).toBe(2);
|
||||
expect(poll.answers.length).toBe(3);
|
||||
expect(poll.answers.some((a) => a.id === "one" && a.text === "ONE")).toBe(true);
|
||||
expect(poll.answers.some((a) => a.id === "two" && a.text === "TWO")).toBe(true);
|
||||
expect(poll.answers.some((a) => a.id === "thr" && a.text === "THR")).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail to parse a missing question", () => {
|
||||
const input: IPartialEvent<PollStartEventContent> = {
|
||||
type: M_POLL_START.name,
|
||||
content: {
|
||||
[M_TEXT.name]: "FALLBACK Question here",
|
||||
[M_POLL_START.name]: {
|
||||
kind: M_POLL_KIND_DISCLOSED.name,
|
||||
max_selections: 2,
|
||||
answers: [
|
||||
{ id: "one", [M_TEXT.name]: "ONE" },
|
||||
{ id: "two", [M_TEXT.name]: "TWO" },
|
||||
{ id: "thr", [M_TEXT.name]: "THR" },
|
||||
],
|
||||
},
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new PollStartEvent(input)).toThrow(new InvalidEventError("A question is required"));
|
||||
});
|
||||
|
||||
it("should fail to parse non-array answers", () => {
|
||||
const input: IPartialEvent<PollStartEventContent> = {
|
||||
type: M_POLL_START.name,
|
||||
content: {
|
||||
[M_TEXT.name]: "FALLBACK Question here",
|
||||
[M_POLL_START.name]: {
|
||||
question: { [M_TEXT.name]: "Question here" },
|
||||
kind: M_POLL_KIND_DISCLOSED.name,
|
||||
max_selections: 2,
|
||||
answers: "one",
|
||||
} as any, // force invalid type
|
||||
},
|
||||
};
|
||||
expect(() => new PollStartEvent(input)).toThrow(new InvalidEventError("Poll answers must be an array"));
|
||||
});
|
||||
|
||||
it("should fail to parse invalid answers", () => {
|
||||
const input: IPartialEvent<PollStartEventContent> = {
|
||||
type: M_POLL_START.name,
|
||||
content: {
|
||||
[M_TEXT.name]: "FALLBACK Question here",
|
||||
[M_POLL_START.name]: {
|
||||
question: { [M_TEXT.name]: "Question here" },
|
||||
kind: M_POLL_KIND_DISCLOSED.name,
|
||||
max_selections: 2,
|
||||
answers: [{ id: "one" }, { [M_TEXT.name]: "TWO" }],
|
||||
} as any, // force invalid type
|
||||
},
|
||||
};
|
||||
expect(() => new PollStartEvent(input)).toThrow(); // error tested by PollAnswerSubevent tests
|
||||
});
|
||||
|
||||
it("should fail to parse lack of answers", () => {
|
||||
const input: IPartialEvent<PollStartEventContent> = {
|
||||
type: M_POLL_START.name,
|
||||
content: {
|
||||
[M_TEXT.name]: "FALLBACK Question here",
|
||||
[M_POLL_START.name]: {
|
||||
question: { [M_TEXT.name]: "Question here" },
|
||||
kind: M_POLL_KIND_DISCLOSED.name,
|
||||
max_selections: 2,
|
||||
answers: [],
|
||||
} as any, // force invalid type
|
||||
},
|
||||
};
|
||||
expect(() => new PollStartEvent(input)).toThrow(new InvalidEventError("No answers available"));
|
||||
});
|
||||
|
||||
it("should truncate answers at 20", () => {
|
||||
const input: IPartialEvent<PollStartEventContent> = {
|
||||
type: M_POLL_START.name,
|
||||
content: {
|
||||
[M_TEXT.name]: "FALLBACK Question here",
|
||||
[M_POLL_START.name]: {
|
||||
question: { [M_TEXT.name]: "Question here" },
|
||||
kind: M_POLL_KIND_DISCLOSED.name,
|
||||
max_selections: 2,
|
||||
answers: [
|
||||
{ id: "01", [M_TEXT.name]: "A" },
|
||||
{ id: "02", [M_TEXT.name]: "B" },
|
||||
{ id: "03", [M_TEXT.name]: "C" },
|
||||
{ id: "04", [M_TEXT.name]: "D" },
|
||||
{ id: "05", [M_TEXT.name]: "E" },
|
||||
{ id: "06", [M_TEXT.name]: "F" },
|
||||
{ id: "07", [M_TEXT.name]: "G" },
|
||||
{ id: "08", [M_TEXT.name]: "H" },
|
||||
{ id: "09", [M_TEXT.name]: "I" },
|
||||
{ id: "10", [M_TEXT.name]: "J" },
|
||||
{ id: "11", [M_TEXT.name]: "K" },
|
||||
{ id: "12", [M_TEXT.name]: "L" },
|
||||
{ id: "13", [M_TEXT.name]: "M" },
|
||||
{ id: "14", [M_TEXT.name]: "N" },
|
||||
{ id: "15", [M_TEXT.name]: "O" },
|
||||
{ id: "16", [M_TEXT.name]: "P" },
|
||||
{ id: "17", [M_TEXT.name]: "Q" },
|
||||
{ id: "18", [M_TEXT.name]: "R" },
|
||||
{ id: "19", [M_TEXT.name]: "S" },
|
||||
{ id: "20", [M_TEXT.name]: "T" },
|
||||
{ id: "FAIL", [M_TEXT.name]: "U" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const poll = new PollStartEvent(input);
|
||||
expect(poll.answers.length).toBe(20);
|
||||
expect(poll.answers.some((a) => a.id === "FAIL")).toBe(false);
|
||||
});
|
||||
|
||||
it("should infer a kind from unknown kinds", () => {
|
||||
const input: IPartialEvent<PollStartEventContent> = {
|
||||
type: M_POLL_START.name,
|
||||
content: {
|
||||
[M_TEXT.name]: "FALLBACK Question here",
|
||||
[M_POLL_START.name]: {
|
||||
question: { [M_TEXT.name]: "Question here" },
|
||||
kind: "org.example.custom.poll.kind",
|
||||
max_selections: 2,
|
||||
answers: [
|
||||
{ id: "01", [M_TEXT.name]: "A" },
|
||||
{ id: "02", [M_TEXT.name]: "B" },
|
||||
{ id: "03", [M_TEXT.name]: "C" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const poll = new PollStartEvent(input);
|
||||
expect(poll.kind).toBe(M_POLL_KIND_UNDISCLOSED);
|
||||
expect(poll.rawKind).toBe("org.example.custom.poll.kind");
|
||||
});
|
||||
|
||||
it("should infer a kind from missing kinds", () => {
|
||||
const input: IPartialEvent<PollStartEventContent> = {
|
||||
type: M_POLL_START.name,
|
||||
content: {
|
||||
[M_TEXT.name]: "FALLBACK Question here",
|
||||
[M_POLL_START.name]: {
|
||||
question: { [M_TEXT.name]: "Question here" },
|
||||
max_selections: 2,
|
||||
answers: [
|
||||
{ id: "01", [M_TEXT.name]: "A" },
|
||||
{ id: "02", [M_TEXT.name]: "B" },
|
||||
{ id: "03", [M_TEXT.name]: "C" },
|
||||
],
|
||||
} as any, // force invalid type
|
||||
},
|
||||
};
|
||||
const poll = new PollStartEvent(input);
|
||||
expect(poll.kind).toBe(M_POLL_KIND_UNDISCLOSED);
|
||||
expect(poll.rawKind).toBeFalsy();
|
||||
});
|
||||
|
||||
describe("from & serialize", () => {
|
||||
it("should serialize to a poll start event", () => {
|
||||
const poll = PollStartEvent.from("Question here", ["A", "B", "C"], M_POLL_KIND_DISCLOSED, 2);
|
||||
expect(poll.question.text).toBe("Question here");
|
||||
expect(poll.kind).toBe(M_POLL_KIND_DISCLOSED);
|
||||
expect(M_POLL_KIND_DISCLOSED.matches(poll.rawKind)).toBe(true);
|
||||
expect(poll.maxSelections).toBe(2);
|
||||
expect(poll.answers.length).toBe(3);
|
||||
expect(poll.answers.some((a) => a.text === "A")).toBe(true);
|
||||
expect(poll.answers.some((a) => a.text === "B")).toBe(true);
|
||||
expect(poll.answers.some((a) => a.text === "C")).toBe(true);
|
||||
|
||||
// Ids are non-empty and unique
|
||||
expect(poll.answers[0].id).toHaveLength(16);
|
||||
expect(poll.answers[1].id).toHaveLength(16);
|
||||
expect(poll.answers[2].id).toHaveLength(16);
|
||||
expect(poll.answers[0].id).not.toEqual(poll.answers[1].id);
|
||||
expect(poll.answers[0].id).not.toEqual(poll.answers[2].id);
|
||||
expect(poll.answers[1].id).not.toEqual(poll.answers[2].id);
|
||||
|
||||
const serialized = poll.serialize();
|
||||
expect(M_POLL_START.matches(serialized.type)).toBe(true);
|
||||
expect(serialized.content).toMatchObject({
|
||||
[M_TEXT.name]: "Question here\n1. A\n2. B\n3. C",
|
||||
[M_POLL_START.name]: {
|
||||
question: {
|
||||
[M_TEXT.name]: expect.any(String), // tested by MessageEvent tests
|
||||
},
|
||||
kind: M_POLL_KIND_DISCLOSED.name,
|
||||
max_selections: 2,
|
||||
answers: [
|
||||
// M_TEXT tested by MessageEvent tests
|
||||
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
|
||||
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
|
||||
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should serialize to a custom kind poll start event", () => {
|
||||
const poll = PollStartEvent.from("Question here", ["A", "B", "C"], "org.example.poll.kind", 2);
|
||||
expect(poll.question.text).toBe("Question here");
|
||||
expect(poll.kind).toBe(M_POLL_KIND_UNDISCLOSED);
|
||||
expect(poll.rawKind).toBe("org.example.poll.kind");
|
||||
expect(poll.maxSelections).toBe(2);
|
||||
expect(poll.answers.length).toBe(3);
|
||||
expect(poll.answers.some((a) => a.text === "A")).toBe(true);
|
||||
expect(poll.answers.some((a) => a.text === "B")).toBe(true);
|
||||
expect(poll.answers.some((a) => a.text === "C")).toBe(true);
|
||||
|
||||
const serialized = poll.serialize();
|
||||
expect(M_POLL_START.matches(serialized.type)).toBe(true);
|
||||
expect(serialized.content).toMatchObject({
|
||||
[M_TEXT.name]: "Question here\n1. A\n2. B\n3. C",
|
||||
[M_POLL_START.name]: {
|
||||
question: {
|
||||
[M_TEXT.name]: expect.any(String), // tested by MessageEvent tests
|
||||
},
|
||||
kind: "org.example.poll.kind",
|
||||
max_selections: 2,
|
||||
answers: [
|
||||
// M_MESSAGE tested by MessageEvent tests
|
||||
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
|
||||
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
|
||||
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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 { NamespacedValue } from "matrix-events-sdk";
|
||||
|
||||
import { isEventTypeSame } from "../../../src/@types/extensible_events";
|
||||
|
||||
describe("isEventTypeSame", () => {
|
||||
it("should match string and string", () => {
|
||||
const a = "org.example.message-like";
|
||||
const b = "org.example.different";
|
||||
|
||||
expect(isEventTypeSame(a, b)).toBe(false);
|
||||
expect(isEventTypeSame(b, a)).toBe(false);
|
||||
|
||||
expect(isEventTypeSame(a, a)).toBe(true);
|
||||
expect(isEventTypeSame(b, b)).toBe(true);
|
||||
});
|
||||
|
||||
it("should match string and namespace", () => {
|
||||
const a = "org.example.message-like";
|
||||
const b = new NamespacedValue<string, string>("org.example.stable", "org.example.unstable");
|
||||
|
||||
expect(isEventTypeSame(a, b)).toBe(false);
|
||||
expect(isEventTypeSame(b, a)).toBe(false);
|
||||
|
||||
expect(isEventTypeSame(a, a)).toBe(true);
|
||||
expect(isEventTypeSame(b, b)).toBe(true);
|
||||
expect(isEventTypeSame(b.name, b)).toBe(true);
|
||||
expect(isEventTypeSame(b.altName, b)).toBe(true);
|
||||
expect(isEventTypeSame(b, b.name)).toBe(true);
|
||||
expect(isEventTypeSame(b, b.altName)).toBe(true);
|
||||
});
|
||||
|
||||
it("should match namespace and namespace", () => {
|
||||
const a = new NamespacedValue<string, string>("org.example.stable1", "org.example.unstable1");
|
||||
const b = new NamespacedValue<string, string>("org.example.stable2", "org.example.unstable2");
|
||||
|
||||
expect(isEventTypeSame(a, b)).toBe(false);
|
||||
expect(isEventTypeSame(b, a)).toBe(false);
|
||||
|
||||
expect(isEventTypeSame(a, a)).toBe(true);
|
||||
expect(isEventTypeSame(a.name, a)).toBe(true);
|
||||
expect(isEventTypeSame(a.altName, a)).toBe(true);
|
||||
expect(isEventTypeSame(a, a.name)).toBe(true);
|
||||
expect(isEventTypeSame(a, a.altName)).toBe(true);
|
||||
|
||||
expect(isEventTypeSame(b, b)).toBe(true);
|
||||
expect(isEventTypeSame(b.name, b)).toBe(true);
|
||||
expect(isEventTypeSame(b.altName, b)).toBe(true);
|
||||
expect(isEventTypeSame(b, b.name)).toBe(true);
|
||||
expect(isEventTypeSame(b, b.altName)).toBe(true);
|
||||
});
|
||||
|
||||
it("should match namespaces of different pointers", () => {
|
||||
const a = new NamespacedValue<string, string>("org.example.stable", "org.example.unstable");
|
||||
const b = new NamespacedValue<string, string>("org.example.stable", "org.example.unstable");
|
||||
|
||||
expect(isEventTypeSame(a, b)).toBe(true);
|
||||
expect(isEventTypeSame(b, a)).toBe(true);
|
||||
|
||||
expect(isEventTypeSame(a, a)).toBe(true);
|
||||
expect(isEventTypeSame(a.name, a)).toBe(true);
|
||||
expect(isEventTypeSame(a.altName, a)).toBe(true);
|
||||
expect(isEventTypeSame(a, a.name)).toBe(true);
|
||||
expect(isEventTypeSame(a, a.altName)).toBe(true);
|
||||
|
||||
expect(isEventTypeSame(b, b)).toBe(true);
|
||||
expect(isEventTypeSame(b.name, b)).toBe(true);
|
||||
expect(isEventTypeSame(b.altName, b)).toBe(true);
|
||||
expect(isEventTypeSame(b, b.name)).toBe(true);
|
||||
expect(isEventTypeSame(b, b.altName)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,15 +1,15 @@
|
||||
import { RelationType } from "../../src";
|
||||
import { FilterComponent } from "../../src/filter-component";
|
||||
import { mkEvent } from '../test-utils/test-utils';
|
||||
import { mkEvent } from "../test-utils/test-utils";
|
||||
|
||||
describe("Filter Component", function() {
|
||||
describe("types", function() {
|
||||
it("should filter out events with other types", function() {
|
||||
const filter = new FilterComponent({ types: ['m.room.message'] });
|
||||
describe("Filter Component", function () {
|
||||
describe("types", function () {
|
||||
it("should filter out events with other types", function () {
|
||||
const filter = new FilterComponent({ types: ["m.room.message"] });
|
||||
const event = mkEvent({
|
||||
type: 'm.room.member',
|
||||
content: { },
|
||||
room: 'roomId',
|
||||
type: "m.room.member",
|
||||
content: {},
|
||||
room: "roomId",
|
||||
event: true,
|
||||
});
|
||||
|
||||
@@ -18,12 +18,12 @@ describe("Filter Component", function() {
|
||||
expect(checkResult).toBe(false);
|
||||
});
|
||||
|
||||
it("should validate events with the same type", function() {
|
||||
const filter = new FilterComponent({ types: ['m.room.message'] });
|
||||
it("should validate events with the same type", function () {
|
||||
const filter = new FilterComponent({ types: ["m.room.message"] });
|
||||
const event = mkEvent({
|
||||
type: 'm.room.message',
|
||||
content: { },
|
||||
room: 'roomId',
|
||||
type: "m.room.message",
|
||||
content: {},
|
||||
room: "roomId",
|
||||
event: true,
|
||||
});
|
||||
|
||||
@@ -32,17 +32,20 @@ describe("Filter Component", function() {
|
||||
expect(checkResult).toBe(true);
|
||||
});
|
||||
|
||||
it("should filter out events by relation participation", function() {
|
||||
const currentUserId = '@me:server.org';
|
||||
const filter = new FilterComponent({
|
||||
related_by_senders: [currentUserId],
|
||||
}, currentUserId);
|
||||
it("should filter out events by relation participation", function () {
|
||||
const currentUserId = "@me:server.org";
|
||||
const filter = new FilterComponent(
|
||||
{
|
||||
related_by_senders: [currentUserId],
|
||||
},
|
||||
currentUserId,
|
||||
);
|
||||
|
||||
const threadRootNotParticipated = mkEvent({
|
||||
type: 'm.room.message',
|
||||
type: "m.room.message",
|
||||
content: {},
|
||||
room: 'roomId',
|
||||
user: '@someone-else:server.org',
|
||||
room: "roomId",
|
||||
user: "@someone-else:server.org",
|
||||
event: true,
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
@@ -57,14 +60,17 @@ describe("Filter Component", function() {
|
||||
expect(filter.check(threadRootNotParticipated)).toBe(false);
|
||||
});
|
||||
|
||||
it("should keep events by relation participation", function() {
|
||||
const currentUserId = '@me:server.org';
|
||||
const filter = new FilterComponent({
|
||||
related_by_senders: [currentUserId],
|
||||
}, currentUserId);
|
||||
it("should keep events by relation participation", function () {
|
||||
const currentUserId = "@me:server.org";
|
||||
const filter = new FilterComponent(
|
||||
{
|
||||
related_by_senders: [currentUserId],
|
||||
},
|
||||
currentUserId,
|
||||
);
|
||||
|
||||
const threadRootParticipated = mkEvent({
|
||||
type: 'm.room.message',
|
||||
type: "m.room.message",
|
||||
content: {},
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
@@ -74,23 +80,23 @@ describe("Filter Component", function() {
|
||||
},
|
||||
},
|
||||
},
|
||||
user: '@someone-else:server.org',
|
||||
room: 'roomId',
|
||||
user: "@someone-else:server.org",
|
||||
room: "roomId",
|
||||
event: true,
|
||||
});
|
||||
|
||||
expect(filter.check(threadRootParticipated)).toBe(true);
|
||||
});
|
||||
|
||||
it("should filter out events by relation type", function() {
|
||||
it("should filter out events by relation type", function () {
|
||||
const filter = new FilterComponent({
|
||||
related_by_rel_types: ["m.thread"],
|
||||
});
|
||||
|
||||
const referenceRelationEvent = mkEvent({
|
||||
type: 'm.room.message',
|
||||
type: "m.room.message",
|
||||
content: {},
|
||||
room: 'roomId',
|
||||
room: "roomId",
|
||||
event: true,
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
@@ -102,13 +108,13 @@ describe("Filter Component", function() {
|
||||
expect(filter.check(referenceRelationEvent)).toBe(false);
|
||||
});
|
||||
|
||||
it("should keep events by relation type", function() {
|
||||
it("should keep events by relation type", function () {
|
||||
const filter = new FilterComponent({
|
||||
related_by_rel_types: ["m.thread"],
|
||||
});
|
||||
|
||||
const threadRootEvent = mkEvent({
|
||||
type: 'm.room.message',
|
||||
type: "m.room.message",
|
||||
content: {},
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
@@ -118,22 +124,22 @@ describe("Filter Component", function() {
|
||||
},
|
||||
},
|
||||
},
|
||||
room: 'roomId',
|
||||
room: "roomId",
|
||||
event: true,
|
||||
});
|
||||
|
||||
const eventWithMultipleRelations = mkEvent({
|
||||
"type": "m.room.message",
|
||||
"content": {},
|
||||
"unsigned": {
|
||||
type: "m.room.message",
|
||||
content: {},
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
"testtesttest": {},
|
||||
"m.annotation": {
|
||||
"chunk": [
|
||||
chunk: [
|
||||
{
|
||||
"type": "m.reaction",
|
||||
"key": "🤫",
|
||||
"count": 1,
|
||||
type: "m.reaction",
|
||||
key: "🤫",
|
||||
count: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -143,20 +149,20 @@ describe("Filter Component", function() {
|
||||
},
|
||||
},
|
||||
},
|
||||
"room": 'roomId',
|
||||
"event": true,
|
||||
room: "roomId",
|
||||
event: true,
|
||||
});
|
||||
|
||||
const noMatchEvent = mkEvent({
|
||||
"type": "m.room.message",
|
||||
"content": {},
|
||||
"unsigned": {
|
||||
type: "m.room.message",
|
||||
content: {},
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
"testtesttest": {},
|
||||
testtesttest: {},
|
||||
},
|
||||
},
|
||||
"room": 'roomId',
|
||||
"event": true,
|
||||
room: "roomId",
|
||||
event: true,
|
||||
});
|
||||
|
||||
expect(filter.check(threadRootEvent)).toBe(true);
|
||||
|
||||
+11
-11
@@ -19,17 +19,17 @@ import { Filter, IFilterDefinition } from "../../src/filter";
|
||||
import { mkEvent } from "../test-utils/test-utils";
|
||||
import { EventType } from "../../src";
|
||||
|
||||
describe("Filter", function() {
|
||||
describe("Filter", function () {
|
||||
const filterId = "f1lt3ring15g00d4ursoul";
|
||||
const userId = "@sir_arthur_david:humming.tiger";
|
||||
let filter: Filter;
|
||||
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
filter = new Filter(userId);
|
||||
});
|
||||
|
||||
describe("fromJson", function() {
|
||||
it("create a new Filter from the provided values", function() {
|
||||
describe("fromJson", function () {
|
||||
it("create a new Filter from the provided values", function () {
|
||||
const definition = {
|
||||
event_fields: ["type", "content"],
|
||||
};
|
||||
@@ -40,8 +40,8 @@ describe("Filter", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("setTimelineLimit", function() {
|
||||
it("should set room.timeline.limit of the filter definition", function() {
|
||||
describe("setTimelineLimit", function () {
|
||||
it("should set room.timeline.limit of the filter definition", function () {
|
||||
filter.setTimelineLimit(10);
|
||||
expect(filter.getDefinition()).toEqual({
|
||||
room: {
|
||||
@@ -53,18 +53,18 @@ describe("Filter", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("setDefinition/getDefinition", function() {
|
||||
it("should set and get the filter body", function() {
|
||||
describe("setDefinition/getDefinition", function () {
|
||||
it("should set and get the filter body", function () {
|
||||
const definition = {
|
||||
event_format: "client" as IFilterDefinition['event_format'],
|
||||
event_format: "client" as IFilterDefinition["event_format"],
|
||||
};
|
||||
filter.setDefinition(definition);
|
||||
expect(filter.getDefinition()).toEqual(definition);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setUnreadThreadNotifications", function() {
|
||||
it("setUnreadThreadNotifications", function() {
|
||||
describe("setUnreadThreadNotifications", function () {
|
||||
it("setUnreadThreadNotifications", function () {
|
||||
filter.setUnreadThreadNotifications(true);
|
||||
expect(filter.getDefinition()).toEqual({
|
||||
room: {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user