1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-31 15:24:23 +03:00

Apply prettier formatting

This commit is contained in:
Michael Weimann
2022-12-09 09:38:20 +01:00
parent 08a9073bd5
commit 349c2c2587
239 changed files with 22004 additions and 21928 deletions

View File

@ -1,12 +1,15 @@
{ {
"sourceMaps": true, "sourceMaps": true,
"presets": [ "presets": [
["@babel/preset-env", { [
"targets": { "@babel/preset-env",
"node": 10 {
}, "targets": {
"modules": "commonjs" "node": 10
}], },
"modules": "commonjs"
}
],
"@babel/preset-typescript" "@babel/preset-typescript"
], ],
"plugins": [ "plugins": [

View File

@ -1,13 +1,6 @@
module.exports = { module.exports = {
plugins: [ plugins: ["matrix-org", "import", "jsdoc"],
"matrix-org", extends: ["plugin:matrix-org/babel", "plugin:import/typescript"],
"import",
"jsdoc",
],
extends: [
"plugin:matrix-org/babel",
"plugin:import/typescript",
],
env: { env: {
browser: true, browser: true,
node: true, node: true,
@ -28,12 +21,15 @@ module.exports = {
"padded-blocks": ["error"], "padded-blocks": ["error"],
"no-extend-native": ["error"], "no-extend-native": ["error"],
"camelcase": ["error"], "camelcase": ["error"],
"no-multi-spaces": ["error", { "ignoreEOLComments": true }], "no-multi-spaces": ["error", { ignoreEOLComments: true }],
"space-before-function-paren": ["error", { "space-before-function-paren": [
"anonymous": "never", "error",
"named": "never", {
"asyncArrow": "always", anonymous: "never",
}], named: "never",
asyncArrow: "always",
},
],
"arrow-parens": "off", "arrow-parens": "off",
"prefer-promise-reject-errors": "off", "prefer-promise-reject-errors": "off",
"no-constant-condition": "off", "no-constant-condition": "off",
@ -42,76 +38,78 @@ module.exports = {
"no-console": "error", "no-console": "error",
// restrict EventEmitters to force callers to use TypedEventEmitter // restrict EventEmitters to force callers to use TypedEventEmitter
"no-restricted-imports": ["error", { "no-restricted-imports": [
name: "events", "error",
message: "Please use TypedEventEmitter instead", {
}], name: "events",
message: "Please use TypedEventEmitter instead",
},
],
"import/no-restricted-paths": ["error", { "import/no-restricted-paths": [
"zones": [{ "error",
"target": "./src/", {
"from": "./src/index.ts", zones: [
"message": "The package index is dynamic between src and lib depending on " + {
"whether release or development, target the specific module or matrix.ts instead", 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: [{ overrides: [
files: [ {
"**/*.ts", files: ["**/*.ts"],
], plugins: ["eslint-plugin-tsdoc"],
plugins: [ extends: ["plugin:matrix-org/typescript"],
"eslint-plugin-tsdoc", rules: {
], // TypeScript has its own version of this
extends: [ "@babel/no-invalid-this": "off",
"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 // We're okay being explicit at the moment
"@typescript-eslint/no-empty-interface": "off", "@typescript-eslint/no-empty-interface": "off",
// We disable this while we're transitioning // We disable this while we're transitioning
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
// We'd rather not do this but we do // We'd rather not do this but we do
"@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/ban-ts-comment": "off",
// We're okay with assertion errors when we ask for them // We're okay with assertion errors when we ask for them
"@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-non-null-assertion": "off",
// The non-TypeScript rule produces false positives // The non-TypeScript rule produces false positives
"func-call-spacing": "off", "func-call-spacing": "off",
"@typescript-eslint/func-call-spacing": ["error"], "@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 // We don't need amazing docs in our spec files
files: [ files: ["src/**/*.ts"],
"src/**/*.ts", rules: {
], "tsdoc/syntax": "error",
rules: { // We use some select jsdoc rules as the tsdoc linter has only one rule
"tsdoc/syntax": "error", "jsdoc/no-types": "error",
// We use some select jsdoc rules as the tsdoc linter has only one rule "jsdoc/empty-tags": "error",
"jsdoc/no-types": "error", "jsdoc/check-property-names": "error",
"jsdoc/empty-tags": "error", "jsdoc/check-values": "error",
"jsdoc/check-property-names": "error", // These need a bit more work before we can enable
"jsdoc/check-values": "error", // "jsdoc/check-param-names": "error",
// These need a bit more work before we can enable // "jsdoc/check-indentation": "error",
// "jsdoc/check-param-names": "error", },
// "jsdoc/check-indentation": "error",
}, },
}, { {
files: [ files: ["spec/**/*.ts"],
"spec/**/*.ts", rules: {
], // We don't need super strict typing in test utilities
rules: { "@typescript-eslint/explicit-function-return-type": "off",
// We don't need super strict typing in test utilities "@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/explicit-function-return-type": "off", },
"@typescript-eslint/explicit-member-accessibility": "off",
}, },
}], ],
}; };

View File

@ -2,9 +2,9 @@
## Checklist ## Checklist
* [ ] Tests written for new code (and old code if feasible) - [ ] Tests written for new code (and old code if feasible)
* [ ] Linter and other CI checks pass - [ ] 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)) - [ ] 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: If you would like to specify text for the changelog entry other than your PR title, add the following:

View File

@ -1,6 +1,4 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [ "extends": ["github>matrix-org/renovate-config-element-web"]
"github>matrix-org/renovate-config-element-web"
]
} }

View File

@ -1,30 +1,30 @@
name: Backport name: Backport
on: on:
pull_request_target: pull_request_target:
types: types:
- closed - closed
- labeled - labeled
branches: branches:
- develop - develop
jobs: jobs:
backport: backport:
name: Backport name: Backport
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Only react to merged PRs for security reasons. # Only react to merged PRs for security reasons.
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target. # See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
if: > if: >
github.event.pull_request.merged github.event.pull_request.merged
&& ( && (
github.event.action == 'closed' github.event.action == 'closed'
|| ( || (
github.event.action == 'labeled' github.event.action == 'labeled'
&& contains(github.event.label.name, 'backport') && contains(github.event.label.name, 'backport')
) )
) )
steps: steps:
- uses: tibdex/backport@v2 - uses: tibdex/backport@v2
with: with:
labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>" labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>"
# We can't use GITHUB_TOKEN here or CI won't run on the new PR # We can't use GITHUB_TOKEN here or CI won't run on the new PR
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }} github_token: ${{ secrets.ELEMENT_BOT_TOKEN }}

View File

@ -1,34 +1,34 @@
name: Deploy documentation PR preview name: Deploy documentation PR preview
on: on:
workflow_run: workflow_run:
workflows: [ "Static Analysis" ] workflows: ["Static Analysis"]
types: types:
- completed - completed
jobs: jobs:
netlify: netlify:
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action # 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: # (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
- name: 📥 Download artifact - name: 📥 Download artifact
uses: dawidd6/action-download-artifact@e6e25ac3a2b93187502a8be1ef9e9603afc34925 # v2.24.2 uses: dawidd6/action-download-artifact@e6e25ac3a2b93187502a8be1ef9e9603afc34925 # v2.24.2
with: with:
workflow: static_analysis.yml workflow: static_analysis.yml
run_id: ${{ github.event.workflow_run.id }} run_id: ${{ github.event.workflow_run.id }}
name: docs name: docs
path: docs path: docs
- name: 📤 Deploy to Netlify - name: 📤 Deploy to Netlify
uses: matrix-org/netlify-pr-preview@v1 uses: matrix-org/netlify-pr-preview@v1
with: with:
path: docs path: docs
owner: ${{ github.event.workflow_run.head_repository.owner.login }} owner: ${{ github.event.workflow_run.head_repository.owner.login }}
branch: ${{ github.event.workflow_run.head_branch }} branch: ${{ github.event.workflow_run.head_branch }}
revision: ${{ github.event.workflow_run.head_sha }} revision: ${{ github.event.workflow_run.head_sha }}
token: ${{ secrets.NETLIFY_AUTH_TOKEN }} token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
site_id: ${{ secrets.NETLIFY_SITE_ID }} site_id: ${{ secrets.NETLIFY_SITE_ID }}
desc: Documentation preview desc: Documentation preview
deployment_env: PR Documentation Preview deployment_env: PR Documentation Preview

View File

@ -1,27 +1,27 @@
name: Notify Downstream Projects name: Notify Downstream Projects
on: on:
push: push:
branches: [ develop ] branches: [develop]
concurrency: ${{ github.workflow }}-${{ github.ref }} concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs: jobs:
notify-downstream: notify-downstream:
# Only respect triggers from our develop branch, ignore that of forks # Only respect triggers from our develop branch, ignore that of forks
if: github.repository == 'matrix-org/matrix-js-sdk' if: github.repository == 'matrix-org/matrix-js-sdk'
continue-on-error: true continue-on-error: true
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- repo: vector-im/element-web - repo: vector-im/element-web
event: element-web-notify event: element-web-notify
- repo: matrix-org/matrix-react-sdk - repo: matrix-org/matrix-react-sdk
event: upstream-sdk-notify event: upstream-sdk-notify
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it - name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
uses: peter-evans/repository-dispatch@v2 uses: peter-evans/repository-dispatch@v2
with: with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }} token: ${{ secrets.ELEMENT_BOT_TOKEN }}
repository: ${{ matrix.repo }} repository: ${{ matrix.repo }}
event-type: ${{ matrix.event }} event-type: ${{ matrix.event }}

View File

@ -1,91 +1,91 @@
name: Pull Request name: Pull Request
on: on:
pull_request_target: pull_request_target:
types: [ opened, edited, labeled, unlabeled, synchronize ] types: [opened, edited, labeled, unlabeled, synchronize]
workflow_call: workflow_call:
inputs: inputs:
labels: labels:
type: string type: string
default: "T-Defect,T-Deprecation,T-Enhancement,T-Task" default: "T-Defect,T-Deprecation,T-Enhancement,T-Task"
required: false required: false
description: "No longer used, uses allchange logic now, will be removed at a later date" description: "No longer used, uses allchange logic now, will be removed at a later date"
secrets: secrets:
ELEMENT_BOT_TOKEN: ELEMENT_BOT_TOKEN:
required: true required: true
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref }} concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref }}
jobs: jobs:
changelog: changelog:
name: Preview Changelog name: Preview Changelog
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: matrix-org/allchange@main - uses: matrix-org/allchange@main
with: with:
ghToken: ${{ secrets.GITHUB_TOKEN }} ghToken: ${{ secrets.GITHUB_TOKEN }}
requireLabel: true requireLabel: true
prevent-blocked: prevent-blocked:
name: Prevent Blocked name: Prevent Blocked
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
pull-requests: read pull-requests: read
steps: steps:
- name: Add notice - name: Add notice
uses: actions/github-script@v6 uses: actions/github-script@v6
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked') if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
with: with:
script: | script: |
core.setFailed("Preventing merge whilst PR is marked blocked!"); core.setFailed("Preventing merge whilst PR is marked blocked!");
community-prs: community-prs:
name: Label Community PRs name: Label Community PRs
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event.action == 'opened' if: github.event.action == 'opened'
steps: steps:
- name: Check membership - name: Check membership
uses: tspascoal/get-user-teams-membership@v2 uses: tspascoal/get-user-teams-membership@v2
id: teams id: teams
with: with:
username: ${{ github.event.pull_request.user.login }} username: ${{ github.event.pull_request.user.login }}
organization: matrix-org organization: matrix-org
team: Core Team team: Core Team
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
- name: Add label - name: Add label
if: ${{ steps.teams.outputs.isTeamMember == 'false' }} if: ${{ steps.teams.outputs.isTeamMember == 'false' }}
uses: actions/github-script@v6 uses: actions/github-script@v6
with: with:
script: | script: |
github.rest.issues.addLabels({ github.rest.issues.addLabels({
issue_number: context.issue.number, issue_number: context.issue.number,
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
labels: ['Z-Community-PR'] labels: ['Z-Community-PR']
}); });
close-if-fork-develop: close-if-fork-develop:
name: Forbid develop branch fork contributions name: Forbid develop branch fork contributions
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: > if: >
github.event.action == 'opened' && github.event.action == 'opened' &&
github.event.pull_request.head.ref == 'develop' && github.event.pull_request.head.ref == 'develop' &&
github.event.pull_request.head.repo.full_name != github.repository github.event.pull_request.head.repo.full_name != github.repository
steps: steps:
- name: Close pull request - name: Close pull request
uses: actions/github-script@v6 uses: actions/github-script@v6
with: with:
script: | script: |
github.rest.issues.createComment({ github.rest.issues.createComment({
issue_number: context.issue.number, issue_number: context.issue.number,
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
body: "Thanks for opening this pull request, unfortunately we do not accept contributions from the main" + 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." + " 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", " See https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md",
}); });
github.rest.pulls.update({ github.rest.pulls.update({
pull_number: context.issue.number, pull_number: context.issue.number,
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
state: 'closed' state: 'closed'
}); });

View File

@ -1,41 +1,41 @@
# Must only be called from `release#published` triggers # Must only be called from `release#published` triggers
name: Publish to npm name: Publish to npm
on: on:
workflow_call: workflow_call:
secrets: secrets:
NPM_TOKEN: NPM_TOKEN:
required: true required: true
jobs: jobs:
npm: npm:
name: Publish to npm name: Publish to npm
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 🧮 Checkout code - name: 🧮 Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: 🔧 Yarn cache - name: 🔧 Yarn cache
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
cache: "yarn" cache: "yarn"
registry-url: 'https://registry.npmjs.org' registry-url: "https://registry.npmjs.org"
- name: 🔨 Install dependencies - name: 🔨 Install dependencies
run: "yarn install --pure-lockfile" run: "yarn install --pure-lockfile"
- name: 🚀 Publish to npm - name: 🚀 Publish to npm
id: npm-publish id: npm-publish
uses: JS-DevTools/npm-publish@v1 uses: JS-DevTools/npm-publish@v1
with: with:
token: ${{ secrets.NPM_TOKEN }} token: ${{ secrets.NPM_TOKEN }}
access: public access: public
tag: next tag: next
- name: 🎖️ Add `latest` dist-tag to final releases - name: 🎖️ Add `latest` dist-tag to final releases
if: github.event.release.prerelease == false if: github.event.release.prerelease == false
run: | run: |
package=$(cat package.json | jq -er .name) package=$(cat package.json | jq -er .name)
npm dist-tag add "$package@$release" latest npm dist-tag add "$package@$release" latest
env: env:
# JS-DevTools/npm-publish overrides `NODE_AUTH_TOKEN` with `INPUT_TOKEN` in .npmrc # JS-DevTools/npm-publish overrides `NODE_AUTH_TOKEN` with `INPUT_TOKEN` in .npmrc
INPUT_TOKEN: ${{ secrets.NPM_TOKEN }} INPUT_TOKEN: ${{ secrets.NPM_TOKEN }}
release: ${{ steps.npm-publish.outputs.version }} release: ${{ steps.npm-publish.outputs.version }}

View File

@ -1,58 +1,58 @@
name: Release Process name: Release Process
on: on:
release: release:
types: [ published ] types: [published]
concurrency: ${{ github.workflow }}-${{ github.ref }} concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs: jobs:
jsdoc: jsdoc:
name: Publish Documentation name: Publish Documentation
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 🧮 Checkout code - name: 🧮 Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: 🔧 Yarn cache - name: 🔧 Yarn cache
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
cache: "yarn" cache: "yarn"
- name: 🔨 Install dependencies - name: 🔨 Install dependencies
run: "yarn install --pure-lockfile" run: "yarn install --pure-lockfile"
- name: 📖 Generate JSDoc - name: 📖 Generate JSDoc
run: "yarn gendoc" run: "yarn gendoc"
- name: 📋 Copy to temp - name: 📋 Copy to temp
run: | run: |
cp -a "./_docs" "$RUNNER_TEMP/" cp -a "./_docs" "$RUNNER_TEMP/"
- name: 🧮 Checkout gh-pages - name: 🧮 Checkout gh-pages
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
ref: gh-pages ref: gh-pages
- name: 🔪 Prepare - name: 🔪 Prepare
run: | run: |
tag="${{ github.ref_name }}" tag="${{ github.ref_name }}"
VERSION="${tag#v}" VERSION="${tag#v}"
[ ! -e "$VERSION" ] || rm -r $VERSION [ ! -e "$VERSION" ] || rm -r $VERSION
cp -r $RUNNER_TEMP/_docs/ $VERSION cp -r $RUNNER_TEMP/_docs/ $VERSION
# Add the new directory to the index if it isn't there already # Add the new directory to the index if it isn't there already
if ! grep -q ">Version $VERSION</a>" index.html; then if ! grep -q ">Version $VERSION</a>" index.html; then
perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print
"<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' "$VERSION" index.html "<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' "$VERSION" index.html
fi fi
- name: 🚀 Deploy - name: 🚀 Deploy
uses: peaceiris/actions-gh-pages@v3 uses: peaceiris/actions-gh-pages@v3
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
keep_files: true keep_files: true
publish_dir: . publish_dir: .
npm: npm:
name: Publish name: Publish
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
secrets: secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@ -1,50 +1,50 @@
# Must only be called from a workflow_run in the context of the upstream repo # Must only be called from a workflow_run in the context of the upstream repo
name: SonarCloud name: SonarCloud
on: on:
workflow_call: workflow_call:
secrets: secrets:
SONAR_TOKEN: SONAR_TOKEN:
required: true required: true
inputs: inputs:
extra_args: extra_args:
type: string type: string
required: false required: false
description: "Extra args to pass to SonarCloud" description: "Extra args to pass to SonarCloud"
jobs: jobs:
sonarqube: sonarqube:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event.workflow_run.conclusion == 'success' if: github.event.workflow_run.conclusion == 'success'
steps: steps:
# We create the status here and then update it to success/failure in the `report` stage # 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. # This provides an easy link to this workflow_run from the PR before Cypress is done.
- uses: Sibz/github-status-action@v1 - uses: Sibz/github-status-action@v1
with: with:
authToken: ${{ secrets.GITHUB_TOKEN }} authToken: ${{ secrets.GITHUB_TOKEN }}
state: pending state: pending
context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }}) context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
sha: ${{ github.event.workflow_run.head_sha }} sha: ${{ github.event.workflow_run.head_sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: "🩻 SonarCloud Scan" - name: "🩻 SonarCloud Scan"
id: sonarcloud id: sonarcloud
uses: matrix-org/sonarcloud-workflow-action@v2.3 uses: matrix-org/sonarcloud-workflow-action@v2.3
with: with:
repository: ${{ github.event.workflow_run.head_repository.full_name }} repository: ${{ github.event.workflow_run.head_repository.full_name }}
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }} is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
version_cmd: 'cat package.json | jq -r .version' version_cmd: "cat package.json | jq -r .version"
branch: ${{ github.event.workflow_run.head_branch }} branch: ${{ github.event.workflow_run.head_branch }}
revision: ${{ github.event.workflow_run.head_sha }} revision: ${{ github.event.workflow_run.head_sha }}
token: ${{ secrets.SONAR_TOKEN }} token: ${{ secrets.SONAR_TOKEN }}
coverage_run_id: ${{ github.event.workflow_run.id }} coverage_run_id: ${{ github.event.workflow_run.id }}
coverage_workflow_name: tests.yml coverage_workflow_name: tests.yml
coverage_extract_path: coverage coverage_extract_path: coverage
extra_args: ${{ inputs.extra_args }} extra_args: ${{ inputs.extra_args }}
- uses: Sibz/github-status-action@v1 - uses: Sibz/github-status-action@v1
if: always() if: always()
with: with:
authToken: ${{ secrets.GITHUB_TOKEN }} authToken: ${{ secrets.GITHUB_TOKEN }}
state: ${{ steps.sonarcloud.outcome == 'success' && 'success' || 'failure' }} state: ${{ steps.sonarcloud.outcome == 'success' && 'success' || 'failure' }}
context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }}) context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
sha: ${{ github.event.workflow_run.head_sha }} sha: ${{ github.event.workflow_run.head_sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}

View File

@ -1,43 +1,43 @@
name: SonarQube name: SonarQube
on: on:
workflow_run: workflow_run:
workflows: [ "Tests" ] workflows: ["Tests"]
types: types:
- completed - completed
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }} group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
# This is a workaround for https://github.com/SonarSource/SonarJS/issues/578 # This is a workaround for https://github.com/SonarSource/SonarJS/issues/578
prepare: prepare:
name: Prepare name: Prepare
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:
reportPaths: ${{ steps.extra_args.outputs.reportPaths }} reportPaths: ${{ steps.extra_args.outputs.reportPaths }}
testExecutionReportPaths: ${{ steps.extra_args.outputs.testExecutionReportPaths }} testExecutionReportPaths: ${{ steps.extra_args.outputs.testExecutionReportPaths }}
steps: steps:
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action # 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: # (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
- name: 📥 Download artifact - name: 📥 Download artifact
uses: dawidd6/action-download-artifact@v2 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: with:
workflow: tests.yaml extra_args: -Dsonar.javascript.lcov.reportPaths=${{ needs.prepare.outputs.reportPaths }} -Dsonar.testExecutionReportPaths=${{ needs.prepare.outputs.testExecutionReportPaths }}
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 }}

View File

@ -1,74 +1,74 @@
name: Static Analysis name: Static Analysis
on: on:
pull_request: { } pull_request: {}
push: push:
branches: [ develop, master ] branches: [develop, master]
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
ts_lint: ts_lint:
name: "Typescript Syntax Check" name: "Typescript Syntax Check"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
cache: 'yarn' cache: "yarn"
- name: Install Deps - name: Install Deps
run: "yarn install" run: "yarn install"
- name: Typecheck - name: Typecheck
run: "yarn run lint:types" run: "yarn run lint:types"
- name: Switch js-sdk to release mode - name: Switch js-sdk to release mode
run: | run: |
scripts/switch_package_to_release.js scripts/switch_package_to_release.js
yarn install yarn install
yarn run build:compile yarn run build:compile
yarn run build:types yarn run build:types
- name: Typecheck (release mode) - name: Typecheck (release mode)
run: "yarn run lint:types" run: "yarn run lint:types"
js_lint: js_lint:
name: "ESLint" name: "ESLint"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
cache: 'yarn' cache: "yarn"
- name: Install Deps - name: Install Deps
run: "yarn install" run: "yarn install"
- name: Run Linter - name: Run Linter
run: "yarn run lint:js" run: "yarn run lint:js"
docs: docs:
name: "JSDoc Checker" name: "JSDoc Checker"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
cache: 'yarn' cache: "yarn"
- name: Install Deps - name: Install Deps
run: "yarn install" run: "yarn install"
- name: Generate Docs - name: Generate Docs
run: "yarn run gendoc" run: "yarn run gendoc"
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: docs name: docs
path: _docs path: _docs
# We'll only use this in a workflow_run, then we're done with it # We'll only use this in a workflow_run, then we're done with it
retention-days: 1 retention-days: 1

View File

@ -1,61 +1,61 @@
name: Tests name: Tests
on: on:
pull_request: { } pull_request: {}
push: push:
branches: [ develop, master ] branches: [develop, master]
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
jest: jest:
name: 'Jest [${{ matrix.specs }}] (Node ${{ matrix.node }})' name: "Jest [${{ matrix.specs }}] (Node ${{ matrix.node }})"
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10 timeout-minutes: 10
strategy: strategy:
matrix: matrix:
specs: [browserify, integ, unit] specs: [browserify, integ, unit]
node: [16, 18, latest] node: [16, 18, latest]
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
cache: 'yarn' cache: "yarn"
node-version: ${{ matrix.node }} node-version: ${{ matrix.node }}
- name: Install dependencies - name: Install dependencies
run: "yarn install" run: "yarn install"
- name: Build - name: Build
if: matrix.specs == 'browserify' if: matrix.specs == 'browserify'
run: "yarn build" run: "yarn build"
- name: Get number of CPU cores - name: Get number of CPU cores
id: cpu-cores id: cpu-cores
uses: SimenB/github-actions-cpu-cores@v1 uses: SimenB/github-actions-cpu-cores@v1
- name: Run tests with coverage and metrics - name: Run tests with coverage and metrics
if: github.ref == 'refs/heads/develop' if: github.ref == 'refs/heads/develop'
run: | run: |
yarn coverage --ci --reporters github-actions '--reporters=<rootDir>/spec/slowReporter.js' --max-workers ${{ steps.cpu-cores.outputs.count }} ./spec/${{ matrix.specs }} 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 mv coverage/lcov.info coverage/${{ matrix.node }}-${{ matrix.specs }}.lcov.info
env: env:
JEST_SONAR_UNIQUE_OUTPUT_NAME: true JEST_SONAR_UNIQUE_OUTPUT_NAME: true
- name: Run tests with coverage - name: Run tests with coverage
if: github.ref != 'refs/heads/develop' if: github.ref != 'refs/heads/develop'
run: | run: |
yarn coverage --ci --reporters github-actions --max-workers ${{ steps.cpu-cores.outputs.count }} ./spec/${{ matrix.specs }} 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 mv coverage/lcov.info coverage/${{ matrix.node }}-${{ matrix.specs }}.lcov.info
env: env:
JEST_SONAR_UNIQUE_OUTPUT_NAME: true JEST_SONAR_UNIQUE_OUTPUT_NAME: true
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: coverage name: coverage
path: | path: |
coverage coverage
!coverage/lcov-report !coverage/lcov-report

View File

@ -1,38 +1,38 @@
name: Upgrade Dependencies name: Upgrade Dependencies
on: on:
workflow_dispatch: { } workflow_dispatch: {}
workflow_call: workflow_call:
secrets: secrets:
ELEMENT_BOT_TOKEN: ELEMENT_BOT_TOKEN:
required: true required: true
jobs: jobs:
upgrade: upgrade:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
cache: 'yarn' cache: "yarn"
- name: Upgrade - name: Upgrade
run: yarn upgrade && yarn install run: yarn upgrade && yarn install
- name: Create Pull Request - name: Create Pull Request
id: cpr id: cpr
uses: peter-evans/create-pull-request@v4 uses: peter-evans/create-pull-request@v4
with: with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }} token: ${{ secrets.ELEMENT_BOT_TOKEN }}
branch: actions/upgrade-deps branch: actions/upgrade-deps
delete-branch: true delete-branch: true
title: Upgrade dependencies title: Upgrade dependencies
labels: | labels: |
Dependencies Dependencies
T-Task T-Task
- name: Enable automerge - name: Enable automerge
uses: peter-evans/enable-pull-request-automerge@v2 uses: peter-evans/enable-pull-request-automerge@v2
if: steps.cpr.outputs.pull-request-operation == 'created' if: steps.cpr.outputs.pull-request-operation == 'created'
with: with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }} token: ${{ secrets.ELEMENT_BOT_TOKEN }}
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }} pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}

File diff suppressed because it is too large Load Diff

View File

@ -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 matrix-js-sdk follows the same pattern as https://github.com/vector-im/element-web/blob/develop/CONTRIBUTING.md

289
README.md
View File

@ -6,27 +6,25 @@
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=matrix-js-sdk&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk) [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=matrix-js-sdk&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=matrix-js-sdk&metric=bugs)](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk) [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=matrix-js-sdk&metric=bugs)](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 SDK for JavaScript and TypeScript. This SDK can be run in a 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. browser or in Node.js.
The Matrix specification is constantly evolving - while this SDK aims for maximum backwards compatibility, it only 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 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 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 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. endpoints from before Matrix 1.1, for example.
Quickstart # Quickstart
==========
## In a browser
In a browser
------------
Download the browser version from Download the browser version from
https://github.com/matrix-org/matrix-js-sdk/releases/latest and add that as a 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`` `<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 attached to `window` through which you can access the SDK. See below for how to
include libolm to enable end-to-end-encryption. include libolm to enable end-to-end-encryption.
The browser bundle supports recent versions of browsers. Typically this is ES2015 The browser bundle supports recent versions of browsers. Typically this is ES2015
@ -35,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. 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. 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. This library relies on `fetch` which is available in Node from v18.0.0 - it should work fine also with polyfills.
@ -45,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) 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. if you do not have it already.
``yarn add matrix-js-sdk`` `yarn add matrix-js-sdk`
```javascript ```javascript
import * as sdk from "matrix-js-sdk"; import * as sdk from "matrix-js-sdk";
const client = sdk.createClient("https://matrix.org"); const client = sdk.createClient("https://matrix.org");
client.publicRooms(function(err, data) { client.publicRooms(function (err, data) {
console.log("Public Rooms: %s", JSON.stringify(data)); console.log("Public Rooms: %s", JSON.stringify(data));
}); });
``` ```
See below for how to include libolm to enable end-to-end-encryption. Please check See below for how to include libolm to enable end-to-end-encryption. Please check
@ -61,14 +58,14 @@ See below for how to include libolm to enable end-to-end-encryption. Please chec
To start the client: To start the client:
```javascript ```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: You can perform a call to `/sync` to get the current state of the client:
```javascript ```javascript
client.once('sync', function(state, prevState, res) { client.once("sync", function (state, prevState, res) {
if(state === 'PREPARED') { if (state === "PREPARED") {
console.log("prepared"); console.log("prepared");
} else { } else {
console.log(state); console.log(state);
@ -81,8 +78,8 @@ To send a message:
```javascript ```javascript
const content = { const content = {
"body": "message text", body: "message text",
"msgtype": "m.text" msgtype: "m.text",
}; };
client.sendEvent("roomId", "m.room.message", content, "", (err, res) => { client.sendEvent("roomId", "m.room.message", content, "", (err, res) => {
console.log(err); console.log(err);
@ -92,11 +89,11 @@ client.sendEvent("roomId", "m.room.message", content, "", (err, res) => {
To listen for message events: To listen for message events:
```javascript ```javascript
client.on("Room.timeline", function(event, room, toStartOfTimeline) { client.on("Room.timeline", function (event, room, toStartOfTimeline) {
if (event.getType() !== "m.room.message") { if (event.getType() !== "m.room.message") {
return; // only use messages return; // only use messages
} }
console.log(event.event.content.body); console.log(event.event.content.body);
}); });
``` ```
@ -104,73 +101,70 @@ By default, the `matrix-js-sdk` client uses the `MemoryStore` to store events as
```javascript ```javascript
Object.keys(client.store.rooms).forEach((roomId) => { Object.keys(client.store.rooms).forEach((roomId) => {
client.getRoom(roomId).timeline.forEach(t => { client.getRoom(roomId).timeline.forEach((t) => {
console.log(t.event); 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 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: 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 syncing (via `/initialSync` and `/events`)
- Handles historical `RoomMember` information (e.g. display names). - Handles the generation of "friendly" room and member names.
- Manages room member state across multiple events (e.g. it handles typing, power - Handles historical `RoomMember` information (e.g. display names).
levels and membership changes). - Manages room member state across multiple events (e.g. it handles typing, power
- Exposes high-level objects like `Rooms`, `RoomState`, `RoomMembers` and `Users` levels and membership changes).
which can be listened to for things like name changes, new messages, membership - Exposes high-level objects like `Rooms`, `RoomState`, `RoomMembers` and `Users`
changes, presence changes, and more. which can be listened to for things like name changes, new messages, membership
- Handle "local echo" of messages sent using the SDK. This means that messages changes, presence changes, and more.
that have just been sent will appear in the timeline as 'sending', until it - Handle "local echo" of messages sent using the SDK. This means that messages
completes. This is beneficial because it prevents there being a gap between that have just been sent will appear in the timeline as 'sending', until it
hitting the send button and having the "remote echo" arrive. completes. This is beneficial because it prevents there being a gap between
- Mark messages which failed to send as not sent. hitting the send button and having the "remote echo" arrive.
- Automatically retry requests to send messages due to network errors. - Mark messages which failed to send as not sent.
- Automatically retry requests to send messages due to rate limiting errors. - Automatically retry requests to send messages due to network errors.
- Handle queueing of messages. - Automatically retry requests to send messages due to rate limiting errors.
- Handles pagination. - Handle queueing of messages.
- Handle assigning push actions for events. - Handles pagination.
- Handles room initial sync on accepting invites. - Handle assigning push actions for events.
- Handles WebRTC calling. - Handles room initial sync on accepting invites.
- Handles WebRTC calling.
Later versions of the SDK will: 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 ### Emitted events
The SDK will emit events using an ``EventEmitter``. It also The SDK will emit events using an `EventEmitter`. It also
emits object models (e.g. ``Rooms``, ``RoomMembers``) when they emits object models (e.g. `Rooms`, `RoomMembers`) when they
are updated. are updated.
```javascript ```javascript
// Listen for low-level MatrixEvents // Listen for low-level MatrixEvents
client.on("event", function(event) { client.on("event", function (event) {
console.log(event.getType()); console.log(event.getType());
}); });
// Listen for typing changes // Listen for typing changes
client.on("RoomMember.typing", function(event, member) { client.on("RoomMember.typing", function (event, member) {
if (member.typing) { 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 // start the client to setup the connection to the server
client.startClient(); client.startClient();
``` ```
### Promises and Callbacks ### Promises and Callbacks
@ -187,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: you can pass the result of the promise into it with something like:
```javascript ```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 The main thing to note is that it is problematic to discard the result of a
@ -199,61 +193,65 @@ promise-returning function, as that will cause exceptions to go unobserved.
Methods which return a promise show this in their documentation. Methods which return a promise show this in their documentation.
Many methods in the SDK support *both* Node.js-style callbacks *and* Promises, Many methods in the SDK support _both_ Node.js-style callbacks _and_ Promises,
via an optional ``callback`` argument. The callback support is now deprecated: 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 new methods do not include a `callback` argument, and in the future it may be
removed from existing methods. removed from existing methods.
Examples ## Examples
--------
This section provides some useful code snippets which demonstrate the This section provides some useful code snippets which demonstrate the
core functionality of the SDK. These examples assume the SDK is setup like this: core functionality of the SDK. These examples assume the SDK is setup like this:
```javascript ```javascript
import * as sdk from "matrix-js-sdk"; import * as sdk from "matrix-js-sdk";
const myUserId = "@example:localhost"; const myUserId = "@example:localhost";
const myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP"; const myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
const matrixClient = sdk.createClient({ const matrixClient = sdk.createClient({
baseUrl: "http://localhost:8008", baseUrl: "http://localhost:8008",
accessToken: myAccessToken, accessToken: myAccessToken,
userId: myUserId userId: myUserId,
}); });
``` ```
### Automatically join rooms when invited ### Automatically join rooms when invited
```javascript ```javascript
matrixClient.on("RoomMember.membership", function(event, member) { matrixClient.on("RoomMember.membership", function (event, member) {
if (member.membership === "invite" && member.userId === myUserId) { if (member.membership === "invite" && member.userId === myUserId) {
matrixClient.joinRoom(member.roomId).then(function() { matrixClient.joinRoom(member.roomId).then(function () {
console.log("Auto-joined %s", member.roomId); console.log("Auto-joined %s", member.roomId);
}); });
} }
}); });
matrixClient.startClient(); matrixClient.startClient();
``` ```
### Print out messages for all rooms ### Print out messages for all rooms
```javascript ```javascript
matrixClient.on("Room.timeline", function(event, room, toStartOfTimeline) { matrixClient.on("Room.timeline", function (event, room, toStartOfTimeline) {
if (toStartOfTimeline) { if (toStartOfTimeline) {
return; // don't print paginated results return; // don't print paginated results
} }
if (event.getType() !== "m.room.message") { if (event.getType() !== "m.room.message") {
return; // only print messages return; // only print messages
} }
console.log( console.log(
// the room name will update with m.room.name events automatically // the room name will update with m.room.name events automatically
"(%s) %s :: %s", room.name, event.getSender(), event.getContent().body "(%s) %s :: %s",
); room.name,
}); event.getSender(),
event.getContent().body,
);
});
matrixClient.startClient(); matrixClient.startClient();
``` ```
Output: Output:
``` ```
(My Room) @megan:localhost :: Hello world (My Room) @megan:localhost :: Hello world
(My Room) @megan:localhost :: how are you? (My Room) @megan:localhost :: how are you?
@ -265,27 +263,24 @@ Output:
### Print out membership lists whenever they are changed ### Print out membership lists whenever they are changed
```javascript ```javascript
matrixClient.on("RoomState.members", function(event, state, member) { matrixClient.on("RoomState.members", function (event, state, member) {
const room = matrixClient.getRoom(state.roomId); const room = matrixClient.getRoom(state.roomId);
if (!room) { if (!room) {
return; return;
} }
const memberList = state.getMembers(); const memberList = state.getMembers();
console.log(room.name); console.log(room.name);
console.log(Array(room.name.length + 1).join("=")); // underline console.log(Array(room.name.length + 1).join("=")); // underline
for (var i = 0; i < memberList.length; i++) { for (var i = 0; i < memberList.length; i++) {
console.log( console.log("(%s) %s", memberList[i].membership, memberList[i].name);
"(%s) %s", }
memberList[i].membership, });
memberList[i].name
);
}
});
matrixClient.startClient(); matrixClient.startClient();
``` ```
Output: Output:
``` ```
My Room My Room
======= =======
@ -295,8 +290,7 @@ Output:
(invite) @charlie:localhost (invite) @charlie:localhost
``` ```
API Reference # API Reference
=============
A hosted reference can be found at A hosted reference can be found at
http://matrix-org.github.io/matrix-js-sdk/index.html http://matrix-org.github.io/matrix-js-sdk/index.html
@ -310,21 +304,20 @@ host the API reference from the source files like this:
$ python -m http.server 8005 $ 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 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 [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 It is also necessary to call `await matrixClient.initCrypto()` after creating a new
``MatrixClient`` (but **before** calling ``matrixClient.startClient()``) to `MatrixClient` (but **before** calling `matrixClient.startClient()`) to
initialise the crypto layer. initialise the crypto layer.
If the ``Olm`` global is not available, the SDK will show a warning, as shown If the `Olm` global is not available, the SDK will show a warning, as shown
below; ``initCrypto()`` will also fail. below; `initCrypto()` will also fail.
``` ```
Unable to load crypto module: crypto will be disabled: Error: global.Olm is not defined Unable to load crypto module: crypto will be disabled: Error: global.Olm is not defined
@ -336,41 +329,42 @@ specification.
To provide the Olm library in a browser application: To provide the Olm library in a browser application:
* download the transpiled libolm (from https://packages.matrix.org/npm/olm/). - download the transpiled libolm (from https://packages.matrix.org/npm/olm/).
* load ``olm.js`` as a ``<script>`` *before* ``browser-matrix.js``. - load `olm.js` as a `<script>` _before_ `browser-matrix.js`.
To provide the Olm library in a node.js application: To provide the Olm library in a node.js application:
* ``yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz`` - `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 (replace the URL with the latest version you want to use from
https://packages.matrix.org/npm/olm/) 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 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 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 application also works without e2e crypto enabled, add `--optional` to mark it
as an optional dependency. 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: First, you need to pull in the right build tools:
``` ```
$ yarn install $ yarn install
``` ```
Building ## Building
--------
To build a browser version from scratch when developing:: To build a browser version from scratch when developing::
``` ```
$ yarn build $ yarn build
``` ```
To run tests (Jest): To run tests (Jest):
``` ```
$ yarn test $ yarn test
``` ```
@ -379,6 +373,7 @@ To run tests (Jest):
> The `sync-browserify.spec.ts` requires a browser build (`yarn build`) in order to pass > The `sync-browserify.spec.ts` requires a browser build (`yarn build`) in order to pass
To run linting: To run linting:
``` ```
$ yarn lint $ yarn lint
``` ```

View File

@ -20,19 +20,19 @@ blurrier.
When we are low on disk space overall or near the group limit / origin quota: When we are low on disk space overall or near the group limit / origin quota:
* Chrome - Chrome
* Log database may fail to start with AbortError - Log database may fail to start with AbortError
* IndexedDB fails to start for crypto: AbortError in connect from - IndexedDB fails to start for crypto: AbortError in connect from
indexeddb-store-worker indexeddb-store-worker
* When near the quota, QuotaExceededError is used more consistently - When near the quota, QuotaExceededError is used more consistently
* Firefox - Firefox
* The first error will be QuotaExceededError - The first error will be QuotaExceededError
* Future write attempts will fail with various errors when space is low, - Future write attempts will fail with various errors when space is low,
including nonsense like "InvalidStateError: A mutation operation was including nonsense like "InvalidStateError: A mutation operation was
attempted on a database that did not allow mutations." attempted on a database that did not allow mutations."
* Once you start getting errors, the DB is effectively wedged in read-only - Once you start getting errors, the DB is effectively wedged in read-only
mode mode
* Can revive access if you reopen the DB - Can revive access if you reopen the DB
## Cache Eviction ## 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 separately from the others, so it has a separate quota limit and isn't evicted
when low on space. when low on space.
* Chrome, Firefox - Chrome, Firefox
* IndexedDB for origin deleted - IndexedDB for origin deleted
* Local Storage remains in place - Local Storage remains in place
## Persistent Storage ## 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 request persistent storage that won't be deleted by the browser because of low
space. space.
* Chrome - Chrome
* Chrome 75 seems to grant this without any prompt based on [interaction - Chrome 75 seems to grant this without any prompt based on [interaction
criteria](https://developers.google.com/web/updates/2016/06/persistent-storage) criteria](https://developers.google.com/web/updates/2016/06/persistent-storage)
* Firefox - Firefox
* Firefox 67 shows a prompt to grant - Firefox 67 shows a prompt to grant
* Reverting persistent seems to require revoking permission _and_ clearing - Reverting persistent seems to require revoking permission _and_ clearing
site data site data
## Storage Estimation ## Storage Estimation
Storage Standard offers a `navigator.storage.estimate` API to get some clue of Storage Standard offers a `navigator.storage.estimate` API to get some clue of
how much space remains. how much space remains.
* Chrome, Firefox - Chrome, Firefox
* Can run this at any time to request an estimate of space remaining - Can run this at any time to request an estimate of space remaining
* Firefox - Firefox
* Returns `0` for `usage` if a site is persisted - Returns `0` for `usage` if a site is persisted

View File

@ -6,4 +6,4 @@ To try it out, **you must build the SDK first** and then host this folder:
$ python -m http.server 8003 $ python -m http.server 8003
``` ```
Then visit ``http://localhost:8003``. Then visit `http://localhost:8003`.

View File

@ -1,6 +1,6 @@
console.log("Loading browser sdk"); console.log("Loading browser sdk");
var client = matrixcs.createClient({baseUrl: "https://matrix.org"}); var client = matrixcs.createClient({ baseUrl: "https://matrix.org" });
client.publicRooms().then(function (data) { client.publicRooms().then(function (data) {
console.log("data %s [...]", JSON.stringify(data).substring(0, 100)); console.log("data %s [...]", JSON.stringify(data).substring(0, 100));
console.log("Congratulations! The SDK is working on the browser!"); console.log("Congratulations! The SDK is working on the browser!");

View File

@ -1,18 +1,17 @@
<html lang="en"> <html lang="en">
<head> <head>
<title>Test</title> <title>Test</title>
<meta charset="utf-8"/> <meta charset="utf-8" />
<link rel="icon" href="data:,"> <link rel="icon" href="data:," />
<script src="lib/matrix.js"></script> <script src="lib/matrix.js"></script>
<script src="browserTest.js"></script> <script src="browserTest.js"></script>
</head> </head>
<body> <body>
Sanity Testing (check the console) : This example is here to make sure that Sanity Testing (check the console) : This example is here to make sure that the SDK works inside a browser. It
the SDK works inside a browser. It simply does a GET /publicRooms on simply does a GET /publicRooms on matrix.org
matrix.org <br />
<br/> You should see a message confirming that the SDK works below:
You should see a message confirming that the SDK works below: <br />
<br/> <div id="result"></div>
<div id="result"></div> </body>
</body>
</html> </html>

View File

@ -1,59 +1,60 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge"> <meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Test Crypto in Browser</title> <title>Test Crypto in Browser</title>
<script src="lib/olm.js"></script> <script src="lib/olm.js"></script>
<script src="lib/matrix.js"></script> <script src="lib/matrix.js"></script>
</head> </head>
<body> <body>
<h1>Testing export/import of Olm devices in the browser</h1> <h1>Testing export/import of Olm devices in the browser</h1>
<ul> <ul>
<li> <li>Make sure you built the current version of the Matrix JS SDK (<code>yarn build</code>)</li>
Make sure you built the current version of the Matrix JS SDK <li>
(<code>yarn build</code>) copy <code>olm.js</code> and <code>olm.wasm</code> from a recent release of Olm (was tested with version
</li> 3.1.4) in directory <code>lib/</code>
<li> </li>
copy <code>olm.js</code> and <code>olm.wasm</code> <li>start a local Matrix homeserver (on port 8008, or change the port in the code)</li>
from a recent release of Olm (was tested with version 3.1.4) <li>Serve this HTML file (e.g. <code>python3 -m http.server</code>) and go to it through your browser</li>
in directory <code>lib/</code> <li>
</li> in the JS console, do:
<li>start a local Matrix homeserver (on port 8008, or change the port in the code)</li> <pre>
<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()); aliceMatrixClient = await newMatrixClient("alice-"+randomHex());
await aliceMatrixClient.exportDevice(); await aliceMatrixClient.exportDevice();
await aliceMatrixClient.getAccessToken(); await aliceMatrixClient.getAccessToken();
</pre> </pre
</li> >
<li> </li>
copy the result of <code>exportDevice</code> and <code>getAccessToken</code> somewhere <li>
(<strong>not</strong> in a JS variable as it will be destroyed when you refresh the page) copy the result of <code>exportDevice</code> and <code>getAccessToken</code> somewhere (<strong
</li> >not</strong
<li><strong>refresh the page (F5)</strong> to make sure the client is destroyed</li> >
<li> in a JS variable as it will be destroyed when you refresh the page)
Do the following, replacing <code>ALICE_ID</code> </li>
with the user ID of Alice (you can find it in the exported data) <li><strong>refresh the page (F5)</strong> to make sure the client is destroyed</li>
<pre> <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()); bobMatrixClient = await newMatrixClient("bob-"+randomHex());
roomId = await bobMatrixClient.createEncryptedRoom([ALICE_ID]); roomId = await bobMatrixClient.createEncryptedRoom([ALICE_ID]);
await bobMatrixClient.sendTextMessage('Hi Alice!', roomId); await bobMatrixClient.sendTextMessage('Hi Alice!', roomId);
</pre> </pre
</li> >
<li>Again, <strong>refresh the page (F5)</strong>. You may want to clear your console as well.</li> </li>
<li> <li>Again, <strong>refresh the page (F5)</strong>. You may want to clear your console as well.</li>
Now do the following, using the exported data and the access token you saved previously: <li>
<pre> Now do the following, using the exported data and the access token you saved previously:
<pre>
aliceMatrixClient = await importMatrixClient(EXPORTED_DATA, ACCESS_TOKEN); aliceMatrixClient = await importMatrixClient(EXPORTED_DATA, ACCESS_TOKEN);
</pre> </pre
</li> >
<li>You should see the message sent by Bob printed in the console.</li> </li>
</ul> <li>You should see the message sent by Bob printed in the console.</li>
</ul>
<script src="olm-device-export-import.js"></script> <script src="olm-device-export-import.js"></script>
</body> </body>
</html> </html>

View File

@ -1,34 +1,26 @@
if (!Olm) { if (!Olm) {
console.error( console.error("global.Olm does not seem to be present." + " Did you forget to add olm in the lib/ directory?");
"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 BASE_URL = "http://localhost:8008";
const ROOM_CRYPTO_CONFIG = { algorithm: 'm.megolm.v1.aes-sha2' }; const ROOM_CRYPTO_CONFIG = { algorithm: "m.megolm.v1.aes-sha2" };
const PASSWORD = 'password'; const PASSWORD = "password";
// useful to create new usernames // 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) { window.newMatrixClient = async function (username) {
const registrationClient = matrixcs.createClient(BASE_URL); const registrationClient = matrixcs.createClient(BASE_URL);
const userRegisterResult = await registrationClient.register( const userRegisterResult = await registrationClient.register(username, PASSWORD, null, { type: "m.login.dummy" });
username,
PASSWORD,
null,
{ type: 'm.login.dummy' }
);
const matrixClient = matrixcs.createClient({ const matrixClient = matrixcs.createClient({
baseUrl: BASE_URL, baseUrl: BASE_URL,
userId: userRegisterResult.user_id, userId: userRegisterResult.user_id,
accessToken: userRegisterResult.access_token, accessToken: userRegisterResult.access_token,
deviceId: userRegisterResult.device_id, deviceId: userRegisterResult.device_id,
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage), sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
cryptoStore: new matrixcs.MemoryCryptoStore(), cryptoStore: new matrixcs.MemoryCryptoStore(),
}); });
extendMatrixClient(matrixClient); extendMatrixClient(matrixClient);
@ -36,15 +28,15 @@ window.newMatrixClient = async function (username) {
await matrixClient.initCrypto(); await matrixClient.initCrypto();
await matrixClient.startClient(); await matrixClient.startClient();
return matrixClient; return matrixClient;
} };
window.importMatrixClient = async function (exportedDevice, accessToken) { window.importMatrixClient = async function (exportedDevice, accessToken) {
const matrixClient = matrixcs.createClient({ const matrixClient = matrixcs.createClient({
baseUrl: BASE_URL, baseUrl: BASE_URL,
deviceToImport: exportedDevice, deviceToImport: exportedDevice,
accessToken, accessToken,
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage), sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
cryptoStore: new matrixcs.MemoryCryptoStore(), cryptoStore: new matrixcs.MemoryCryptoStore(),
}); });
extendMatrixClient(matrixClient); extendMatrixClient(matrixClient);
@ -52,71 +44,62 @@ window.importMatrixClient = async function (exportedDevice, accessToken) {
await matrixClient.initCrypto(); await matrixClient.initCrypto();
await matrixClient.startClient(); await matrixClient.startClient();
return matrixClient; return matrixClient;
} };
function extendMatrixClient(matrixClient) { function extendMatrixClient(matrixClient) {
// automatic join // automatic join
matrixClient.on('RoomMember.membership', async (event, member) => { matrixClient.on("RoomMember.membership", async (event, member) => {
if (member.membership === 'invite' && member.userId === matrixClient.getUserId()) { if (member.membership === "invite" && member.userId === matrixClient.getUserId()) {
await matrixClient.joinRoom(member.roomId); await matrixClient.joinRoom(member.roomId);
// setting up of room encryption seems to be triggered automatically // 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 // 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 => { matrixClient.onDecryptedMessage = (message) => {
console.log('Got encrypted message: ', message); console.log("Got encrypted message: ", message);
} };
matrixClient.on('Event.decrypted', (event) => { matrixClient.on("Event.decrypted", (event) => {
if (event.getType() === 'm.room.message'){ if (event.getType() === "m.room.message") {
matrixClient.onDecryptedMessage(event.getContent().body); matrixClient.onDecryptedMessage(event.getContent().body);
} else { } else {
console.log('decrypted an event of type', event.getType()); console.log("decrypted an event of type", event.getType());
console.log(event); console.log(event);
} }
}); });
matrixClient.createEncryptedRoom = async function(usersToInvite) { matrixClient.createEncryptedRoom = async function (usersToInvite) {
const { const { room_id: roomId } = await this.createRoom({
room_id: roomId, visibility: "private",
} = await this.createRoom({ invite: usersToInvite,
visibility: 'private',
invite: usersToInvite,
}); });
// matrixClient.setRoomEncryption() only updates local state // matrixClient.setRoomEncryption() only updates local state
// but does not send anything to the server // but does not send anything to the server
// (see https://github.com/matrix-org/matrix-js-sdk/issues/905) // (see https://github.com/matrix-org/matrix-js-sdk/issues/905)
// so we do it ourselves with 'sendStateEvent' // so we do it ourselves with 'sendStateEvent'
await this.sendStateEvent( await this.sendStateEvent(roomId, "m.room.encryption", ROOM_CRYPTO_CONFIG);
roomId, 'm.room.encryption', ROOM_CRYPTO_CONFIG, await this.setRoomEncryption(roomId, ROOM_CRYPTO_CONFIG);
);
await this.setRoomEncryption(
roomId, ROOM_CRYPTO_CONFIG,
);
// Marking all devices as verified // Marking all devices as verified
let room = this.getRoom(roomId); 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); let memberkeys = await this.downloadKeys(members);
for (const userId in memberkeys) { for (const userId in memberkeys) {
for (const deviceId in memberkeys[userId]) { for (const deviceId in memberkeys[userId]) {
await this.setDeviceVerified(userId, deviceId); await this.setDeviceVerified(userId, deviceId);
} }
} }
return roomId; return roomId;
} };
matrixClient.sendTextMessage = async function(message, roomId) { matrixClient.sendTextMessage = async function (message, roomId) {
return matrixClient.sendMessage( return matrixClient.sendMessage(roomId, {
roomId, body: message,
{ msgtype: "m.text",
body: message, });
msgtype: 'm.text', };
} }
)
}
}

View File

@ -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. 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: 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: Room commands:
'/exit' Return to the room list index. '/exit' Return to the room list index.
'/members' Show the room member list. '/members' Show the room member list.
$ /enter 2 $ /enter 2
[2015-06-12 15:14:54] Megan2 <<< herro [2015-06-12 15:14:54] Megan2 <<< herro

View File

@ -5,7 +5,7 @@ var clc = require("cli-color");
var matrixClient = sdk.createClient({ var matrixClient = sdk.createClient({
baseUrl: "http://localhost:8008", baseUrl: "http://localhost:8008",
accessToken: myAccessToken, accessToken: myAccessToken,
userId: myUserId userId: myUserId,
}); });
// Data structures // Data structures
@ -14,15 +14,15 @@ var viewingRoom = null;
var numMessagesToShow = 20; var numMessagesToShow = 20;
// Reading from stdin // Reading from stdin
var CLEAR_CONSOLE = '\x1B[2J'; var CLEAR_CONSOLE = "\x1B[2J";
var readline = require("readline"); var readline = require("readline");
var rl = readline.createInterface({ var rl = readline.createInterface({
input: process.stdin, input: process.stdin,
output: process.stdout, output: process.stdout,
completer: completer completer: completer,
}); });
rl.setPrompt("$ "); rl.setPrompt("$ ");
rl.on('line', function(line) { rl.on("line", function (line) {
if (line.trim().length === 0) { if (line.trim().length === 0) {
rl.prompt(); rl.prompt();
return; return;
@ -37,14 +37,11 @@ rl.on('line', function(line) {
if (line === "/exit") { if (line === "/exit") {
viewingRoom = null; viewingRoom = null;
printRoomList(); printRoomList();
} } else if (line === "/members") {
else if (line === "/members") {
printMemberList(viewingRoom); printMemberList(viewingRoom);
} } else if (line === "/roominfo") {
else if (line === "/roominfo") {
printRoomInfo(viewingRoom); printRoomInfo(viewingRoom);
} } else if (line === "/resend") {
else if (line === "/resend") {
// get the oldest not sent event. // get the oldest not sent event.
var notSentEvent; var notSentEvent;
for (var i = 0; i < viewingRoom.timeline.length; i++) { for (var i = 0; i < viewingRoom.timeline.length; i++) {
@ -54,76 +51,84 @@ rl.on('line', function(line) {
} }
} }
if (notSentEvent) { if (notSentEvent) {
matrixClient.resendEvent(notSentEvent, viewingRoom).then(function() { matrixClient.resendEvent(notSentEvent, viewingRoom).then(
printMessages(); function () {
rl.prompt(); printMessages();
}, function(err) { rl.prompt();
printMessages(); },
print("/resend Error: %s", err); function (err) {
rl.prompt(); printMessages();
}); print("/resend Error: %s", err);
rl.prompt();
},
);
printMessages(); printMessages();
rl.prompt(); rl.prompt();
} }
} } else if (line.indexOf("/more ") === 0) {
else if (line.indexOf("/more ") === 0) {
var amount = parseInt(line.split(" ")[1]) || 20; var amount = parseInt(line.split(" ")[1]) || 20;
matrixClient.scrollback(viewingRoom, amount).then(function(room) { matrixClient.scrollback(viewingRoom, amount).then(
printMessages(); function (room) {
rl.prompt(); printMessages();
}, function(err) { rl.prompt();
print("/more Error: %s", err); },
}); function (err) {
} print("/more Error: %s", err);
else if (line.indexOf("/invite ") === 0) { },
);
} else if (line.indexOf("/invite ") === 0) {
var userId = line.split(" ")[1].trim(); var userId = line.split(" ")[1].trim();
matrixClient.invite(viewingRoom.roomId, userId).then(function() { matrixClient.invite(viewingRoom.roomId, userId).then(
printMessages(); function () {
rl.prompt(); printMessages();
}, function(err) { rl.prompt();
print("/invite Error: %s", err); },
}); function (err) {
} print("/invite Error: %s", err);
else if (line.indexOf("/file ") === 0) { },
);
} else if (line.indexOf("/file ") === 0) {
var filename = line.split(" ")[1].trim(); var filename = line.split(" ")[1].trim();
var stream = fs.createReadStream(filename); var stream = fs.createReadStream(filename);
matrixClient.uploadContent({ matrixClient
stream: stream, .uploadContent({
name: filename stream: stream,
}).then(function(url) { name: filename,
var content = { })
msgtype: "m.file", .then(function (url) {
body: filename, var content = {
url: JSON.parse(url).content_uri msgtype: "m.file",
}; body: filename,
matrixClient.sendMessage(viewingRoom.roomId, content); url: JSON.parse(url).content_uri,
}); };
} matrixClient.sendMessage(viewingRoom.roomId, content);
else { });
matrixClient.sendTextMessage(viewingRoom.roomId, line).finally(function() { } else {
matrixClient.sendTextMessage(viewingRoom.roomId, line).finally(function () {
printMessages(); printMessages();
rl.prompt(); rl.prompt();
}); });
// print local echo immediately // print local echo immediately
printMessages(); printMessages();
} }
} } else {
else {
if (line.indexOf("/join ") === 0) { if (line.indexOf("/join ") === 0) {
var roomIndex = line.split(" ")[1]; var roomIndex = line.split(" ")[1];
viewingRoom = roomList[roomIndex]; viewingRoom = roomList[roomIndex];
if (viewingRoom.getMember(myUserId).membership === "invite") { if (viewingRoom.getMember(myUserId).membership === "invite") {
// join the room first // join the room first
matrixClient.joinRoom(viewingRoom.roomId).then(function(room) { matrixClient.joinRoom(viewingRoom.roomId).then(
setRoomList(); function (room) {
viewingRoom = room; setRoomList();
printMessages(); viewingRoom = room;
rl.prompt(); printMessages();
}, function(err) { rl.prompt();
print("/join Error: %s", err); },
}); function (err) {
} print("/join Error: %s", err);
else { },
);
} else {
printMessages(); printMessages();
} }
} }
@ -133,18 +138,18 @@ rl.on('line', function(line) {
// ==== END User input // ==== END User input
// show the room list after syncing. // show the room list after syncing.
matrixClient.on("sync", function(state, prevState, data) { matrixClient.on("sync", function (state, prevState, data) {
switch (state) { switch (state) {
case "PREPARED": case "PREPARED":
setRoomList(); setRoomList();
printRoomList(); printRoomList();
printHelp(); printHelp();
rl.prompt(); rl.prompt();
break; break;
} }
}); });
matrixClient.on("Room", function() { matrixClient.on("Room", function () {
setRoomList(); setRoomList();
if (!viewingRoom) { if (!viewingRoom) {
printRoomList(); printRoomList();
@ -153,7 +158,7 @@ matrixClient.on("Room", function() {
}); });
// print incoming messages. // print incoming messages.
matrixClient.on("Room.timeline", function(event, room, toStartOfTimeline) { matrixClient.on("Room.timeline", function (event, room, toStartOfTimeline) {
if (toStartOfTimeline) { if (toStartOfTimeline) {
return; // don't print paginated results return; // don't print paginated results
} }
@ -165,20 +170,19 @@ matrixClient.on("Room.timeline", function(event, room, toStartOfTimeline) {
function setRoomList() { function setRoomList() {
roomList = matrixClient.getRooms(); roomList = matrixClient.getRooms();
roomList.sort(function(a,b) { roomList.sort(function (a, b) {
// < 0 = a comes first (lower index) - we want high indexes = newer // < 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) { if (!aMsg) {
return -1; return -1;
} }
var bMsg = b.timeline[b.timeline.length-1]; var bMsg = b.timeline[b.timeline.length - 1];
if (!bMsg) { if (!bMsg) {
return 1; return 1;
} }
if (aMsg.getTs() > bMsg.getTs()) { if (aMsg.getTs() > bMsg.getTs()) {
return 1; return 1;
} } else if (aMsg.getTs() < bMsg.getTs()) {
else if (aMsg.getTs() < bMsg.getTs()) {
return -1; return -1;
} }
return 0; return 0;
@ -189,16 +193,15 @@ function printRoomList() {
print(CLEAR_CONSOLE); print(CLEAR_CONSOLE);
print("Room List:"); print("Room List:");
var fmts = { var fmts = {
"invite": clc.cyanBright, invite: clc.cyanBright,
"leave": clc.blackBright leave: clc.blackBright,
}; };
for (var i = 0; i < roomList.length; i++) { 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 dateStr = "---";
var fmt; var fmt;
if (msg) { if (msg) {
dateStr = new Date(msg.getTs()).toISOString().replace( dateStr = new Date(msg.getTs()).toISOString().replace(/T/, " ").replace(/\..+/, "");
/T/, ' ').replace(/\..+/, '');
} }
var myMembership = roomList[i].getMyMembership(); var myMembership = roomList[i].getMyMembership();
if (myMembership) { if (myMembership) {
@ -207,9 +210,10 @@ function printRoomList() {
var roomName = fixWidth(roomList[i].name, 25); var roomName = fixWidth(roomList[i].name, 25);
print( print(
"[%s] %s (%s members) %s", "[%s] %s (%s members) %s",
i, fmt ? fmt(roomName) : roomName, i,
fmt ? fmt(roomName) : roomName,
roomList[i].getJoinedMembers().length, roomList[i].getJoinedMembers().length,
dateStr dateStr,
); );
} }
} }
@ -230,12 +234,12 @@ function printHelp() {
} }
function completer(line) { function completer(line) {
var completions = [ var completions = ["/help", "/join ", "/exit", "/members", "/more ", "/resend", "/invite"];
"/help", "/join ", "/exit", "/members", "/more ", "/resend", "/invite" var hits = completions.filter(function (c) {
]; return c.indexOf(line) == 0;
var hits = completions.filter(function(c) { return c.indexOf(line) == 0 }); });
// show all completions if none found // show all completions if none found
return [hits.length ? hits : completions, line] return [hits.length ? hits : completions, line];
} }
function printMessages() { function printMessages() {
@ -252,14 +256,14 @@ function printMessages() {
function printMemberList(room) { function printMemberList(room) {
var fmts = { var fmts = {
"join": clc.green, join: clc.green,
"ban": clc.red, ban: clc.red,
"invite": clc.blue, invite: clc.blue,
"leave": clc.blackBright leave: clc.blackBright,
}; };
var members = room.currentState.getMembers(); var members = room.currentState.getMembers();
// sorted based on name. // sorted based on name.
members.sort(function(a, b) { members.sort(function (a, b) {
if (a.name > b.name) { if (a.name > b.name) {
return -1; return -1;
} }
@ -268,21 +272,24 @@ function printMemberList(room) {
} }
return 0; 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("-")); print(new Array(room.name.length + 28).join("-"));
room.currentState.getMembers().forEach(function(member) { room.currentState.getMembers().forEach(function (member) {
if (!member.membership) { if (!member.membership) {
return; return;
} }
var fmt = fmts[member.membership] || function(a){return a;}; var fmt =
var membershipWithPadding = ( fmts[member.membership] ||
member.membership + new Array(10 - member.membership.length).join(" ") function (a) {
); return a;
};
var membershipWithPadding = member.membership + new Array(10 - member.membership.length).join(" ");
print( print(
"%s"+fmt(" :: ")+"%s"+fmt(" (")+"%s"+fmt(")"), "%s" + fmt(" :: ") + "%s" + fmt(" (") + "%s" + fmt(")"),
membershipWithPadding, member.name, membershipWithPadding,
(member.userId === myUserId ? "Me" : member.userId), member.name,
fmt member.userId === myUserId ? "Me" : member.userId,
fmt,
); );
}); });
} }
@ -292,38 +299,31 @@ function printRoomInfo(room) {
var eTypeHeader = " Event Type(state_key) "; var eTypeHeader = " Event Type(state_key) ";
var sendHeader = " Sender "; var sendHeader = " Sender ";
// pad content to 100 // pad content to 100
var restCount = ( var restCount = 100 - "Content".length - " | ".length - " | ".length - eTypeHeader.length - sendHeader.length;
100 - "Content".length - " | ".length - " | ".length - var padSide = new Array(Math.floor(restCount / 2)).join(" ");
eTypeHeader.length - sendHeader.length
);
var padSide = new Array(Math.floor(restCount/2)).join(" ");
var contentHeader = padSide + "Content" + padSide; var contentHeader = padSide + "Content" + padSide;
print(eTypeHeader+sendHeader+contentHeader); print(eTypeHeader + sendHeader + contentHeader);
print(new Array(100).join("-")); print(new Array(100).join("-"));
eventMap.keys().forEach(function(eventType) { eventMap.keys().forEach(function (eventType) {
if (eventType === "m.room.member") { return; } // use /members instead. if (eventType === "m.room.member") {
return;
} // use /members instead.
var eventEventMap = eventMap.get(eventType); var eventEventMap = eventMap.get(eventType);
eventEventMap.keys().forEach(function(stateKey) { eventEventMap.keys().forEach(function (stateKey) {
var typeAndKey = eventType + ( var typeAndKey = eventType + (stateKey.length > 0 ? "(" + stateKey + ")" : "");
stateKey.length > 0 ? "("+stateKey+")" : ""
);
var typeStr = fixWidth(typeAndKey, eTypeHeader.length); var typeStr = fixWidth(typeAndKey, eTypeHeader.length);
var event = eventEventMap.get(stateKey); var event = eventEventMap.get(stateKey);
var sendStr = fixWidth(event.getSender(), sendHeader.length); var sendStr = fixWidth(event.getSender(), sendHeader.length);
var contentStr = fixWidth( var contentStr = fixWidth(JSON.stringify(event.getContent()), contentHeader.length);
JSON.stringify(event.getContent()), contentHeader.length print(typeStr + " | " + sendStr + " | " + contentStr);
);
print(typeStr+" | "+sendStr+" | "+contentStr);
}); });
}) });
} }
function printLine(event) { function printLine(event) {
var fmt; var fmt;
var name = event.sender ? event.sender.name : event.getSender(); var name = event.sender ? event.sender.name : event.getSender();
var time = new Date( var time = new Date(event.getTs()).toISOString().replace(/T/, " ").replace(/\..+/, "");
event.getTs()
).toISOString().replace(/T/, ' ').replace(/\..+/, '');
var separator = "<<<"; var separator = "<<<";
if (event.getSender() === myUserId) { if (event.getSender() === myUserId) {
name = "Me"; name = "Me";
@ -331,8 +331,7 @@ function printLine(event) {
if (event.status === sdk.EventStatus.SENDING) { if (event.status === sdk.EventStatus.SENDING) {
separator = "..."; separator = "...";
fmt = clc.xterm(8); fmt = clc.xterm(8);
} } else if (event.status === sdk.EventStatus.NOT_SENT) {
else if (event.status === sdk.EventStatus.NOT_SENT) {
separator = " x "; separator = " x ";
fmt = clc.redBright; fmt = clc.redBright;
} }
@ -341,69 +340,58 @@ function printLine(event) {
var maxNameWidth = 15; var maxNameWidth = 15;
if (name.length > maxNameWidth) { if (name.length > maxNameWidth) {
name = name.slice(0, maxNameWidth-1) + "\u2026"; name = name.slice(0, maxNameWidth - 1) + "\u2026";
} }
if (event.getType() === "m.room.message") { if (event.getType() === "m.room.message") {
body = event.getContent().body; body = event.getContent().body;
} } else if (event.isState()) {
else if (event.isState()) {
var stateName = event.getType(); var stateName = event.getType();
if (event.getStateKey().length > 0) { if (event.getStateKey().length > 0) {
stateName += " ("+event.getStateKey()+")"; stateName += " (" + event.getStateKey() + ")";
} }
body = ( body = "[State: " + stateName + " updated to: " + JSON.stringify(event.getContent()) + "]";
"[State: "+stateName+" updated to: "+JSON.stringify(event.getContent())+"]"
);
separator = "---"; separator = "---";
fmt = clc.xterm(249).italic; fmt = clc.xterm(249).italic;
} } else {
else {
// random message event // random message event
body = ( body = "[Message: " + event.getType() + " Content: " + JSON.stringify(event.getContent()) + "]";
"[Message: "+event.getType()+" Content: "+JSON.stringify(event.getContent())+"]"
);
separator = "---"; separator = "---";
fmt = clc.xterm(249).italic; fmt = clc.xterm(249).italic;
} }
if (fmt) { if (fmt) {
print( print("[%s] %s %s %s", time, name, separator, body, fmt);
"[%s] %s %s %s", time, name, separator, body, fmt } else {
);
}
else {
print("[%s] %s %s %s", time, name, separator, body); print("[%s] %s %s %s", time, name, separator, body);
} }
} }
function print(str, formatter) { 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 // last arg is the formatter so get rid of it and use it on each
// param passed in but not the template string. // param passed in but not the template string.
var newArgs = []; var newArgs = [];
var i = 0; var i = 0;
for (i=0; i<arguments.length-1; i++) { for (i = 0; i < arguments.length - 1; i++) {
newArgs.push(arguments[i]); newArgs.push(arguments[i]);
} }
var fmt = arguments[arguments.length-1]; var fmt = arguments[arguments.length - 1];
for (i=0; i<newArgs.length; i++) { for (i = 0; i < newArgs.length; i++) {
newArgs[i] = fmt(newArgs[i]); newArgs[i] = fmt(newArgs[i]);
} }
console.log.apply(console.log, newArgs); console.log.apply(console.log, newArgs);
} } else {
else {
console.log.apply(console.log, arguments); console.log.apply(console.log, arguments);
} }
} }
function fixWidth(str, len) { function fixWidth(str, len) {
if (str.length > len) { if (str.length > len) {
return str.substring(0, len-2) + "\u2026"; return str.substring(0, len - 2) + "\u2026";
} } else if (str.length < len) {
else if (str.length < len) {
return str + new Array(len - str.length).join(" "); return str + new Array(len - str.length).join(" ");
} }
return str; return str;
} }
matrixClient.startClient(numMessagesToShow); // messages for each room. matrixClient.startClient(numMessagesToShow); // messages for each room.

View File

@ -1,14 +1,14 @@
{ {
"name": "example-app", "name": "example-app",
"version": "0.0.0", "version": "0.0.0",
"description": "", "description": "",
"main": "app.js", "main": "app.js",
"scripts": { "scripts": {
"preinstall": "npm install ../.." "preinstall": "npm install ../.."
}, },
"author": "", "author": "",
"license": "Apache 2.0", "license": "Apache 2.0",
"dependencies": { "dependencies": {
"cli-color": "^1.0.0" "cli-color": "^1.0.0"
} }
} }

View File

@ -6,4 +6,4 @@ To try it out, **you must build the SDK first** and then host this folder:
$ python -m SimpleHTTPServer 8003 $ python -m SimpleHTTPServer 8003
``` ```
Then visit ``http://localhost:8003``. Then visit `http://localhost:8003`.

View File

@ -9,7 +9,7 @@ const client = matrixcs.createClient({
baseUrl: BASE_URL, baseUrl: BASE_URL,
accessToken: TOKEN, accessToken: TOKEN,
userId: USER_ID, userId: USER_ID,
deviceId: DEVICE_ID deviceId: DEVICE_ID,
}); });
let call; let call;
@ -21,18 +21,16 @@ function disableButtons(place, answer, hangup) {
function addListeners(call) { function addListeners(call) {
let lastError = ""; let lastError = "";
call.on("hangup", function() { call.on("hangup", function () {
disableButtons(false, true, true); disableButtons(false, true, true);
document.getElementById("result").innerHTML = ( document.getElementById("result").innerHTML = "<p>Call ended. Last error: " + lastError + "</p>";
"<p>Call ended. Last error: "+lastError+"</p>"
);
}); });
call.on("error", function(err) { call.on("error", function (err) {
lastError = err.message; lastError = err.message;
call.hangup(); call.hangup();
disableButtons(false, true, true); disableButtons(false, true, true);
}); });
call.on("feeds_changed", function(feeds) { call.on("feeds_changed", function (feeds) {
const localFeed = feeds.find((feed) => feed.isLocal()); const localFeed = feeds.find((feed) => feed.isLocal());
const remoteFeed = 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("result").innerHTML = "<p>Please wait. Syncing...</p>";
document.getElementById("config").innerHTML = "<p>" + document.getElementById("config").innerHTML =
"Homeserver: <code>"+BASE_URL+"</code><br/>"+ "<p>" +
"Room: <code>"+ROOM_ID+"</code><br/>"+ "Homeserver: <code>" +
"User: <code>"+USER_ID+"</code><br/>"+ BASE_URL +
"</code><br/>" +
"Room: <code>" +
ROOM_ID +
"</code><br/>" +
"User: <code>" +
USER_ID +
"</code><br/>" +
"</p>"; "</p>";
disableButtons(true, true, true); disableButtons(true, true, true);
}; };
client.on("sync", function(state, prevState, data) { client.on("sync", function (state, prevState, data) {
switch (state) { switch (state) {
case "PREPARED": case "PREPARED":
syncComplete(); syncComplete();
break; break;
} }
}); });
function syncComplete() { function syncComplete() {
document.getElementById("result").innerHTML = "<p>Ready for calls.</p>"; document.getElementById("result").innerHTML = "<p>Ready for calls.</p>";
disableButtons(false, true, true); disableButtons(false, true, true);
document.getElementById("call").onclick = function() { document.getElementById("call").onclick = function () {
console.log("Placing call..."); console.log("Placing call...");
call = matrixcs.createNewMatrixCall( call = matrixcs.createNewMatrixCall(client, ROOM_ID);
client, ROOM_ID
);
console.log("Call => %s", call); console.log("Call => %s", call);
addListeners(call); addListeners(call);
call.placeVideoCall(); call.placeVideoCall();
@ -85,14 +88,14 @@ function syncComplete() {
disableButtons(true, true, false); disableButtons(true, true, false);
}; };
document.getElementById("hangup").onclick = function() { document.getElementById("hangup").onclick = function () {
console.log("Hanging up call..."); console.log("Hanging up call...");
console.log("Call => %s", call); console.log("Call => %s", call);
call.hangup(); call.hangup();
document.getElementById("result").innerHTML = "<p>Hungup call.</p>"; document.getElementById("result").innerHTML = "<p>Hungup call.</p>";
}; };
document.getElementById("answer").onclick = function() { document.getElementById("answer").onclick = function () {
console.log("Answering call..."); console.log("Answering call...");
console.log("Call => %s", call); console.log("Call => %s", call);
call.answer(); call.answer();
@ -100,7 +103,7 @@ function syncComplete() {
document.getElementById("result").innerHTML = "<p>Answered call.</p>"; document.getElementById("result").innerHTML = "<p>Answered call.</p>";
}; };
client.on("Call.incoming", function(c) { client.on("Call.incoming", function (c) {
console.log("Call ringing"); console.log("Call ringing");
disableButtons(true, false, false); disableButtons(true, false, false);
document.getElementById("result").innerHTML = "<p>Incoming call...</p>"; document.getElementById("result").innerHTML = "<p>Incoming call...</p>";

View File

@ -1,25 +1,23 @@
<html> <html>
<head>
<title>VoIP Test</title>
<script src="lib/matrix.js"></script>
<script src="browserTest.js"></script>
</head>
<head> <body>
<title>VoIP Test</title> You can place and receive calls with this example. Make sure to edit the constants in
<script src="lib/matrix.js"></script> <code>browserTest.js</code> first.
<script src="browserTest.js"></script> <div id="config"></div>
</head> <div id="result"></div>
<button id="call">Place Call</button>
<body> <button id="answer">Answer Call</button>
You can place and receive calls with this example. Make sure to edit the <button id="hangup">Hangup Call</button>
constants in <code>browserTest.js</code> first. <div id="videoBackground" class="video-background">
<div id="config"></div> <video class="video-element" id="local"></video>
<div id="result"></div> <video class="video-element" id="remote"></video>
<button id="call">Place Call</button> </div>
<button id="answer">Answer Call</button> </body>
<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> </html>
<style> <style>
@ -31,4 +29,4 @@
.video-element { .video-element {
height: 100%; height: 100%;
} }
</style> </style>

View File

@ -1,146 +1,146 @@
{ {
"name": "matrix-js-sdk", "name": "matrix-js-sdk",
"version": "22.0.0", "version": "22.0.0",
"description": "Matrix Client-Server SDK for Javascript", "description": "Matrix Client-Server SDK for Javascript",
"engines": { "engines": {
"node": ">=16.0.0" "node": ">=16.0.0"
}, },
"scripts": { "scripts": {
"prepublishOnly": "yarn build", "prepublishOnly": "yarn build",
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"", "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", "dist": "echo 'This is for the release script so it can make assets (browser bundle).' && yarn build",
"clean": "rimraf lib dist", "clean": "rimraf lib dist",
"build": "yarn build:dev && yarn build:compile-browser && yarn build:minify-browser", "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: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:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src", "build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
"build:compile-browser": "mkdirp 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:compile-browser": "mkdirp 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", "build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
"gendoc": "typedoc", "gendoc": "typedoc",
"lint": "yarn lint:types && yarn lint:js", "lint": "yarn lint:types && yarn lint:js",
"lint:js": "eslint --max-warnings 0 src spec && prettier --check .", "lint:js": "eslint --max-warnings 0 src spec && prettier --check .",
"lint:js-fix": "prettier --loglevel=warn --write . && eslint --fix src spec", "lint:js-fix": "prettier --loglevel=warn --write . && eslint --fix src spec",
"lint:types": "tsc --noEmit", "lint:types": "tsc --noEmit",
"test": "jest", "test": "jest",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"coverage": "yarn test --coverage" "coverage": "yarn test --coverage"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/matrix-org/matrix-js-sdk" "url": "https://github.com/matrix-org/matrix-js-sdk"
}, },
"keywords": [ "keywords": [
"matrix-org" "matrix-org"
],
"main": "./src/index.ts",
"browser": "./lib/browser-index.ts",
"matrix_src_main": "./src/index.ts",
"matrix_src_browser": "./src/browser-index.ts",
"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",
"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",
"uuid": "7"
},
"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.28.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": "https://github.com/matrix-org/eslint-plugin-matrix-org.git#weeman1337/prettier",
"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.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}"
], ],
"setupFilesAfterEnv": [ "main": "./src/index.ts",
"<rootDir>/spec/setupTests.ts" "browser": "./lib/browser-index.ts",
"matrix_src_main": "./src/index.ts",
"matrix_src_browser": "./src/browser-index.ts",
"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"
], ],
"collectCoverageFrom": [ "dependencies": {
"<rootDir>/src/**/*.{js,ts}" "@babel/runtime": "^7.12.5",
], "another-json": "^0.2.0",
"coverageReporters": [ "bs58": "^5.0.0",
"text-summary", "content-type": "^1.0.4",
"lcov" "loglevel": "^1.7.1",
], "matrix-events-sdk": "0.0.1",
"testResultsProcessor": "@casualbot/jest-sonar-reporter" "matrix-widget-api": "^1.0.0",
}, "p-retry": "4",
"@casualbot/jest-sonar-reporter": { "qs": "^6.9.6",
"outputDirectory": "coverage", "sdp-transform": "^2.14.1",
"outputName": "jest-sonar-report.xml", "unhomoglyph": "^1.0.6",
"relativePaths": true "uuid": "7"
} },
"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.28.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": "https://github.com/matrix-org/eslint-plugin-matrix-org.git#weeman1337/prettier",
"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.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}"
],
"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
}
} }

View File

@ -1,14 +1,14 @@
#!/usr/bin/env node #!/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() { async function main() {
const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, 'utf8')); const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, "utf8"));
for (const field of ['main', 'typings']) { for (const field of ["main", "typings"]) {
if (pkgJson["matrix_lib_"+field] !== undefined) { if (pkgJson["matrix_lib_" + field] !== undefined) {
pkgJson[field] = pkgJson["matrix_lib_"+field]; pkgJson[field] = pkgJson["matrix_lib_" + field];
} }
} }
await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2)); await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2));

View File

@ -17,12 +17,12 @@ limitations under the License.
*/ */
// load olm before the sdk if possible // 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 { LocalStorageCryptoStore } from "../src/crypto/store/localStorage-crypto-store";
import { logger } from '../src/logger'; import { logger } from "../src/logger";
import { syncPromise } from "./test-utils/test-utils"; import { syncPromise } from "./test-utils/test-utils";
import { createClient } from "../src/matrix"; import { createClient } from "../src/matrix";
import { ICreateClientOpts, IDownloadKeyResult, MatrixClient, PendingEventOrdering } from "../src/client"; import { ICreateClientOpts, IDownloadKeyResult, MatrixClient, PendingEventOrdering } from "../src/client";
@ -30,7 +30,7 @@ import { MockStorageApi } from "./MockStorageApi";
import { encodeUri } from "../src/utils"; import { encodeUri } from "../src/utils";
import { IDeviceKeys, IOneTimeKey } from "../src/crypto/dehydration"; import { IDeviceKeys, IOneTimeKey } from "../src/crypto/dehydration";
import { IKeyBackupSession } from "../src/crypto/keybackup"; 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 * Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
@ -73,14 +73,14 @@ export class TestClient {
} }
public toString(): string { public toString(): string {
return 'TestClient[' + this.userId + ']'; return "TestClient[" + this.userId + "]";
} }
/** /**
* start the client, and wait for it to initialise. * start the client, and wait for it to initialise.
*/ */
public start(): Promise<void> { public start(): Promise<void> {
logger.log(this + ': starting'); logger.log(this + ": starting");
this.httpBackend.when("GET", "/versions").respond(200, {}); this.httpBackend.when("GET", "/versions").respond(200, {});
this.httpBackend.when("GET", "/pushrules").respond(200, {}); this.httpBackend.when("GET", "/pushrules").respond(200, {});
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" }); this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
@ -95,11 +95,8 @@ export class TestClient {
pendingEventOrdering: PendingEventOrdering.Detached, pendingEventOrdering: PendingEventOrdering.Detached,
}); });
return Promise.all([ return Promise.all([this.httpBackend.flushAllExpected(), syncPromise(this.client)]).then(() => {
this.httpBackend.flushAllExpected(), logger.log(this + ": started");
syncPromise(this.client),
]).then(() => {
logger.log(this + ': started');
}); });
} }
@ -116,12 +113,13 @@ export class TestClient {
* Set up expectations that the client will upload device keys. * Set up expectations that the client will upload device keys.
*/ */
public expectDeviceKeyUpload() { public expectDeviceKeyUpload() {
this.httpBackend.when("POST", "/keys/upload") this.httpBackend
.when("POST", "/keys/upload")
.respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content) => { .respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content) => {
expect(content.one_time_keys).toBe(undefined); expect(content.one_time_keys).toBe(undefined);
expect(content.device_keys).toBeTruthy(); 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. // we expect this to happen before any one-time keys are uploaded.
expect(Object.keys(this.oneTimeKeys!).length).toEqual(0); expect(Object.keys(this.oneTimeKeys!).length).toEqual(0);
@ -143,30 +141,35 @@ export class TestClient {
return Promise.resolve(this.oneTimeKeys!); return Promise.resolve(this.oneTimeKeys!);
} }
this.httpBackend.when("POST", "/keys/upload") this.httpBackend
.when("POST", "/keys/upload")
.respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content: IUploadKeysRequest) => { .respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content: IUploadKeysRequest) => {
expect(content.device_keys).toBe(undefined); expect(content.device_keys).toBe(undefined);
expect(content.one_time_keys).toBe(undefined); expect(content.one_time_keys).toBe(undefined);
return { one_time_key_counts: { return {
signed_curve25519: Object.keys(this.oneTimeKeys!).length, 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) => { .respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content: IUploadKeysRequest) => {
expect(content.device_keys).toBe(undefined); expect(content.device_keys).toBe(undefined);
expect(content.one_time_keys).toBeTruthy(); expect(content.one_time_keys).toBeTruthy();
expect(content.one_time_keys).not.toEqual({}); expect(content.one_time_keys).not.toEqual({});
logger.log('%s: received %i one-time keys', this, logger.log("%s: received %i one-time keys", this, Object.keys(content.one_time_keys!).length);
Object.keys(content.one_time_keys!).length);
this.oneTimeKeys = content.one_time_keys; this.oneTimeKeys = content.one_time_keys;
return { one_time_key_counts: { return {
signed_curve25519: Object.keys(this.oneTimeKeys!).length, one_time_key_counts: {
} }; signed_curve25519: Object.keys(this.oneTimeKeys!).length,
},
};
}); });
// this can take ages // 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); expect(flushed).toEqual(2);
return this.oneTimeKeys!; return this.oneTimeKeys!;
}); });
@ -180,23 +183,27 @@ export class TestClient {
* @param response - response to the query. * @param response - response to the query.
*/ */
public expectKeyQuery(response: IDownloadKeyResult) { public expectKeyQuery(response: IDownloadKeyResult) {
this.httpBackend.when('POST', '/keys/query').respond<IDownloadKeyResult>( this.httpBackend.when("POST", "/keys/query").respond<IDownloadKeyResult>(200, (_path, content) => {
200, (_path, content) => { Object.keys(response.device_keys).forEach((userId) => {
Object.keys(response.device_keys).forEach((userId) => { expect((content.device_keys! as Record<string, any>)[userId]).toEqual([]);
expect((content.device_keys! as Record<string, any>)[userId]).toEqual([]);
});
return response;
}); });
return response;
});
} }
/** /**
* Set up expectations that the client will query key backups for a particular session * Set up expectations that the client will query key backups for a particular session
*/ */
public expectKeyBackupQuery(roomId: string, sessionId: string, status: number, response: IKeyBackupSession) { public expectKeyBackupQuery(roomId: string, sessionId: string, status: number, response: IKeyBackupSession) {
this.httpBackend.when('GET', encodeUri("/room_keys/keys/$roomId/$sessionId", { this.httpBackend
$roomId: roomId, .when(
$sessionId: sessionId, "GET",
})).respond(status, response); encodeUri("/room_keys/keys/$roomId/$sessionId", {
$roomId: roomId,
$sessionId: sessionId,
}),
)
.respond(status, response);
} }
/** /**
@ -205,7 +212,7 @@ export class TestClient {
* @returns base64 device key * @returns base64 device key
*/ */
public getDeviceKey(): string { public getDeviceKey(): string {
const keyId = 'curve25519:' + this.deviceId; const keyId = "curve25519:" + this.deviceId;
return this.deviceKeys!.keys[keyId]; return this.deviceKeys!.keys[keyId];
} }
@ -215,7 +222,7 @@ export class TestClient {
* @returns base64 device key * @returns base64 device key
*/ */
public getSigningKey(): string { public getSigningKey(): string {
const keyId = 'ed25519:' + this.deviceId; const keyId = "ed25519:" + this.deviceId;
return this.deviceKeys!.keys[keyId]; return this.deviceKeys!.keys[keyId];
} }
@ -224,10 +231,7 @@ export class TestClient {
*/ */
public flushSync(): Promise<void> { public flushSync(): Promise<void> {
logger.log(`${this}: flushSync`); logger.log(`${this}: flushSync`);
return Promise.all([ return Promise.all([this.httpBackend.flush("/sync", 1), syncPromise(this.client)]).then(() => {
this.httpBackend.flush('/sync', 1),
syncPromise(this.client),
]).then(() => {
logger.log(`${this}: flushSync completed`); logger.log(`${this}: flushSync completed`);
}); });
} }

View File

@ -24,7 +24,7 @@ const DEVICE_ID = "device_id";
const ACCESS_TOKEN = "access_token"; const ACCESS_TOKEN = "access_token";
const ROOM_ID = "!room_id:server.test"; const ROOM_ID = "!room_id:server.test";
describe("Browserify Test", function() { describe("Browserify Test", function () {
let client: MatrixClient; let client: MatrixClient;
let httpBackend: HttpBackend; let httpBackend: HttpBackend;
@ -68,9 +68,7 @@ describe("Browserify Test", function() {
join: { join: {
[ROOM_ID]: { [ROOM_ID]: {
timeline: { timeline: {
events: [ events: [event],
event,
],
limited: false, limited: false,
}, },
}, },
@ -81,7 +79,7 @@ describe("Browserify Test", function() {
httpBackend.when("GET", "/sync").respond(200, syncData); httpBackend.when("GET", "/sync").respond(200, syncData);
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(); const unexpectedErrorFn = jest.fn();
client.once(global.matrixcs.ClientEvent.SyncUnexpectedError, unexpectedErrorFn); client.once(global.matrixcs.ClientEvent.SyncUnexpectedError, unexpectedErrorFn);

View File

@ -16,9 +16,9 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { TestClient } from '../TestClient'; import { TestClient } from "../TestClient";
import * as testUtils from '../test-utils/test-utils'; import * as testUtils from "../test-utils/test-utils";
import { logger } from '../../src/logger'; import { logger } from "../../src/logger";
const ROOM_ID = "!room:id"; const ROOM_ID = "!room:id";
@ -31,20 +31,22 @@ const ROOM_ID = "!room:id";
function getSyncResponse(roomMembers: string[]) { function getSyncResponse(roomMembers: string[]) {
const stateEvents = [ const stateEvents = [
testUtils.mkEvent({ testUtils.mkEvent({
type: 'm.room.encryption', type: "m.room.encryption",
skey: '', skey: "",
content: { content: {
algorithm: 'm.megolm.v1.aes-sha2', algorithm: "m.megolm.v1.aes-sha2",
}, },
}), }),
]; ];
Array.prototype.push.apply( Array.prototype.push.apply(
stateEvents, stateEvents,
roomMembers.map((m) => testUtils.mkMembership({ roomMembers.map((m) =>
mship: 'join', testUtils.mkMembership({
sender: m, mship: "join",
})), sender: m,
}),
),
); );
const syncResponse = { const syncResponse = {
@ -63,9 +65,9 @@ function getSyncResponse(roomMembers: string[]) {
return syncResponse; return syncResponse;
} }
describe("DeviceList management:", function() { describe("DeviceList management:", function () {
if (!global.Olm) { if (!global.Olm) {
logger.warn('not running deviceList tests: Olm not present'); logger.warn("not running deviceList tests: Olm not present");
return; return;
} }
@ -73,14 +75,12 @@ describe("DeviceList management:", function() {
let sessionStoreBackend: Storage; let sessionStoreBackend: Storage;
async function createTestClient() { async function createTestClient() {
const testClient = new TestClient( const testClient = new TestClient("@alice:localhost", "xzcvb", "akjgkrgjs", sessionStoreBackend);
"@alice:localhost", "xzcvb", "akjgkrgjs", sessionStoreBackend,
);
await testClient.client.initCrypto(); await testClient.client.initCrypto();
return testClient; return testClient;
} }
beforeEach(async function() { beforeEach(async function () {
// we create our own sessionStoreBackend so that we can use it for // we create our own sessionStoreBackend so that we can use it for
// another TestClient. // another TestClient.
sessionStoreBackend = new testUtils.MockStorageApi(); sessionStoreBackend = new testUtils.MockStorageApi();
@ -88,311 +88,311 @@ describe("DeviceList management:", function() {
aliceTestClient = await createTestClient(); aliceTestClient = await createTestClient();
}); });
afterEach(function() { afterEach(function () {
return aliceTestClient.stop(); return aliceTestClient.stop();
}); });
it("Alice shouldn't do a second /query for non-e2e-capable devices", function() { it("Alice shouldn't do a second /query for non-e2e-capable devices", function () {
aliceTestClient.expectKeyQuery({ aliceTestClient.expectKeyQuery({
device_keys: { '@alice:localhost': {} }, device_keys: { "@alice:localhost": {} },
failures: {}, failures: {},
}); });
return aliceTestClient.start().then(function() { return aliceTestClient
const syncResponse = getSyncResponse(['@bob:xyz']); .start()
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse); .then(function () {
const syncResponse = getSyncResponse(["@bob:xyz"]);
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
return aliceTestClient.flushSync(); return aliceTestClient.flushSync();
}).then(function() { })
logger.log("Forcing alice to download our device keys"); .then(function () {
logger.log("Forcing alice to download our device keys");
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, { aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, {
device_keys: { device_keys: {
'@bob:xyz': {}, "@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,
}),
]);
}); });
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", () => { it.skip("We should not get confused by out-of-order device query responses", () => {
// https://github.com/vector-im/element-web/issues/3126 // https://github.com/vector-im/element-web/issues/3126
aliceTestClient.expectKeyQuery({ aliceTestClient.expectKeyQuery({
device_keys: { '@alice:localhost': {} }, device_keys: { "@alice:localhost": {} },
failures: {}, failures: {},
}); });
return aliceTestClient.start().then(() => { return aliceTestClient
aliceTestClient.httpBackend.when('GET', '/sync').respond( .start()
200, getSyncResponse(['@bob:xyz', '@chris:abc'])); .then(() => {
return aliceTestClient.flushSync(); aliceTestClient.httpBackend
}).then(() => { .when("GET", "/sync")
// to make sure the initial device queries are flushed out, we .respond(200, getSyncResponse(["@bob:xyz", "@chris:abc"]));
// 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(); return aliceTestClient.flushSync();
}); })
}).then(() => { .then(() => {
// check that we don't yet have a request for chris's devices. // to make sure the initial device queries are flushed out, we
aliceTestClient.httpBackend.when('POST', '/keys/query', { // attempt to send a message.
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 aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, {
// it complete. device_keys: {
aliceTestClient.httpBackend.when('POST', '/keys/query', { "@bob:xyz": {},
device_keys: { "@chris:abc": {},
'@bob:xyz': {}, },
}, });
token: '2',
}).respond(200, { aliceTestClient.httpBackend.when("PUT", "/send/").respond(200, { event_id: "$event1" });
device_keys: { '@bob:xyz': {} },
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);
});
}); });
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 // https://github.com/vector-im/element-web/issues/4983
describe("Alice should know she has stale device lists", () => { describe("Alice should know she has stale device lists", () => {
beforeEach(async function() { beforeEach(async function () {
await aliceTestClient.start(); await aliceTestClient.start();
aliceTestClient.httpBackend.when('GET', '/sync').respond( aliceTestClient.httpBackend.when("GET", "/sync").respond(200, getSyncResponse(["@bob:xyz"]));
200, getSyncResponse(['@bob:xyz']));
await aliceTestClient.flushSync(); await aliceTestClient.flushSync();
aliceTestClient.httpBackend.when('POST', '/keys/query').respond( aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, {
200, { device_keys: {
device_keys: { "@bob:xyz": {},
'@bob:xyz': {},
},
}, },
); });
await aliceTestClient.httpBackend.flush('/keys/query', 1); await aliceTestClient.httpBackend.flush("/keys/query", 1);
await aliceTestClient.client.crypto!.deviceList.saveIfDirty(); await aliceTestClient.client.crypto!.deviceList.saveIfDirty();
// @ts-ignore accessing a protected field // @ts-ignore accessing a protected field
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => { aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus['@bob:xyz']; const bobStat = data!.trackingStatus["@bob:xyz"];
// Alice should be tracking bob's device list // Alice should be tracking bob's device list
expect(bobStat).toBeGreaterThan( expect(bobStat).toBeGreaterThan(0);
0,
);
}); });
}); });
it("when Bob leaves", async function() { it("when Bob leaves", async function () {
aliceTestClient.httpBackend.when('GET', '/sync').respond( aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
200, { next_batch: 2,
next_batch: 2, device_lists: {
device_lists: { left: ["@bob:xyz"],
left: ['@bob:xyz'], },
}, rooms: {
rooms: { join: {
join: { [ROOM_ID]: {
[ROOM_ID]: { timeline: {
timeline: { events: [
events: [ testUtils.mkMembership({
testUtils.mkMembership({ mship: "leave",
mship: 'leave', sender: "@bob:xyz",
sender: '@bob:xyz', }),
}), ],
],
},
}, },
}, },
}, },
}, },
); });
await aliceTestClient.flushSync(); await aliceTestClient.flushSync();
await aliceTestClient.client.crypto!.deviceList.saveIfDirty(); await aliceTestClient.client.crypto!.deviceList.saveIfDirty();
// @ts-ignore accessing a protected field // @ts-ignore accessing a protected field
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => { aliceTestClient.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,
);
});
});
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();
// @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 // 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 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();
// @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);
});
});
it("when Bob leaves whilst Alice is offline", async function () {
aliceTestClient.stop(); aliceTestClient.stop();
const anotherTestClient = await createTestClient(); const anotherTestClient = await createTestClient();
try { try {
await anotherTestClient.start(); await anotherTestClient.start();
anotherTestClient.httpBackend.when('GET', '/sync').respond( anotherTestClient.httpBackend.when("GET", "/sync").respond(200, getSyncResponse([]));
200, getSyncResponse([]));
await anotherTestClient.flushSync(); await anotherTestClient.flushSync();
await anotherTestClient.client?.crypto?.deviceList?.saveIfDirty(); await anotherTestClient.client?.crypto?.deviceList?.saveIfDirty();
// @ts-ignore accessing private property // @ts-ignore accessing private property
anotherTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { 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 // Alice should have marked bob's device list as untracked
expect(bobStat).toEqual(0); expect(bobStat).toEqual(0);

View File

@ -26,15 +26,15 @@ limitations under the License.
*/ */
// load olm before the sdk if possible // load olm before the sdk if possible
import '../olm-loader'; import "../olm-loader";
import type { Session } from "@matrix-org/olm"; import type { Session } from "@matrix-org/olm";
import { logger } from '../../src/logger'; import { logger } from "../../src/logger";
import * as testUtils from "../test-utils/test-utils"; import * as testUtils from "../test-utils/test-utils";
import { TestClient } from "../TestClient"; import { TestClient } from "../TestClient";
import { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, IUploadKeysRequest } from "../../src/client"; import { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, IUploadKeysRequest } from "../../src/client";
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix"; 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"; import { IDeviceKeys, IOneTimeKey } from "../../src/crypto/dehydration";
let aliTestClient: TestClient; let aliTestClient: TestClient;
@ -68,13 +68,12 @@ function expectQueryKeys(querier: TestClient, uploader: TestClient): Promise<num
const uploaderKeys: Record<string, IDeviceKeys> = {}; const uploaderKeys: Record<string, IDeviceKeys> = {};
uploaderKeys[uploader.deviceId!] = uploader.deviceKeys!; uploaderKeys[uploader.deviceId!] = uploader.deviceKeys!;
querier.httpBackend.when("POST", "/keys/query") querier.httpBackend.when("POST", "/keys/query").respond(200, function (_path, content: IQueryKeysRequest) {
.respond(200, function(_path, content: IQueryKeysRequest) { expect(content.device_keys![uploader.userId!]).toEqual([]);
expect(content.device_keys![uploader.userId!]).toEqual([]); const result: Record<string, Record<string, IDeviceKeys>> = {};
const result: Record<string, Record<string, IDeviceKeys>> = {}; result[uploader.userId!] = uploaderKeys;
result[uploader.userId!] = uploaderKeys; return { device_keys: result };
return { device_keys: result }; });
});
return querier.httpBackend.flush("/keys/query", 1); return querier.httpBackend.flush("/keys/query", 1);
} }
const expectAliQueryKeys = () => expectQueryKeys(aliTestClient, bobTestClient); const expectAliQueryKeys = () => expectQueryKeys(aliTestClient, bobTestClient);
@ -87,12 +86,10 @@ const expectBobQueryKeys = () => expectQueryKeys(bobTestClient, aliTestClient);
*/ */
async function expectAliClaimKeys(): Promise<void> { async function expectAliClaimKeys(): Promise<void> {
const keys = await bobTestClient.awaitOneTimeKeyUpload(); const keys = await bobTestClient.awaitOneTimeKeyUpload();
aliTestClient.httpBackend.when( aliTestClient.httpBackend.when("POST", "/keys/claim").respond(200, function (_path, content: IClaimKeysRequest) {
"POST", "/keys/claim",
).respond(200, function(_path, content: IClaimKeysRequest) {
const claimType = content.one_time_keys![bobUserId][bobDeviceId]; const claimType = content.one_time_keys![bobUserId][bobDeviceId];
expect(claimType).toEqual("signed_curve25519"); expect(claimType).toEqual("signed_curve25519");
let keyId = ''; let keyId = "";
for (keyId in keys) { for (keyId in keys) {
if (bobTestClient.oneTimeKeys!.hasOwnProperty(keyId)) { if (bobTestClient.oneTimeKeys!.hasOwnProperty(keyId)) {
if (keyId.indexOf(claimType + ":") === 0) { if (keyId.indexOf(claimType + ":") === 0) {
@ -133,8 +130,7 @@ async function aliDownloadsKeys(): Promise<void> {
aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const devices = data!.devices[bobUserId]!; const devices = data!.devices[bobUserId]!;
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys!.keys); expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys!.keys);
expect(devices[bobDeviceId].verified). expect(devices[bobDeviceId].verified).toBe(DeviceInfo.DeviceVerification.UNVERIFIED);
toBe(DeviceInfo.DeviceVerification.UNVERIFIED);
}); });
} }
@ -157,9 +153,7 @@ async function aliSendsFirstMessage(): Promise<OlmPayload> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, ciphertext] = await Promise.all([ const [_, ciphertext] = await Promise.all([
sendMessage(aliTestClient.client), sendMessage(aliTestClient.client),
expectAliQueryKeys() expectAliQueryKeys().then(expectAliClaimKeys).then(expectAliSendMessageRequest),
.then(expectAliClaimKeys)
.then(expectAliSendMessageRequest),
]); ]);
return ciphertext; return ciphertext;
} }
@ -172,10 +166,7 @@ async function aliSendsFirstMessage(): Promise<OlmPayload> {
*/ */
async function aliSendsMessage(): Promise<OlmPayload> { async function aliSendsMessage(): Promise<OlmPayload> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, ciphertext] = await Promise.all([ const [_, ciphertext] = await Promise.all([sendMessage(aliTestClient.client), expectAliSendMessageRequest()]);
sendMessage(aliTestClient.client),
expectAliSendMessageRequest(),
]);
return ciphertext; return ciphertext;
} }
@ -189,8 +180,7 @@ async function bobSendsReplyMessage(): Promise<OlmPayload> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, ciphertext] = await Promise.all([ const [_, ciphertext] = await Promise.all([
sendMessage(bobTestClient.client), sendMessage(bobTestClient.client),
expectBobQueryKeys() expectBobQueryKeys().then(expectBobSendMessageRequest),
.then(expectBobSendMessageRequest),
]); ]);
return ciphertext; return ciphertext;
} }
@ -226,15 +216,13 @@ async function expectBobSendMessageRequest(): Promise<OlmPayload> {
} }
function sendMessage(client: MatrixClient): Promise<ISendEventResponse> { function sendMessage(client: MatrixClient): Promise<ISendEventResponse> {
return client.sendMessage( return client.sendMessage(roomId, { msgtype: "m.text", body: "Hello, World" });
roomId, { msgtype: "m.text", body: "Hello, World" },
);
} }
async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]): Promise<IContent> { async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]): Promise<IContent> {
const path = "/send/m.room.encrypted/"; const path = "/send/m.room.encrypted/";
const prom = new Promise<IContent>((resolve) => { 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); resolve(content);
return { return {
event_id: "asdfgh", event_id: "asdfgh",
@ -249,16 +237,12 @@ async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]):
function aliRecvMessage(): Promise<void> { function aliRecvMessage(): Promise<void> {
const message = bobMessages.shift()!; const message = bobMessages.shift()!;
return recvMessage( return recvMessage(aliTestClient.httpBackend, aliTestClient.client, bobUserId, message);
aliTestClient.httpBackend, aliTestClient.client, bobUserId, message,
);
} }
function bobRecvMessage(): Promise<void> { function bobRecvMessage(): Promise<void> {
const message = aliMessages.shift()!; const message = aliMessages.shift()!;
return recvMessage( return recvMessage(bobTestClient.httpBackend, bobTestClient.client, aliUserId, message);
bobTestClient.httpBackend, bobTestClient.client, aliUserId, message,
);
} }
async function recvMessage( async function recvMessage(
@ -289,13 +273,12 @@ async function recvMessage(
httpBackend.when("GET", "/sync").respond(200, syncData); httpBackend.when("GET", "/sync").respond(200, syncData);
const eventPromise = new Promise<MatrixEvent>((resolve) => { const eventPromise = new Promise<MatrixEvent>((resolve) => {
const onEvent = function(event: MatrixEvent) { const onEvent = function (event: MatrixEvent) {
// ignore the m.room.member events // ignore the m.room.member events
if (event.getType() == "m.room.member") { if (event.getType() == "m.room.member") {
return; return;
} }
logger.log(client.credentials.userId + " received event", logger.log(client.credentials.userId + " received event", event);
event);
client.removeListener(ClientEvent.Event, onEvent); client.removeListener(ClientEvent.Event, onEvent);
resolve(event); resolve(event);
@ -382,8 +365,7 @@ describe("MatrixClient crypto", () => {
it("handles failures to upload device keys", async () => { 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 // since device keys are uploaded asynchronously, there's not really much to do here other than fail the
// upload. // upload.
bobTestClient.httpBackend.when("POST", "/keys/upload") bobTestClient.httpBackend.when("POST", "/keys/upload").fail(0, new Error("bleh"));
.fail(0, new Error("bleh"));
await bobTestClient.httpBackend.flushAllExpected(); await bobTestClient.httpBackend.flushAllExpected();
}); });
@ -398,10 +380,7 @@ describe("MatrixClient crypto", () => {
const bobDeviceKeys = bobTestClient.deviceKeys!; const bobDeviceKeys = bobTestClient.deviceKeys!;
expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeTruthy(); expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeTruthy();
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc"; bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
await Promise.all([ await Promise.all([aliTestClient.client.downloadKeys([bobUserId]), expectAliQueryKeys()]);
aliTestClient.client.downloadKeys([bobUserId]),
expectAliQueryKeys(),
]);
const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId); const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId);
// should get an empty list // should get an empty list
expect(devices).toEqual([]); expect(devices).toEqual([]);
@ -411,26 +390,24 @@ describe("MatrixClient crypto", () => {
const eveUserId = "@eve:localhost"; const eveUserId = "@eve:localhost";
const bobDeviceKeys = { const bobDeviceKeys = {
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'], algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: 'bvcxz', device_id: "bvcxz",
keys: { keys: {
'ed25519:bvcxz': 'pYuWKMCVuaDLRTM/eWuB8OlXEb61gZhfLVJ+Y54tl0Q', "ed25519:bvcxz": "pYuWKMCVuaDLRTM/eWuB8OlXEb61gZhfLVJ+Y54tl0Q",
'curve25519:bvcxz': '7Gni0loo/nzF0nFp9847RbhElGewzwUXHPrljjBGPTQ', "curve25519:bvcxz": "7Gni0loo/nzF0nFp9847RbhElGewzwUXHPrljjBGPTQ",
}, },
user_id: '@eve:localhost', user_id: "@eve:localhost",
signatures: { signatures: {
'@eve:localhost': { "@eve:localhost": {
'ed25519:bvcxz': 'CliUPZ7dyVPBxvhSA1d+X+LYa5b2AYdjcTwG' + "ed25519:bvcxz":
'0stXcIxjaJNemQqtdgwKDtBFl3pN2I13SEijRDCf1A8bYiQMDg', "CliUPZ7dyVPBxvhSA1d+X+LYa5b2AYdjcTwG" + "0stXcIxjaJNemQqtdgwKDtBFl3pN2I13SEijRDCf1A8bYiQMDg",
}, },
}, },
}; };
const bobKeys: Record<string, typeof bobDeviceKeys> = {}; const bobKeys: Record<string, typeof bobDeviceKeys> = {};
bobKeys[bobDeviceId] = bobDeviceKeys; bobKeys[bobDeviceId] = bobDeviceKeys;
aliTestClient.httpBackend.when( aliTestClient.httpBackend.when("POST", "/keys/query").respond(200, { device_keys: { [bobUserId]: bobKeys } });
"POST", "/keys/query",
).respond(200, { device_keys: { [bobUserId]: bobKeys } });
await Promise.all([ await Promise.all([
aliTestClient.client.downloadKeys([bobUserId, eveUserId]), aliTestClient.client.downloadKeys([bobUserId, eveUserId]),
@ -447,26 +424,24 @@ describe("MatrixClient crypto", () => {
it("Ali gets keys with an incorrect deviceId", async () => { it("Ali gets keys with an incorrect deviceId", async () => {
const bobDeviceKeys = { const bobDeviceKeys = {
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'], algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: 'bad_device', device_id: "bad_device",
keys: { keys: {
'ed25519:bad_device': 'e8XlY5V8x2yJcwa5xpSzeC/QVOrU+D5qBgyTK0ko+f0', "ed25519:bad_device": "e8XlY5V8x2yJcwa5xpSzeC/QVOrU+D5qBgyTK0ko+f0",
'curve25519:bad_device': 'YxuuLG/4L5xGeP8XPl5h0d7DzyYVcof7J7do+OXz0xc', "curve25519:bad_device": "YxuuLG/4L5xGeP8XPl5h0d7DzyYVcof7J7do+OXz0xc",
}, },
user_id: '@bob:localhost', user_id: "@bob:localhost",
signatures: { signatures: {
'@bob:localhost': { "@bob:localhost": {
'ed25519:bad_device': 'fEFTq67RaSoIEVBJ8DtmRovbwUBKJ0A' + "ed25519:bad_device":
'me9m9PDzM9azPUwZ38Xvf6vv1A7W1PSafH4z3Y2ORIyEnZgHaNby3CQ', "fEFTq67RaSoIEVBJ8DtmRovbwUBKJ0A" + "me9m9PDzM9azPUwZ38Xvf6vv1A7W1PSafH4z3Y2ORIyEnZgHaNby3CQ",
}, },
}, },
}; };
const bobKeys: Record<string, typeof bobDeviceKeys> = {}; const bobKeys: Record<string, typeof bobDeviceKeys> = {};
bobKeys[bobDeviceId] = bobDeviceKeys; bobKeys[bobDeviceId] = bobDeviceKeys;
aliTestClient.httpBackend.when( aliTestClient.httpBackend.when("POST", "/keys/query").respond(200, { device_keys: { [bobUserId]: bobKeys } });
"POST", "/keys/query",
).respond(200, { device_keys: { [bobUserId]: bobKeys } });
await Promise.all([ await Promise.all([
aliTestClient.client.downloadKeys([bobUserId]), aliTestClient.client.downloadKeys([bobUserId]),
@ -535,7 +510,7 @@ describe("MatrixClient crypto", () => {
bobTestClient.httpBackend.when("GET", "/sync").respond(200, syncData); bobTestClient.httpBackend.when("GET", "/sync").respond(200, syncData);
const eventPromise = new Promise<MatrixEvent>((resolve) => { const eventPromise = new Promise<MatrixEvent>((resolve) => {
const onEvent = function(event: MatrixEvent) { const onEvent = function (event: MatrixEvent) {
logger.log(bobUserId + " received event", event); logger.log(bobUserId + " received event", event);
resolve(event); resolve(event);
}; };
@ -559,11 +534,10 @@ describe("MatrixClient crypto", () => {
await aliDownloadsKeys(); await aliDownloadsKeys();
aliTestClient.client.setDeviceBlocked(bobUserId, bobDeviceId, true); aliTestClient.client.setDeviceBlocked(bobUserId, bobDeviceId, true);
const p1 = sendMessage(aliTestClient.client); const p1 = sendMessage(aliTestClient.client);
const p2 = expectSendMessageRequest(aliTestClient.httpBackend) const p2 = expectSendMessageRequest(aliTestClient.httpBackend).then(function (sentContent) {
.then(function(sentContent) { // no unblocked devices, so the ciphertext should be empty
// no unblocked devices, so the ciphertext should be empty expect(sentContent.ciphertext).toEqual({});
expect(sentContent.ciphertext).toEqual({}); });
});
await Promise.all([p1, p2]); await Promise.all([p1, p2]);
}); });
@ -589,9 +563,7 @@ describe("MatrixClient crypto", () => {
await firstSync(bobTestClient); await firstSync(bobTestClient);
await aliEnablesEncryption(); await aliEnablesEncryption();
await aliSendsFirstMessage(); await aliSendsFirstMessage();
bobTestClient.httpBackend.when('POST', '/keys/query').respond( bobTestClient.httpBackend.when("POST", "/keys/query").respond(200, {});
200, {},
);
await bobRecvMessage(); await bobRecvMessage();
await bobEnablesEncryption(); await bobEnablesEncryption();
const ciphertext = await bobSendsReplyMessage(); const ciphertext = await bobSendsReplyMessage();
@ -606,17 +578,17 @@ describe("MatrixClient crypto", () => {
await aliTestClient.start(); await aliTestClient.start();
await firstSync(aliTestClient); await firstSync(aliTestClient);
const syncData = { const syncData = {
next_batch: '2', next_batch: "2",
rooms: { rooms: {
join: { join: {
[roomId]: { [roomId]: {
state: { state: {
events: [ events: [
testUtils.mkEvent({ testUtils.mkEvent({
type: 'm.room.encryption', type: "m.room.encryption",
skey: '', skey: "",
content: { content: {
algorithm: 'm.olm.v1.curve25519-aes-sha2', algorithm: "m.olm.v1.curve25519-aes-sha2",
}, },
}), }),
], ],
@ -626,9 +598,8 @@ describe("MatrixClient crypto", () => {
}, },
}; };
aliTestClient.httpBackend.when('GET', '/sync').respond( aliTestClient.httpBackend.when("GET", "/sync").respond(200, syncData);
200, syncData); await aliTestClient.httpBackend.flush("/sync", 1);
await aliTestClient.httpBackend.flush('/sync', 1);
aliTestClient.expectKeyQuery({ aliTestClient.expectKeyQuery({
device_keys: { device_keys: {
[bobUserId]: {}, [bobUserId]: {},
@ -651,7 +622,7 @@ describe("MatrixClient crypto", () => {
// enqueue expectations: // enqueue expectations:
// * Sync with empty one_time_keys => upload keys // * 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", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {}); httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" }); httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
@ -661,24 +632,20 @@ describe("MatrixClient crypto", () => {
// it will upload one-time keys. // it will upload one-time keys.
httpBackend.when("GET", "/sync").respond(200, syncDataEmpty); httpBackend.when("GET", "/sync").respond(200, syncDataEmpty);
await Promise.all([ await Promise.all([aliTestClient.client.startClient({}), httpBackend.flushAllExpected()]);
aliTestClient.client.startClient({}), logger.log(aliTestClient + ": started");
httpBackend.flushAllExpected(), httpBackend.when("POST", "/keys/upload").respond(200, (_path, content: IUploadKeysRequest) => {
]); expect(content.one_time_keys).toBeTruthy();
logger.log(aliTestClient + ': started'); expect(content.one_time_keys).not.toEqual({});
httpBackend.when("POST", "/keys/upload") expect(Object.keys(content.one_time_keys!).length).toBeGreaterThanOrEqual(1);
.respond(200, (_path, content: IUploadKeysRequest) => { // cancel futher calls by telling the client
expect(content.one_time_keys).toBeTruthy(); // we have more than we need
expect(content.one_time_keys).not.toEqual({}); return {
expect(Object.keys(content.one_time_keys!).length).toBeGreaterThanOrEqual(1); one_time_key_counts: {
// cancel futher calls by telling the client signed_curve25519: 70,
// we have more than we need },
return { };
one_time_key_counts: { });
signed_curve25519: 70,
},
};
});
await httpBackend.flushAllExpected(); await httpBackend.flushAllExpected();
}); });
@ -687,7 +654,7 @@ describe("MatrixClient crypto", () => {
sender: "@bob:example.com", sender: "@bob:example.com",
room_id: "!someroom", room_id: "!someroom",
content: { content: {
algorithm: 'm.megolm.v1.aes-sha2', algorithm: "m.megolm.v1.aes-sha2",
session_id: "sessionid", session_id: "sessionid",
sender_key: "senderkey", sender_key: "senderkey",
}, },
@ -696,7 +663,7 @@ describe("MatrixClient crypto", () => {
sender: "@bob:example.com", sender: "@bob:example.com",
room_id: "!someroom", room_id: "!someroom",
content: { content: {
algorithm: 'm.megolm.v1.aes-sha2', algorithm: "m.megolm.v1.aes-sha2",
session_id: "sessionid", session_id: "sessionid",
sender_key: "senderkey", sender_key: "senderkey",
}, },
@ -705,7 +672,7 @@ describe("MatrixClient crypto", () => {
sender: "@bob:example.com", sender: "@bob:example.com",
room_id: "!someroom", room_id: "!someroom",
content: { content: {
algorithm: 'm.megolm.v1.aes-sha2', algorithm: "m.megolm.v1.aes-sha2",
session_id: "othersessionid", session_id: "othersessionid",
sender_key: "senderkey", sender_key: "senderkey",
}, },

View File

@ -29,7 +29,7 @@ import {
import * as utils from "../test-utils/test-utils"; import * as utils from "../test-utils/test-utils";
import { TestClient } from "../TestClient"; import { TestClient } from "../TestClient";
describe("MatrixClient events", function() { describe("MatrixClient events", function () {
const selfUserId = "@alice:localhost"; const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef"; const selfAccessToken = "aseukfgwef";
let client: MatrixClient | undefined; let client: MatrixClient | undefined;
@ -46,23 +46,25 @@ describe("MatrixClient events", function() {
return [client!, httpBackend]; return [client!, httpBackend];
}; };
beforeEach(function() { beforeEach(function () {
[client!, httpBackend] = setupTests(); [client!, httpBackend] = setupTests();
}); });
afterEach(function() { afterEach(function () {
httpBackend?.verifyNoOutstandingExpectation(); httpBackend?.verifyNoOutstandingExpectation();
client?.stopClient(); client?.stopClient();
return httpBackend?.stop(); return httpBackend?.stop();
}); });
describe("emissions", function() { describe("emissions", function () {
const SYNC_DATA = { const SYNC_DATA = {
next_batch: "s_5_3", next_batch: "s_5_3",
presence: { presence: {
events: [ events: [
utils.mkPresence({ 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: { timeline: {
events: [ events: [
utils.mkMessage({ utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: "hmmm", room: "!erufh:bar",
user: "@foo:bar",
msg: "hmmm",
}), }),
], ],
prev_batch: "s", prev_batch: "s",
@ -80,10 +84,13 @@ describe("MatrixClient events", function() {
state: { state: {
events: [ events: [
utils.mkMembership({ utils.mkMembership({
room: "!erufh:bar", mship: "join", user: "@foo:bar", room: "!erufh:bar",
mship: "join",
user: "@foo:bar",
}), }),
utils.mkEvent({ utils.mkEvent({
type: "m.room.create", room: "!erufh:bar", type: "m.room.create",
room: "!erufh:bar",
user: "@foo:bar", user: "@foo:bar",
content: { content: {
creator: "@foo:bar", creator: "@foo:bar",
@ -103,18 +110,23 @@ describe("MatrixClient events", function() {
timeline: { timeline: {
events: [ events: [
utils.mkMessage({ utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", room: "!erufh:bar",
user: "@foo:bar",
msg: "ello ello", msg: "ello ello",
}), }),
utils.mkMessage({ utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: ":D", room: "!erufh:bar",
user: "@foo:bar",
msg: ":D",
}), }),
], ],
}, },
ephemeral: { ephemeral: {
events: [ events: [
utils.mkEvent({ utils.mkEvent({
type: "m.typing", room: "!erufh:bar", content: { type: "m.typing",
room: "!erufh:bar",
content: {
user_ids: ["@foo:bar"], user_ids: ["@foo:bar"],
}, },
}), }),
@ -125,50 +137,49 @@ describe("MatrixClient events", function() {
}, },
}; };
it("should emit events from both the first and subsequent /sync calls", it("should emit events from both the first and subsequent /sync calls", function () {
function() { httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA); httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
let expectedEvents: Partial<IEvent>[] = []; let expectedEvents: Partial<IEvent>[] = [];
expectedEvents = expectedEvents.concat( expectedEvents = expectedEvents.concat(
SYNC_DATA.presence.events, SYNC_DATA.presence.events,
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events, SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
SYNC_DATA.rooms.join["!erufh:bar"].state.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"].timeline.events,
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events, NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events,
); );
client!.on(ClientEvent.Event, function(event) { client!.on(ClientEvent.Event, function (event) {
let found = false; let found = false;
for (let i = 0; i < expectedEvents.length; i++) { for (let i = 0; i < expectedEvents.length; i++) {
if (expectedEvents[i].event_id === event.getId()) { if (expectedEvents[i].event_id === event.getId()) {
expectedEvents.splice(i, 1); expectedEvents.splice(i, 1);
found = true; found = true;
break; break;
}
} }
expect(found).toBe(true); }
}); 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);
});
}); });
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, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA); httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
let fired = false; let fired = false;
client!.on(UserEvent.Presence, function(event, user) { client!.on(UserEvent.Presence, function (event, user) {
fired = true; fired = true;
expect(user).toBeTruthy(); expect(user).toBeTruthy();
expect(event).toBeTruthy(); expect(event).toBeTruthy();
@ -177,59 +188,52 @@ describe("MatrixClient events", function() {
} }
expect(event.event).toEqual(SYNC_DATA.presence.events[0]); expect(event.event).toEqual(SYNC_DATA.presence.events[0]);
expect(user.presence).toEqual( expect(user.presence).toEqual(SYNC_DATA.presence.events[0]?.content?.presence);
SYNC_DATA.presence.events[0]?.content?.presence,
);
}); });
client!.startClient(); client!.startClient();
httpBackend!.flushAllExpected().then(function() { httpBackend!.flushAllExpected().then(function () {
expect(fired).toBe(true); expect(fired).toBe(true);
done(); 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, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA); httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
let roomInvokeCount = 0; let roomInvokeCount = 0;
let roomNameInvokeCount = 0; let roomNameInvokeCount = 0;
let timelineFireCount = 0; let timelineFireCount = 0;
client!.on(ClientEvent.Room, function(room) { client!.on(ClientEvent.Room, function (room) {
roomInvokeCount++; roomInvokeCount++;
expect(room.roomId).toEqual("!erufh:bar"); expect(room.roomId).toEqual("!erufh:bar");
}); });
client!.on(RoomEvent.Timeline, function(event, room) { client!.on(RoomEvent.Timeline, function (event, room) {
timelineFireCount++; timelineFireCount++;
expect(room?.roomId).toEqual("!erufh:bar"); expect(room?.roomId).toEqual("!erufh:bar");
}); });
client!.on(RoomEvent.Name, function(room) { client!.on(RoomEvent.Name, function (room) {
roomNameInvokeCount++; roomNameInvokeCount++;
}); });
client!.startClient(); client!.startClient();
return Promise.all([ return Promise.all([httpBackend!.flushAllExpected(), utils.syncPromise(client!, 2)]).then(function () {
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 2),
]).then(function() {
expect(roomInvokeCount).toEqual(1); expect(roomInvokeCount).toEqual(1);
expect(roomNameInvokeCount).toEqual(1); expect(roomNameInvokeCount).toEqual(1);
expect(timelineFireCount).toEqual(3); 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, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA); httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
const roomStateEventTypes = [ const roomStateEventTypes = ["m.room.member", "m.room.create"];
"m.room.member", "m.room.create",
];
let eventsInvokeCount = 0; let eventsInvokeCount = 0;
let membersInvokeCount = 0; let membersInvokeCount = 0;
let newMemberInvokeCount = 0; let newMemberInvokeCount = 0;
client!.on(RoomStateEvent.Events, function(event, state) { client!.on(RoomStateEvent.Events, function (event, state) {
eventsInvokeCount++; eventsInvokeCount++;
const index = roomStateEventTypes.indexOf(event.getType()); const index = roomStateEventTypes.indexOf(event.getType());
expect(index).not.toEqual(-1); expect(index).not.toEqual(-1);
@ -237,13 +241,13 @@ describe("MatrixClient events", function() {
roomStateEventTypes.splice(index, 1); roomStateEventTypes.splice(index, 1);
} }
}); });
client!.on(RoomStateEvent.Members, function(event, state, member) { client!.on(RoomStateEvent.Members, function (event, state, member) {
membersInvokeCount++; membersInvokeCount++;
expect(member.roomId).toEqual("!erufh:bar"); expect(member.roomId).toEqual("!erufh:bar");
expect(member.userId).toEqual("@foo:bar"); expect(member.userId).toEqual("@foo:bar");
expect(member.membership).toEqual("join"); expect(member.membership).toEqual("join");
}); });
client!.on(RoomStateEvent.NewMember, function(event, state, member) { client!.on(RoomStateEvent.NewMember, function (event, state, member) {
newMemberInvokeCount++; newMemberInvokeCount++;
expect(member.roomId).toEqual("!erufh:bar"); expect(member.roomId).toEqual("!erufh:bar");
expect(member.userId).toEqual("@foo:bar"); expect(member.userId).toEqual("@foo:bar");
@ -252,17 +256,14 @@ describe("MatrixClient events", function() {
client!.startClient(); client!.startClient();
return Promise.all([ return Promise.all([httpBackend!.flushAllExpected(), utils.syncPromise(client!, 2)]).then(function () {
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 2),
]).then(function() {
expect(membersInvokeCount).toEqual(1); expect(membersInvokeCount).toEqual(1);
expect(newMemberInvokeCount).toEqual(1); expect(newMemberInvokeCount).toEqual(1);
expect(eventsInvokeCount).toEqual(2); 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, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA); httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
@ -270,27 +271,24 @@ describe("MatrixClient events", function() {
let powerLevelInvokeCount = 0; let powerLevelInvokeCount = 0;
let nameInvokeCount = 0; let nameInvokeCount = 0;
let membershipInvokeCount = 0; let membershipInvokeCount = 0;
client!.on(RoomMemberEvent.Name, function(event, member) { client!.on(RoomMemberEvent.Name, function (event, member) {
nameInvokeCount++; nameInvokeCount++;
}); });
client!.on(RoomMemberEvent.Typing, function(event, member) { client!.on(RoomMemberEvent.Typing, function (event, member) {
typingInvokeCount++; typingInvokeCount++;
expect(member.typing).toBe(true); expect(member.typing).toBe(true);
}); });
client!.on(RoomMemberEvent.PowerLevel, function(event, member) { client!.on(RoomMemberEvent.PowerLevel, function (event, member) {
powerLevelInvokeCount++; powerLevelInvokeCount++;
}); });
client!.on(RoomMemberEvent.Membership, function(event, member) { client!.on(RoomMemberEvent.Membership, function (event, member) {
membershipInvokeCount++; membershipInvokeCount++;
expect(member.membership).toEqual("join"); expect(member.membership).toEqual("join");
}); });
client!.startClient(); client!.startClient();
return Promise.all([ return Promise.all([httpBackend!.flushAllExpected(), utils.syncPromise(client!, 2)]).then(function () {
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 2),
]).then(function() {
expect(typingInvokeCount).toEqual(1); expect(typingInvokeCount).toEqual(1);
expect(powerLevelInvokeCount).toEqual(0); expect(powerLevelInvokeCount).toEqual(0);
expect(nameInvokeCount).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() { it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function () {
const error = { errcode: 'M_UNKNOWN_TOKEN' }; const error = { errcode: "M_UNKNOWN_TOKEN" };
httpBackend!.when("GET", "/sync").respond(401, error); httpBackend!.when("GET", "/sync").respond(401, error);
let sessionLoggedOutCount = 0; let sessionLoggedOutCount = 0;
client!.on(HttpApiEvent.SessionLoggedOut, function(errObj) { client!.on(HttpApiEvent.SessionLoggedOut, function (errObj) {
sessionLoggedOutCount++; sessionLoggedOutCount++;
expect(errObj.data).toEqual(error); expect(errObj.data).toEqual(error);
}); });
client!.startClient(); client!.startClient();
return httpBackend!.flushAllExpected().then(function() { return httpBackend!.flushAllExpected().then(function () {
expect(sessionLoggedOutCount).toEqual(1); expect(sessionLoggedOutCount).toEqual(1);
}); });
}); });
it("should emit Session.logged_out on M_UNKNOWN_TOKEN (soft logout)", function() { it("should emit Session.logged_out on M_UNKNOWN_TOKEN (soft logout)", function () {
const error = { errcode: 'M_UNKNOWN_TOKEN', soft_logout: true }; const error = { errcode: "M_UNKNOWN_TOKEN", soft_logout: true };
httpBackend!.when("GET", "/sync").respond(401, error); httpBackend!.when("GET", "/sync").respond(401, error);
let sessionLoggedOutCount = 0; let sessionLoggedOutCount = 0;
client!.on(HttpApiEvent.SessionLoggedOut, function(errObj) { client!.on(HttpApiEvent.SessionLoggedOut, function (errObj) {
sessionLoggedOutCount++; sessionLoggedOutCount++;
expect(errObj.data).toEqual(error); expect(errObj.data).toEqual(error);
}); });
client!.startClient(); client!.startClient();
return httpBackend!.flushAllExpected().then(function() { return httpBackend!.flushAllExpected().then(function () {
expect(sessionLoggedOutCount).toEqual(1); expect(sessionLoggedOutCount).toEqual(1);
}); });
}); });

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@ import { MemoryStore } from "../../src/store/memory";
import { MatrixError } from "../../src/http-api"; import { MatrixError } from "../../src/http-api";
import { IStore } from "../../src/store"; import { IStore } from "../../src/store";
describe("MatrixClient opts", function() { describe("MatrixClient opts", function () {
const baseUrl = "http://localhost.or.something"; const baseUrl = "http://localhost.or.something";
let httpBackend = new HttpBackend(); let httpBackend = new HttpBackend();
const userId = "@alice:localhost"; const userId = "@alice:localhost";
@ -19,11 +19,14 @@ describe("MatrixClient opts", function() {
presence: {}, presence: {},
rooms: { rooms: {
join: { join: {
"!foo:bar": { // roomId "!foo:bar": {
// roomId
timeline: { timeline: {
events: [ events: [
utils.mkMessage({ utils.mkMessage({
room: roomId, user: userB, msg: "hello", room: roomId,
user: userB,
msg: "hello",
}), }),
], ],
prev_batch: "f_1_1", prev_batch: "f_1_1",
@ -31,19 +34,29 @@ describe("MatrixClient opts", function() {
state: { state: {
events: [ events: [
utils.mkEvent({ utils.mkEvent({
type: "m.room.name", room: roomId, user: userB, type: "m.room.name",
room: roomId,
user: userB,
content: { content: {
name: "Old room name", name: "Old room name",
}, },
}), }),
utils.mkMembership({ utils.mkMembership({
room: roomId, mship: "join", user: userB, name: "Bob", room: roomId,
mship: "join",
user: userB,
name: "Bob",
}), }),
utils.mkMembership({ utils.mkMembership({
room: roomId, mship: "join", user: userId, name: "Alice", room: roomId,
mship: "join",
user: userId,
name: "Alice",
}), }),
utils.mkEvent({ utils.mkEvent({
type: "m.room.create", room: roomId, user: userId, type: "m.room.create",
room: roomId,
user: userId,
content: { content: {
creator: userId, creator: userId,
}, },
@ -55,18 +68,18 @@ describe("MatrixClient opts", function() {
}, },
}; };
beforeEach(function() { beforeEach(function () {
httpBackend = new HttpBackend(); httpBackend = new HttpBackend();
}); });
afterEach(function() { afterEach(function () {
httpBackend.verifyNoOutstandingExpectation(); httpBackend.verifyNoOutstandingExpectation();
return httpBackend.stop(); return httpBackend.stop();
}); });
describe("without opts.store", function() { describe("without opts.store", function () {
let client: MatrixClient; let client: MatrixClient;
beforeEach(function() { beforeEach(function () {
client = new MatrixClient({ client = new MatrixClient({
fetchFn: httpBackend.fetchFn as typeof global.fetch, fetchFn: httpBackend.fetchFn as typeof global.fetch,
store: undefined, store: undefined,
@ -77,34 +90,34 @@ describe("MatrixClient opts", function() {
}); });
}); });
afterEach(function() { afterEach(function () {
client.stopClient(); client.stopClient();
}); });
it("should be able to send messages", function(done) { it("should be able to send messages", function (done) {
const eventId = "$flibble:wibble"; const eventId = "$flibble:wibble";
httpBackend.when("PUT", "/txn1").respond(200, { httpBackend.when("PUT", "/txn1").respond(200, {
event_id: eventId, 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); expect(res.event_id).toEqual(eventId);
done(); done();
}); });
httpBackend.flush("/txn1", 1); httpBackend.flush("/txn1", 1);
}); });
it("should be able to sync / get new events", async function() { it("should be able to sync / get new events", async function () {
const expectedEventTypes = [ // from /initialSync const expectedEventTypes = [
"m.room.message", "m.room.name", "m.room.member", "m.room.member", // from /initialSync
"m.room.message",
"m.room.name",
"m.room.member",
"m.room.member",
"m.room.create", "m.room.create",
]; ];
client.on(ClientEvent.Event, function(event) { client.on(ClientEvent.Event, function (event) {
expect(expectedEventTypes.indexOf(event.getType())).not.toEqual( expect(expectedEventTypes.indexOf(event.getType())).not.toEqual(-1);
-1, expectedEventTypes.splice(expectedEventTypes.indexOf(event.getType()), 1);
);
expectedEventTypes.splice(
expectedEventTypes.indexOf(event.getType()), 1,
);
}); });
httpBackend.when("GET", "/versions").respond(200, {}); httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {}); httpBackend.when("GET", "/pushrules").respond(200, {});
@ -114,19 +127,14 @@ describe("MatrixClient opts", function() {
await httpBackend.flush("/versions", 1); await httpBackend.flush("/versions", 1);
await httpBackend.flush("/pushrules", 1); await httpBackend.flush("/pushrules", 1);
await httpBackend.flush("/filter", 1); await httpBackend.flush("/filter", 1);
await Promise.all([ await Promise.all([httpBackend.flush("/sync", 1), utils.syncPromise(client)]);
httpBackend.flush("/sync", 1), expect(expectedEventTypes.length).toEqual(0);
utils.syncPromise(client),
]);
expect(expectedEventTypes.length).toEqual(
0,
);
}); });
}); });
describe("without opts.scheduler", function() { describe("without opts.scheduler", function () {
let client: MatrixClient; let client: MatrixClient;
beforeEach(function() { beforeEach(function () {
client = new MatrixClient({ client = new MatrixClient({
fetchFn: httpBackend.fetchFn as typeof global.fetch, fetchFn: httpBackend.fetchFn as typeof global.fetch,
store: new MemoryStore() as IStore, store: new MemoryStore() as IStore,
@ -137,25 +145,31 @@ describe("MatrixClient opts", function() {
}); });
}); });
afterEach(function() { afterEach(function () {
client.stopClient(); client.stopClient();
}); });
it("shouldn't retry sending events", function(done) { it("shouldn't retry sending events", function (done) {
httpBackend.when("PUT", "/txn1").respond(500, new MatrixError({ httpBackend.when("PUT", "/txn1").respond(
errcode: "M_SOMETHING", 500,
error: "Ruh roh", new MatrixError({
})); errcode: "M_SOMETHING",
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) { error: "Ruh roh",
expect(false).toBe(true); }),
}, function(err) { );
expect(err.errcode).toEqual("M_SOMETHING"); client.sendTextMessage("!foo:bar", "a body", "txn1").then(
done(); function (res) {
}); expect(false).toBe(true);
},
function (err) {
expect(err.errcode).toEqual("M_SOMETHING");
done();
},
);
httpBackend.flush("/txn1", 1); httpBackend.flush("/txn1", 1);
}); });
it("shouldn't queue events", function(done) { it("shouldn't queue events", function (done) {
httpBackend.when("PUT", "/txn1").respond(200, { httpBackend.when("PUT", "/txn1").respond(200, {
event_id: "AAA", event_id: "AAA",
}); });
@ -164,26 +178,26 @@ describe("MatrixClient opts", function() {
}); });
let sentA = false; let sentA = false;
let sentB = 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; sentA = true;
expect(sentB).toBe(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; sentB = true;
expect(sentA).toBe(false); expect(sentA).toBe(false);
}); });
httpBackend.flush("/txn2", 1).then(function() { httpBackend.flush("/txn2", 1).then(function () {
httpBackend.flush("/txn1", 1).then(function() { httpBackend.flush("/txn1", 1).then(function () {
done(); 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, { httpBackend.when("PUT", "/txn1").respond(200, {
event_id: "foo", 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"); expect(res.event_id).toEqual("foo");
done(); done();
}); });

View File

@ -29,13 +29,7 @@ describe("MatrixClient relations", () => {
const setupTests = (): [MatrixClient, HttpBackend] => { const setupTests = (): [MatrixClient, HttpBackend] => {
const scheduler = new MatrixScheduler(); const scheduler = new MatrixScheduler();
const testClient = new TestClient( const testClient = new TestClient(userId, "DEVICE", accessToken, undefined, { scheduler });
userId,
"DEVICE",
accessToken,
undefined,
{ scheduler },
);
const httpBackend = testClient.httpBackend; const httpBackend = testClient.httpBackend;
const client = testClient.client; const client = testClient.client;
@ -52,88 +46,71 @@ describe("MatrixClient relations", () => {
}); });
it("should read related events with the default options", async () => { 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! httpBackend!.when("GET", "/rooms/!room%3Ahere/event/%24event-0").respond(200, null);
.when("GET", "/rooms/!room%3Ahere/event/%24event-0")
.respond(200, null);
httpBackend! httpBackend!
.when("GET", "/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0?dir=b") .when("GET", "/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0?dir=b")
.respond(200, { chunk: [], next_batch: 'NEXT' }); .respond(200, { chunk: [], next_batch: "NEXT" });
await httpBackend!.flushAllExpected(); 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 () => { 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! httpBackend!.when("GET", "/rooms/!room%3Ahere/event/%24event-0").respond(200, null);
.when("GET", "/rooms/!room%3Ahere/event/%24event-0")
.respond(200, null);
httpBackend! httpBackend!
.when("GET", "/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0/m.reference?dir=b") .when("GET", "/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0/m.reference?dir=b")
.respond(200, { chunk: [], next_batch: 'NEXT' }); .respond(200, { chunk: [], next_batch: "NEXT" });
await httpBackend!.flushAllExpected(); 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 () => { 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! httpBackend!
.when("GET", "/rooms/!room%3Ahere/event/%24event-0") .when("GET", "/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0/m.reference/m.room.message?dir=b")
.respond(200, null); .respond(200, { chunk: [], next_batch: "NEXT" });
httpBackend!
.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(); 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 () => { 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, dir: Direction.Forward,
from: 'FROM', from: "FROM",
limit: 10, limit: 10,
to: 'TO', to: "TO",
}); });
httpBackend!.when("GET", "/rooms/!room%3Ahere/event/%24event-0").respond(200, null);
httpBackend! httpBackend!
.when("GET", "/rooms/!room%3Ahere/event/%24event-0") .when("GET", "/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0?dir=f&from=FROM&limit=10&to=TO")
.respond(200, null); .respond(200, { chunk: [], next_batch: "NEXT" });
httpBackend!
.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(); 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 () => { it("should use default direction in the fetchRelations endpoint", async () => {
const response = client!.fetchRelations(roomId, '$event-0', null, null); const response = client!.fetchRelations(roomId, "$event-0", null, null);
httpBackend! httpBackend!
.when( .when("GET", "/rooms/!room%3Ahere/relations/%24event-0?dir=b")
"GET", .respond(200, { chunk: [], next_batch: "NEXT" });
"/rooms/!room%3Ahere/relations/%24event-0?dir=b",
)
.respond(200, { chunk: [], next_batch: 'NEXT' });
await httpBackend!.flushAllExpected(); await httpBackend!.flushAllExpected();
expect(await response).toEqual({ "chunk": [], "next_batch": "NEXT" }); expect(await response).toEqual({ chunk: [], next_batch: "NEXT" });
}); });
}); });

View File

@ -20,7 +20,7 @@ import { EventStatus, RoomEvent, MatrixClient, MatrixScheduler } from "../../src
import { Room } from "../../src/models/room"; import { Room } from "../../src/models/room";
import { TestClient } from "../TestClient"; import { TestClient } from "../TestClient";
describe("MatrixClient retrying", function() { describe("MatrixClient retrying", function () {
const userId = "@alice:localhost"; const userId = "@alice:localhost";
const accessToken = "aseukfgwef"; const accessToken = "aseukfgwef";
const roomId = "!room:here"; const roomId = "!room:here";
@ -30,13 +30,7 @@ describe("MatrixClient retrying", function() {
const setupTests = (): [MatrixClient, HttpBackend, Room] => { const setupTests = (): [MatrixClient, HttpBackend, Room] => {
const scheduler = new MatrixScheduler(); const scheduler = new MatrixScheduler();
const testClient = new TestClient( const testClient = new TestClient(userId, "DEVICE", accessToken, undefined, { scheduler });
userId,
"DEVICE",
accessToken,
undefined,
{ scheduler },
);
const httpBackend = testClient.httpBackend; const httpBackend = testClient.httpBackend;
const client = testClient.client; const client = testClient.client;
const room = new Room(roomId, client, userId); const room = new Room(roomId, client, userId);
@ -45,49 +39,46 @@ describe("MatrixClient retrying", function() {
return [client, httpBackend, room]; return [client, httpBackend, room];
}; };
beforeEach(function() { beforeEach(function () {
[client, httpBackend, room] = setupTests(); [client, httpBackend, room] = setupTests();
}); });
afterEach(function() { afterEach(function () {
httpBackend!.verifyNoOutstandingExpectation(); httpBackend!.verifyNoOutstandingExpectation();
return httpBackend!.stop(); 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() { it("should mark events as EventStatus.CANCELLED when cancelled", function () {
});
xit("should mark events as EventStatus.QUEUED when queued", function() {
});
it("should mark events as EventStatus.CANCELLED when cancelled", function() {
// send a couple of events; the second will be queued // send a couple of events; the second will be queued
const p1 = client!.sendMessage(roomId, { const p1 = client!
"msgtype": "m.text", .sendMessage(roomId, {
"body": "m1", msgtype: "m.text",
}).then(function() { body: "m1",
// we expect the first message to fail })
throw new Error('Message 1 unexpectedly sent successfully'); .then(
}, () => { function () {
// this is expected // 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 // XXX: it turns out that the promise returned by this message
// never gets resolved. // never gets resolved.
// https://github.com/matrix-org/matrix-js-sdk/issues/496 // https://github.com/matrix-org/matrix-js-sdk/issues/496
client!.sendMessage(roomId, { client!.sendMessage(roomId, {
"msgtype": "m.text", msgtype: "m.text",
"body": "m2", body: "m2",
}); });
// both events should be in the timeline at this point // both events should be in the timeline at this point
@ -100,20 +91,23 @@ describe("MatrixClient retrying", function() {
expect(ev2.status).toEqual(EventStatus.SENDING); expect(ev2.status).toEqual(EventStatus.SENDING);
// the first message should get sent, and the second should get queued // the first message should get sent, and the second should get queued
httpBackend!.when("PUT", "/send/m.room.message/").check(function() { httpBackend!
// ev2 should now have been queued .when("PUT", "/send/m.room.message/")
expect(ev2.status).toEqual(EventStatus.QUEUED); .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 // now we can cancel the second and check everything looks sane
client!.cancelPendingEvent(ev2); client!.cancelPendingEvent(ev2);
expect(ev2.status).toEqual(EventStatus.CANCELLED); expect(ev2.status).toEqual(EventStatus.CANCELLED);
expect(tl.length).toEqual(1); expect(tl.length).toEqual(1);
// shouldn't be able to cancel the first message yet // shouldn't be able to cancel the first message yet
expect(function() { expect(function () {
client!.cancelPendingEvent(ev1); client!.cancelPendingEvent(ev1);
}).toThrow(); }).toThrow();
}).respond(400); // fail the first message })
.respond(400); // fail the first message
// wait for the localecho of ev1 to be updated // wait for the localecho of ev1 to be updated
const p3 = new Promise<void>((resolve, reject) => { const p3 = new Promise<void>((resolve, reject) => {
@ -122,7 +116,7 @@ describe("MatrixClient retrying", function() {
resolve(); resolve();
} }
}); });
}).then(function() { }).then(function () {
expect(ev1.status).toEqual(EventStatus.NOT_SENT); expect(ev1.status).toEqual(EventStatus.NOT_SENT);
expect(tl.length).toEqual(1); expect(tl.length).toEqual(1);
@ -132,19 +126,11 @@ describe("MatrixClient retrying", function() {
expect(tl.length).toEqual(0); expect(tl.length).toEqual(0);
}); });
return Promise.all([ return Promise.all([p1, p3, httpBackend!.flushAllExpected()]);
p1,
p3,
httpBackend!.flushAllExpected(),
]);
}); });
describe("resending", function() { describe("resending", function () {
xit("should be able to resend a NOT_SENT event", function() { xit("should be able to resend a NOT_SENT event", function () {});
xit("should be able to resend a sent event", function () {});
});
xit("should be able to resend a sent event", function() {
});
}); });
}); });

View File

@ -26,11 +26,12 @@ import {
RoomEvent, RoomEvent,
ISyncResponse, ISyncResponse,
IMinimalEvent, IMinimalEvent,
IRoomEvent, Room, IRoomEvent,
Room,
} from "../../src"; } from "../../src";
import { TestClient } from "../TestClient"; import { TestClient } from "../TestClient";
describe("MatrixClient room timelines", function() { describe("MatrixClient room timelines", function () {
const userId = "@alice:localhost"; const userId = "@alice:localhost";
const userName = "Alice"; const userName = "Alice";
const accessToken = "aseukfgwef"; const accessToken = "aseukfgwef";
@ -40,10 +41,15 @@ describe("MatrixClient room timelines", function() {
let httpBackend: HttpBackend | undefined; let httpBackend: HttpBackend | undefined;
const USER_MEMBERSHIP_EVENT = utils.mkMembership({ const USER_MEMBERSHIP_EVENT = utils.mkMembership({
room: roomId, mship: "join", user: userId, name: userName, room: roomId,
mship: "join",
user: userId,
name: userName,
}); });
const ROOM_NAME_EVENT = utils.mkEvent({ const ROOM_NAME_EVENT = utils.mkEvent({
type: "m.room.name", room: roomId, user: otherUserId, type: "m.room.name",
room: roomId,
user: otherUserId,
content: { content: {
name: "Old room name", name: "Old room name",
}, },
@ -53,11 +59,14 @@ describe("MatrixClient room timelines", function() {
next_batch: "s_5_3", next_batch: "s_5_3",
rooms: { rooms: {
join: { join: {
"!foo:bar": { // roomId "!foo:bar": {
// roomId
timeline: { timeline: {
events: [ events: [
utils.mkMessage({ utils.mkMessage({
room: roomId, user: otherUserId, msg: "hello", room: roomId,
user: otherUserId,
msg: "hello",
}), }),
], ],
prev_batch: "f_1_1", prev_batch: "f_1_1",
@ -66,12 +75,16 @@ describe("MatrixClient room timelines", function() {
events: [ events: [
ROOM_NAME_EVENT, ROOM_NAME_EVENT,
utils.mkMembership({ utils.mkMembership({
room: roomId, mship: "join", room: roomId,
user: otherUserId, name: "Bob", mship: "join",
user: otherUserId,
name: "Bob",
}), }),
USER_MEMBERSHIP_EVENT, USER_MEMBERSHIP_EVENT,
utils.mkEvent({ utils.mkEvent({
type: "m.room.create", room: roomId, user: userId, type: "m.room.create",
room: roomId,
user: userId,
content: { content: {
creator: userId, creator: userId,
}, },
@ -99,7 +112,7 @@ describe("MatrixClient room timelines", function() {
leave: {}, leave: {},
} as unknown as ISyncResponse["rooms"], } as unknown as ISyncResponse["rooms"],
}; };
events.forEach(function(e) { events.forEach(function (e) {
if (e.room_id !== roomId) { if (e.room_id !== roomId) {
throw new Error("setNextSyncData only works with one room id"); throw new Error("setNextSyncData only works with one room id");
} }
@ -116,13 +129,7 @@ describe("MatrixClient room timelines", function() {
const setupTestClient = (): [MatrixClient, HttpBackend] => { const setupTestClient = (): [MatrixClient, HttpBackend] => {
// these tests should work with or without timelineSupport // these tests should work with or without timelineSupport
const testClient = new TestClient( const testClient = new TestClient(userId, "DEVICE", accessToken, undefined, { timelineSupport: true });
userId,
"DEVICE",
accessToken,
undefined,
{ timelineSupport: true },
);
const httpBackend = testClient.httpBackend; const httpBackend = testClient.httpBackend;
const client = testClient.client; const client = testClient.client;
@ -131,7 +138,7 @@ describe("MatrixClient room timelines", function() {
httpBackend!.when("GET", "/pushrules").respond(200, {}); httpBackend!.when("GET", "/pushrules").respond(200, {});
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "fid" }); httpBackend!.when("POST", "/filter").respond(200, { filter_id: "fid" });
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA); httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, function() { httpBackend!.when("GET", "/sync").respond(200, function () {
return NEXT_SYNC_DATA; return NEXT_SYNC_DATA;
}); });
client!.startClient(); client!.startClient();
@ -139,119 +146,131 @@ describe("MatrixClient room timelines", function() {
return [client!, httpBackend]; return [client!, httpBackend];
}; };
beforeEach(async function() { beforeEach(async function () {
[client!, httpBackend] = setupTestClient(); [client!, httpBackend] = setupTestClient();
await httpBackend.flush("/versions"); await httpBackend.flush("/versions");
await httpBackend.flush("/pushrules"); await httpBackend.flush("/pushrules");
await httpBackend.flush("/filter"); await httpBackend.flush("/filter");
}); });
afterEach(function() { afterEach(function () {
httpBackend!.verifyNoOutstandingExpectation(); httpBackend!.verifyNoOutstandingExpectation();
client!.stopClient(); client!.stopClient();
return httpBackend!.stop(); return httpBackend!.stop();
}); });
describe("local echo events", function() { describe("local echo events", function () {
it("should be added immediately after calling MatrixClient.sendEvent " + it(
"with EventStatus.SENDING and the right event.sender", function(done) { "should be added immediately after calling MatrixClient.sendEvent " +
client!.on(ClientEvent.Sync, function(state) { "with EventStatus.SENDING and the right event.sender",
if (state !== "PREPARED") { function (done) {
return; client!.on(ClientEvent.Sync, function (state) {
} if (state !== "PREPARED") {
const room = client!.getRoom(roomId)!; return;
expect(room.timeline.length).toEqual(1); }
const room = client!.getRoom(roomId)!;
expect(room.timeline.length).toEqual(1);
client!.sendTextMessage(roomId, "I am a fish", "txn1"); client!.sendTextMessage(roomId, "I am a fish", "txn1");
// check it was added // check it was added
expect(room.timeline.length).toEqual(2); expect(room.timeline.length).toEqual(2);
// check status // check status
expect(room.timeline[1].status).toEqual(EventStatus.SENDING); expect(room.timeline[1].status).toEqual(EventStatus.SENDING);
// check member // check member
const member = room.timeline[1].sender; const member = room.timeline[1].sender;
expect(member?.userId).toEqual(userId); expect(member?.userId).toEqual(userId);
expect(member?.name).toEqual(userName); expect(member?.name).toEqual(userName);
httpBackend!.flush("/sync", 1).then(function() { httpBackend!.flush("/sync", 1).then(function () {
done(); done();
});
}); });
}); httpBackend!.flush("/sync", 1);
httpBackend!.flush("/sync", 1); },
}); );
it("should be updated correctly when the send request finishes " + it(
"BEFORE the event comes down the event stream", function(done) { "should be updated correctly when the send request finishes " +
const eventId = "$foo:bar"; "BEFORE the event comes down the event stream",
httpBackend!.when("PUT", "/txn1").respond(200, { function (done) {
event_id: eventId, const eventId = "$foo:bar";
}); httpBackend!.when("PUT", "/txn1").respond(200, {
event_id: eventId,
});
const ev = utils.mkMessage({ const ev = utils.mkMessage({
msg: "I am a fish", user: userId, room: roomId, msg: "I am a fish",
}); user: userId,
ev.event_id = eventId; room: roomId,
ev.unsigned = { transaction_id: "txn1" }; });
setNextSyncData([ev]); ev.event_id = eventId;
ev.unsigned = { transaction_id: "txn1" };
setNextSyncData([ev]);
client!.on(ClientEvent.Sync, function(state) { client!.on(ClientEvent.Sync, function (state) {
if (state !== "PREPARED") { if (state !== "PREPARED") {
return; return;
} }
const room = client!.getRoom(roomId)!; const room = client!.getRoom(roomId)!;
client!.sendTextMessage(roomId, "I am a fish", "txn1").then( client!.sendTextMessage(roomId, "I am a fish", "txn1").then(function () {
function() {
expect(room.timeline[1].getId()).toEqual(eventId); expect(room.timeline[1].getId()).toEqual(eventId);
httpBackend!.flush("/sync", 1).then(function() { httpBackend!.flush("/sync", 1).then(function () {
expect(room.timeline[1].getId()).toEqual(eventId); expect(room.timeline[1].getId()).toEqual(eventId);
done(); done();
}); });
}); });
httpBackend!.flush("/txn1", 1);
});
httpBackend!.flush("/sync", 1);
});
it("should be updated correctly when the send request finishes " +
"AFTER the event comes down the event stream", function(done) {
const eventId = "$foo:bar";
httpBackend!.when("PUT", "/txn1").respond(200, {
event_id: eventId,
});
const ev = utils.mkMessage({
msg: "I am a fish", user: userId, room: roomId,
});
ev.event_id = eventId;
ev.unsigned = { transaction_id: "txn1" };
setNextSyncData([ev]);
client!.on(ClientEvent.Sync, function(state) {
if (state !== "PREPARED") {
return;
}
const room = client!.getRoom(roomId)!;
const promise = client!.sendTextMessage(roomId, "I am a fish", "txn1");
httpBackend!.flush("/sync", 1).then(function() {
expect(room.timeline.length).toEqual(2);
httpBackend!.flush("/txn1", 1); httpBackend!.flush("/txn1", 1);
promise.then(function() { });
httpBackend!.flush("/sync", 1);
},
);
it(
"should be updated correctly when the send request finishes " +
"AFTER the event comes down the event stream",
function (done) {
const eventId = "$foo:bar";
httpBackend!.when("PUT", "/txn1").respond(200, {
event_id: eventId,
});
const ev = utils.mkMessage({
msg: "I am a fish",
user: userId,
room: roomId,
});
ev.event_id = eventId;
ev.unsigned = { transaction_id: "txn1" };
setNextSyncData([ev]);
client!.on(ClientEvent.Sync, function (state) {
if (state !== "PREPARED") {
return;
}
const room = client!.getRoom(roomId)!;
const promise = client!.sendTextMessage(roomId, "I am a fish", "txn1");
httpBackend!.flush("/sync", 1).then(function () {
expect(room.timeline.length).toEqual(2); expect(room.timeline.length).toEqual(2);
expect(room.timeline[1].getId()).toEqual(eventId); httpBackend!.flush("/txn1", 1);
done(); promise.then(function () {
expect(room.timeline.length).toEqual(2);
expect(room.timeline[1].getId()).toEqual(eventId);
done();
});
}); });
}); });
}); httpBackend!.flush("/sync", 1);
httpBackend!.flush("/sync", 1); },
}); );
}); });
describe("paginated events", function() { describe("paginated events", function () {
let sbEvents: Partial<IEvent>[]; let sbEvents: Partial<IEvent>[];
const sbEndTok = "pagin_end"; const sbEndTok = "pagin_end";
beforeEach(function() { beforeEach(function () {
sbEvents = []; sbEvents = [];
httpBackend!.when("GET", "/messages").respond(200, function() { httpBackend!.when("GET", "/messages").respond(200, function () {
return { return {
chunk: sbEvents, chunk: sbEvents,
start: "pagin_start", start: "pagin_start",
@ -260,16 +279,15 @@ describe("MatrixClient room timelines", function() {
}); });
}); });
it("should set Room.oldState.paginationToken to null at the start" + it("should set Room.oldState.paginationToken to null at the start" + " of the timeline.", function (done) {
" of the timeline.", function(done) { client!.on(ClientEvent.Sync, function (state) {
client!.on(ClientEvent.Sync, function(state) {
if (state !== "PREPARED") { if (state !== "PREPARED") {
return; return;
} }
const room = client!.getRoom(roomId)!; const room = client!.getRoom(roomId)!;
expect(room.timeline.length).toEqual(1); expect(room.timeline.length).toEqual(1);
client!.scrollback(room).then(function() { client!.scrollback(room).then(function () {
expect(room.timeline.length).toEqual(1); expect(room.timeline.length).toEqual(1);
expect(room.oldState.paginationToken).toBe(null); expect(room.oldState.paginationToken).toBe(null);
@ -284,7 +302,7 @@ describe("MatrixClient room timelines", function() {
httpBackend!.flush("/sync", 1); httpBackend!.flush("/sync", 1);
}); });
it("should set the right event.sender values", function(done) { it("should set the right event.sender values", function (done) {
// We're aiming for an eventual timeline of: // We're aiming for an eventual timeline of:
// //
// 'Old Alice' joined the room // 'Old Alice' joined the room
@ -296,14 +314,20 @@ describe("MatrixClient room timelines", function() {
// make an m.room.member event for alice's join // make an m.room.member event for alice's join
const joinMshipEvent = utils.mkMembership({ const joinMshipEvent = utils.mkMembership({
mship: "join", user: userId, room: roomId, name: "Old Alice", mship: "join",
user: userId,
room: roomId,
name: "Old Alice",
url: undefined, url: undefined,
}); });
// make an m.room.member event with prev_content for alice's nick // make an m.room.member event with prev_content for alice's nick
// change // change
const oldMshipEvent = utils.mkMembership({ const oldMshipEvent = utils.mkMembership({
mship: "join", user: userId, room: roomId, name: userName, mship: "join",
user: userId,
room: roomId,
name: userName,
url: "mxc://some/url", url: "mxc://some/url",
}); });
oldMshipEvent.prev_content = { oldMshipEvent.prev_content = {
@ -316,16 +340,20 @@ describe("MatrixClient room timelines", function() {
// N.B. synapse returns /messages in reverse chronological order // N.B. synapse returns /messages in reverse chronological order
sbEvents = [ sbEvents = [
utils.mkMessage({ utils.mkMessage({
user: userId, room: roomId, msg: "I'm alice", user: userId,
room: roomId,
msg: "I'm alice",
}), }),
oldMshipEvent, oldMshipEvent,
utils.mkMessage({ utils.mkMessage({
user: userId, room: roomId, msg: "I'm old alice", user: userId,
room: roomId,
msg: "I'm old alice",
}), }),
joinMshipEvent, joinMshipEvent,
]; ];
client!.on(ClientEvent.Sync, function(state) { client!.on(ClientEvent.Sync, function (state) {
if (state !== "PREPARED") { if (state !== "PREPARED") {
return; return;
} }
@ -333,7 +361,7 @@ describe("MatrixClient room timelines", function() {
// sync response // sync response
expect(room.timeline.length).toEqual(1); expect(room.timeline.length).toEqual(1);
client!.scrollback(room).then(function() { client!.scrollback(room).then(function () {
expect(room.timeline.length).toEqual(5); expect(room.timeline.length).toEqual(5);
const joinMsg = room.timeline[0]; const joinMsg = room.timeline[0];
expect(joinMsg.sender?.name).toEqual("Old Alice"); expect(joinMsg.sender?.name).toEqual("Old Alice");
@ -353,25 +381,29 @@ describe("MatrixClient room timelines", function() {
httpBackend!.flush("/sync", 1); httpBackend!.flush("/sync", 1);
}); });
it("should add it them to the right place in the timeline", function(done) { it("should add it them to the right place in the timeline", function (done) {
// set the list of events to return on scrollback // set the list of events to return on scrollback
sbEvents = [ sbEvents = [
utils.mkMessage({ utils.mkMessage({
user: userId, room: roomId, msg: "I am new", user: userId,
room: roomId,
msg: "I am new",
}), }),
utils.mkMessage({ utils.mkMessage({
user: userId, room: roomId, msg: "I am old", user: userId,
room: roomId,
msg: "I am old",
}), }),
]; ];
client!.on(ClientEvent.Sync, function(state) { client!.on(ClientEvent.Sync, function (state) {
if (state !== "PREPARED") { if (state !== "PREPARED") {
return; return;
} }
const room = client!.getRoom(roomId)!; const room = client!.getRoom(roomId)!;
expect(room.timeline.length).toEqual(1); expect(room.timeline.length).toEqual(1);
client!.scrollback(room).then(function() { client!.scrollback(room).then(function () {
expect(room.timeline.length).toEqual(3); expect(room.timeline.length).toEqual(3);
expect(room.timeline[0].event).toEqual(sbEvents[1]); expect(room.timeline[0].event).toEqual(sbEvents[1]);
expect(room.timeline[1].event).toEqual(sbEvents[0]); expect(room.timeline[1].event).toEqual(sbEvents[0]);
@ -387,26 +419,28 @@ describe("MatrixClient room timelines", function() {
httpBackend!.flush("/sync", 1); httpBackend!.flush("/sync", 1);
}); });
it("should use 'end' as the next pagination token", function(done) { it("should use 'end' as the next pagination token", function (done) {
// set the list of events to return on scrollback // set the list of events to return on scrollback
sbEvents = [ sbEvents = [
utils.mkMessage({ utils.mkMessage({
user: userId, room: roomId, msg: "I am new", user: userId,
room: roomId,
msg: "I am new",
}), }),
]; ];
client!.on(ClientEvent.Sync, function(state) { client!.on(ClientEvent.Sync, function (state) {
if (state !== "PREPARED") { if (state !== "PREPARED") {
return; return;
} }
const room = client!.getRoom(roomId)!; const room = client!.getRoom(roomId)!;
expect(room.oldState.paginationToken).toBeTruthy(); expect(room.oldState.paginationToken).toBeTruthy();
client!.scrollback(room, 1).then(function() { client!.scrollback(room, 1).then(function () {
expect(room.oldState.paginationToken).toEqual(sbEndTok); expect(room.oldState.paginationToken).toEqual(sbEndTok);
}); });
httpBackend!.flush("/messages", 1).then(function() { httpBackend!.flush("/messages", 1).then(function () {
// still have a sync to flush // still have a sync to flush
httpBackend!.flush("/sync", 1).then(() => { httpBackend!.flush("/sync", 1).then(() => {
done(); done();
@ -417,22 +451,19 @@ describe("MatrixClient room timelines", function() {
}); });
}); });
describe("new events", function() { describe("new events", function () {
it("should be added to the right place in the timeline", function() { it("should be added to the right place in the timeline", function () {
const eventData = [ const eventData = [
utils.mkMessage({ user: userId, room: roomId }), utils.mkMessage({ user: userId, room: roomId }),
utils.mkMessage({ user: userId, room: roomId }), utils.mkMessage({ user: userId, room: roomId }),
]; ];
setNextSyncData(eventData); setNextSyncData(eventData);
return Promise.all([ return Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]).then(() => {
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(() => {
const room = client!.getRoom(roomId)!; const room = client!.getRoom(roomId)!;
let index = 0; let index = 0;
client!.on(RoomEvent.Timeline, function(event, rm, toStart) { client!.on(RoomEvent.Timeline, function (event, rm, toStart) {
expect(toStart).toBe(false); expect(toStart).toBe(false);
expect(rm).toEqual(room); expect(rm).toEqual(room);
expect(event.event).toEqual(eventData[index]); expect(event.event).toEqual(eventData[index]);
@ -440,41 +471,31 @@ describe("MatrixClient room timelines", function() {
}); });
httpBackend!.flush("/messages", 1); httpBackend!.flush("/messages", 1);
return Promise.all([ return Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]).then(function () {
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(function() {
expect(index).toEqual(2); expect(index).toEqual(2);
expect(room.timeline.length).toEqual(3); expect(room.timeline.length).toEqual(3);
expect(room.timeline[2].event).toEqual( expect(room.timeline[2].event).toEqual(eventData[1]);
eventData[1], expect(room.timeline[1].event).toEqual(eventData[0]);
);
expect(room.timeline[1].event).toEqual(
eventData[0],
);
}); });
}); });
}); });
it("should set the right event.sender values", function() { it("should set the right event.sender values", function () {
const eventData = [ const eventData = [
utils.mkMessage({ user: userId, room: roomId }), utils.mkMessage({ user: userId, room: roomId }),
utils.mkMembership({ utils.mkMembership({
user: userId, room: roomId, mship: "join", name: "New Name", user: userId,
room: roomId,
mship: "join",
name: "New Name",
}), }),
utils.mkMessage({ user: userId, room: roomId }), utils.mkMessage({ user: userId, room: roomId }),
]; ];
setNextSyncData(eventData); setNextSyncData(eventData);
return Promise.all([ return Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]).then(() => {
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(() => {
const room = client!.getRoom(roomId)!; const room = client!.getRoom(roomId)!;
return Promise.all([ return Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]).then(function () {
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(function() {
const preNameEvent = room.timeline[room.timeline.length - 3]; const preNameEvent = room.timeline[room.timeline.length - 3];
const postNameEvent = room.timeline[room.timeline.length - 1]; const postNameEvent = room.timeline[room.timeline.length - 1];
expect(preNameEvent.sender?.name).toEqual(userName); expect(preNameEvent.sender?.name).toEqual(userName);
@ -483,90 +504,83 @@ describe("MatrixClient room timelines", function() {
}); });
}); });
it("should set the right room.name", function() { it("should set the right room.name", function () {
const secondRoomNameEvent = utils.mkEvent({ const secondRoomNameEvent = utils.mkEvent({
user: userId, room: roomId, type: "m.room.name", content: { user: userId,
room: roomId,
type: "m.room.name",
content: {
name: "Room 2", name: "Room 2",
}, },
}); });
setNextSyncData([secondRoomNameEvent]); setNextSyncData([secondRoomNameEvent]);
return Promise.all([ return Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]).then(() => {
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(() => {
const room = client!.getRoom(roomId)!; const room = client!.getRoom(roomId)!;
let nameEmitCount = 0; let nameEmitCount = 0;
client!.on(RoomEvent.Name, function(rm) { client!.on(RoomEvent.Name, function (rm) {
nameEmitCount += 1; nameEmitCount += 1;
}); });
return Promise.all([ return Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)])
httpBackend!.flush("/sync", 1), .then(function () {
utils.syncPromise(client!), expect(nameEmitCount).toEqual(1);
]).then(function() { expect(room.name).toEqual("Room 2");
expect(nameEmitCount).toEqual(1); // do another round
expect(room.name).toEqual("Room 2"); const thirdRoomNameEvent = utils.mkEvent({
// do another round user: userId,
const thirdRoomNameEvent = utils.mkEvent({ room: roomId,
user: userId, room: roomId, type: "m.room.name", content: { type: "m.room.name",
name: "Room 3", content: {
}, name: "Room 3",
},
});
setNextSyncData([thirdRoomNameEvent]);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
return Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]);
})
.then(function () {
expect(nameEmitCount).toEqual(2);
expect(room.name).toEqual("Room 3");
}); });
setNextSyncData([thirdRoomNameEvent]);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
return Promise.all([
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]);
}).then(function() {
expect(nameEmitCount).toEqual(2);
expect(room.name).toEqual("Room 3");
});
}); });
}); });
it("should set the right room members", function() { it("should set the right room members", function () {
const userC = "@cee:bar"; const userC = "@cee:bar";
const userD = "@dee:bar"; const userD = "@dee:bar";
const eventData = [ const eventData = [
utils.mkMembership({ utils.mkMembership({
user: userC, room: roomId, mship: "join", name: "C", user: userC,
room: roomId,
mship: "join",
name: "C",
}), }),
utils.mkMembership({ utils.mkMembership({
user: userC, room: roomId, mship: "invite", skey: userD, user: userC,
room: roomId,
mship: "invite",
skey: userD,
}), }),
]; ];
setNextSyncData(eventData); setNextSyncData(eventData);
return Promise.all([ return Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]).then(() => {
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(() => {
const room = client!.getRoom(roomId)!; const room = client!.getRoom(roomId)!;
return Promise.all([ return Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]).then(function () {
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(function() {
expect(room.currentState.getMembers().length).toEqual(4); expect(room.currentState.getMembers().length).toEqual(4);
expect(room.currentState.getMember(userC)!.name).toEqual("C"); expect(room.currentState.getMember(userC)!.name).toEqual("C");
expect(room.currentState.getMember(userC)!.membership).toEqual( expect(room.currentState.getMember(userC)!.membership).toEqual("join");
"join",
);
expect(room.currentState.getMember(userD)!.name).toEqual(userD); expect(room.currentState.getMember(userD)!.name).toEqual(userD);
expect(room.currentState.getMember(userD)!.membership).toEqual( expect(room.currentState.getMember(userD)!.membership).toEqual("invite");
"invite",
);
}); });
}); });
}); });
}); });
describe("gappy sync", function() { describe("gappy sync", function () {
it("should copy the last known state to the new timeline", function() { it("should copy the last known state to the new timeline", function () {
const eventData = [ const eventData = [utils.mkMessage({ user: userId, room: roomId })];
utils.mkMessage({ user: userId, room: roomId }),
];
setNextSyncData(eventData); setNextSyncData(eventData);
NEXT_SYNC_DATA.rooms!.join[roomId].timeline.limited = true; NEXT_SYNC_DATA.rooms!.join[roomId].timeline.limited = true;
@ -578,72 +592,56 @@ describe("MatrixClient room timelines", function() {
const room = client!.getRoom(roomId)!; const room = client!.getRoom(roomId)!;
httpBackend!.flush("/messages", 1); httpBackend!.flush("/messages", 1);
return Promise.all([ return Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]).then(function () {
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(function() {
expect(room.timeline.length).toEqual(1); expect(room.timeline.length).toEqual(1);
expect(room.timeline[0].event).toEqual(eventData[0]); expect(room.timeline[0].event).toEqual(eventData[0]);
expect(room.currentState.getMembers().length).toEqual(2); expect(room.currentState.getMembers().length).toEqual(2);
expect(room.currentState.getMember(userId)!.name).toEqual(userName); expect(room.currentState.getMember(userId)!.name).toEqual(userName);
expect(room.currentState.getMember(userId)!.membership).toEqual( expect(room.currentState.getMember(userId)!.membership).toEqual("join");
"join",
);
expect(room.currentState.getMember(otherUserId)!.name).toEqual("Bob"); expect(room.currentState.getMember(otherUserId)!.name).toEqual("Bob");
expect(room.currentState.getMember(otherUserId)!.membership).toEqual( expect(room.currentState.getMember(otherUserId)!.membership).toEqual("join");
"join",
);
}); });
}); });
}); });
it("should emit a `RoomEvent.TimelineReset` event when the sync response is `limited`", function() { it("should emit a `RoomEvent.TimelineReset` event when the sync response is `limited`", function () {
const eventData = [ const eventData = [utils.mkMessage({ user: userId, room: roomId })];
utils.mkMessage({ user: userId, room: roomId }),
];
setNextSyncData(eventData); setNextSyncData(eventData);
NEXT_SYNC_DATA.rooms!.join[roomId].timeline.limited = true; NEXT_SYNC_DATA.rooms!.join[roomId].timeline.limited = true;
return Promise.all([ return Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]).then(() => {
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(() => {
const room = client!.getRoom(roomId)!; const room = client!.getRoom(roomId)!;
let emitCount = 0; let emitCount = 0;
client!.on(RoomEvent.TimelineReset, function(emitRoom) { client!.on(RoomEvent.TimelineReset, function (emitRoom) {
expect(emitRoom).toEqual(room); expect(emitRoom).toEqual(room);
emitCount++; emitCount++;
}); });
httpBackend!.flush("/messages", 1); httpBackend!.flush("/messages", 1);
return Promise.all([ return Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]).then(function () {
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(function() {
expect(emitCount).toEqual(1); expect(emitCount).toEqual(1);
}); });
}); });
}); });
}); });
describe('Refresh live timeline', () => { describe("Refresh live timeline", () => {
const initialSyncEventData = [ const initialSyncEventData = [
utils.mkMessage({ user: userId, room: roomId }), utils.mkMessage({ user: userId, room: roomId }),
utils.mkMessage({ user: userId, room: roomId }), utils.mkMessage({ user: userId, room: roomId }),
utils.mkMessage({ user: userId, room: roomId }), utils.mkMessage({ user: userId, room: roomId }),
]; ];
const contextUrl = `/rooms/${encodeURIComponent(roomId)}/context/` + const contextUrl =
`${encodeURIComponent(initialSyncEventData[2].event_id!)}`; `/rooms/${encodeURIComponent(roomId)}/context/` +
`${encodeURIComponent(initialSyncEventData[2].event_id!)}`;
const contextResponse = { const contextResponse = {
start: "start_token", start: "start_token",
events_before: [initialSyncEventData[1], initialSyncEventData[0]], events_before: [initialSyncEventData[1], initialSyncEventData[0]],
event: initialSyncEventData[2], event: initialSyncEventData[2],
events_after: [], events_after: [],
state: [ state: [USER_MEMBERSHIP_EVENT],
USER_MEMBERSHIP_EVENT,
],
end: "end_token", end: "end_token",
}; };
@ -652,32 +650,25 @@ describe("MatrixClient room timelines", function() {
setNextSyncData(initialSyncEventData); setNextSyncData(initialSyncEventData);
// Create a room from the sync // Create a room from the sync
await Promise.all([ await Promise.all([httpBackend!.flushAllExpected(), utils.syncPromise(client!, 1)]);
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 1),
]);
// Get the room after the first sync so the room is created // Get the room after the first sync so the room is created
room = client!.getRoom(roomId)!; room = client!.getRoom(roomId)!;
expect(room).toBeTruthy(); expect(room).toBeTruthy();
}); });
it('should clear and refresh messages in timeline', async () => { it("should clear and refresh messages in timeline", async () => {
// `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()` // `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()`
// to construct a new timeline from. // to construct a new timeline from.
httpBackend!.when("GET", contextUrl) httpBackend!.when("GET", contextUrl).respond(200, function () {
.respond(200, function() { // The timeline should be cleared at this point in the refresh
// The timeline should be cleared at this point in the refresh expect(room.timeline.length).toEqual(0);
expect(room.timeline.length).toEqual(0);
return contextResponse; return contextResponse;
}); });
// Refresh the timeline. // Refresh the timeline.
await Promise.all([ await Promise.all([room.refreshLiveTimeline(), httpBackend!.flushAllExpected()]);
room.refreshLiveTimeline(),
httpBackend!.flushAllExpected(),
]);
// Make sure the message are visible // Make sure the message are visible
const resultantEventsInTimeline = room.getUnfilteredTimelineSet().getLiveTimeline().getEvents(); const resultantEventsInTimeline = room.getUnfilteredTimelineSet().getLiveTimeline().getEvents();
@ -689,7 +680,7 @@ describe("MatrixClient room timelines", function() {
]); ]);
}); });
it('Perfectly merges timelines if a sync finishes while refreshing the timeline', async () => { it("Perfectly merges timelines if a sync finishes while refreshing the timeline", async () => {
// `/context` request for `refreshLiveTimeline()` -> // `/context` request for `refreshLiveTimeline()` ->
// `getEventTimeline()` to construct a new timeline from. // `getEventTimeline()` to construct a new timeline from.
// //
@ -698,11 +689,10 @@ describe("MatrixClient room timelines", function() {
// middle of all of this refresh timeline logic. We want to make // middle of all of this refresh timeline logic. We want to make
// sure the sync pagination still works as expected after messing // sure the sync pagination still works as expected after messing
// the refresh timline logic messes with the pagination tokens. // the refresh timline logic messes with the pagination tokens.
httpBackend!.when("GET", contextUrl) httpBackend!.when("GET", contextUrl).respond(200, () => {
.respond(200, () => { // Now finally return and make the `/context` request respond
// Now finally return and make the `/context` request respond return contextResponse;
return contextResponse; });
});
// Wait for the timeline to reset(when it goes blank) which means // Wait for the timeline to reset(when it goes blank) which means
// it's in the middle of the refrsh logic right before the // it's in the middle of the refrsh logic right before the
@ -714,22 +704,20 @@ describe("MatrixClient room timelines", function() {
// //
// We define this here so the event listener is in place before we // We define this here so the event listener is in place before we
// call `room.refreshLiveTimeline()`. // call `room.refreshLiveTimeline()`.
const racingSyncEventData = [ const racingSyncEventData = [utils.mkMessage({ user: userId, room: roomId })];
utils.mkMessage({ user: userId, room: roomId }),
];
const waitForRaceySyncAfterResetPromise = new Promise<void>((resolve, reject) => { const waitForRaceySyncAfterResetPromise = new Promise<void>((resolve, reject) => {
let eventFired = false; let eventFired = false;
// Throw a more descriptive error if this part of the test times out. // Throw a more descriptive error if this part of the test times out.
const failTimeout = setTimeout(() => { const failTimeout = setTimeout(() => {
if (eventFired) { if (eventFired) {
reject(new Error( reject(
'TestError: `RoomEvent.TimelineReset` fired but we timed out trying to make' + new Error(
'a `/sync` happen in time.', "TestError: `RoomEvent.TimelineReset` fired but we timed out trying to make" +
)); "a `/sync` happen in time.",
),
);
} else { } else {
reject(new Error( reject(new Error("TestError: Timed out while waiting for `RoomEvent.TimelineReset` to fire."));
'TestError: Timed out while waiting for `RoomEvent.TimelineReset` to fire.',
));
} }
}, 4000 /* FIXME: Is there a way to reference the current timeout of this test in Jest? */); }, 4000 /* FIXME: Is there a way to reference the current timeout of this test in Jest? */);
@ -743,23 +731,19 @@ describe("MatrixClient room timelines", function() {
// Then make a `/sync` happen by sending a message and seeing that it // Then make a `/sync` happen by sending a message and seeing that it
// shows up (simulate a /sync naturally racing with us). // shows up (simulate a /sync naturally racing with us).
setNextSyncData(racingSyncEventData); setNextSyncData(racingSyncEventData);
httpBackend!.when("GET", "/sync").respond(200, function() { httpBackend!.when("GET", "/sync").respond(200, function () {
return NEXT_SYNC_DATA; return NEXT_SYNC_DATA;
}); });
await Promise.all([ await Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!, 1)]);
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!, 1),
]);
// Make sure the timeline has the racey sync data // Make sure the timeline has the racey sync data
const afterRaceySyncTimelineEvents = room const afterRaceySyncTimelineEvents = room
.getUnfilteredTimelineSet() .getUnfilteredTimelineSet()
.getLiveTimeline() .getLiveTimeline()
.getEvents(); .getEvents();
const afterRaceySyncTimelineEventIds = afterRaceySyncTimelineEvents const afterRaceySyncTimelineEventIds = afterRaceySyncTimelineEvents.map((event) =>
.map((event) => event.getId()); event.getId(),
expect(afterRaceySyncTimelineEventIds).toEqual([ );
racingSyncEventData[0].event_id, expect(afterRaceySyncTimelineEventIds).toEqual([racingSyncEventData[0].event_id]);
]);
clearTimeout(failTimeout); clearTimeout(failTimeout);
resolve(); resolve();
@ -783,17 +767,12 @@ describe("MatrixClient room timelines", function() {
// Make sure sync pagination still works by seeing a new message show up // Make sure sync pagination still works by seeing a new message show up
// after refreshing the timeline. // after refreshing the timeline.
const afterRefreshEventData = [ const afterRefreshEventData = [utils.mkMessage({ user: userId, room: roomId })];
utils.mkMessage({ user: userId, room: roomId }),
];
setNextSyncData(afterRefreshEventData); setNextSyncData(afterRefreshEventData);
httpBackend!.when("GET", "/sync").respond(200, function() { httpBackend!.when("GET", "/sync").respond(200, function () {
return NEXT_SYNC_DATA; return NEXT_SYNC_DATA;
}); });
await Promise.all([ await Promise.all([httpBackend!.flushAllExpected(), utils.syncPromise(client!, 1)]);
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 1),
]);
// Make sure the timeline includes the the events from the `/sync` // Make sure the timeline includes the the events from the `/sync`
// that raced and beat us in the middle of everything and the // that raced and beat us in the middle of everything and the
@ -808,17 +787,24 @@ describe("MatrixClient room timelines", function() {
]); ]);
}); });
it('Timeline recovers after `/context` request to generate new timeline fails', async () => { it("Timeline recovers after `/context` request to generate new timeline fails", async () => {
// `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()` // `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()`
// to construct a new timeline from. // to construct a new timeline from.
httpBackend!.when("GET", contextUrl).check(() => { httpBackend!
// The timeline should be cleared at this point in the refresh .when("GET", contextUrl)
expect(room.timeline.length).toEqual(0); .check(() => {
}).respond(500, new MatrixError({ // The timeline should be cleared at this point in the refresh
errcode: 'TEST_FAKE_ERROR', expect(room.timeline.length).toEqual(0);
error: 'We purposely intercepted this /context request to make it fail ' + })
'in order to test whether the refresh timeline code is resilient', .respond(
})); 500,
new MatrixError({
errcode: "TEST_FAKE_ERROR",
error:
"We purposely intercepted this /context request to make it fail " +
"in order to test whether the refresh timeline code is resilient",
}),
);
// Refresh the timeline and expect it to fail // Refresh the timeline and expect it to fail
const settledFailedRefreshPromises = await Promise.allSettled([ const settledFailedRefreshPromises = await Promise.allSettled([
@ -827,9 +813,9 @@ describe("MatrixClient room timelines", function() {
]); ]);
// We only expect `TEST_FAKE_ERROR` here. Anything else is // We only expect `TEST_FAKE_ERROR` here. Anything else is
// unexpected and should fail the test. // unexpected and should fail the test.
if (settledFailedRefreshPromises[0].status === 'fulfilled') { if (settledFailedRefreshPromises[0].status === "fulfilled") {
throw new Error('Expected the /context request to fail with a 500'); throw new Error("Expected the /context request to fail with a 500");
} else if (settledFailedRefreshPromises[0].reason.errcode !== 'TEST_FAKE_ERROR') { } else if (settledFailedRefreshPromises[0].reason.errcode !== "TEST_FAKE_ERROR") {
throw settledFailedRefreshPromises[0].reason; throw settledFailedRefreshPromises[0].reason;
} }
@ -839,45 +825,37 @@ describe("MatrixClient room timelines", function() {
// `/messages` request for `refreshLiveTimeline()` -> // `/messages` request for `refreshLiveTimeline()` ->
// `getLatestTimeline()` to construct a new timeline from. // `getLatestTimeline()` to construct a new timeline from.
httpBackend!.when("GET", `/rooms/${encodeURIComponent(roomId)}/messages`) httpBackend!.when("GET", `/rooms/${encodeURIComponent(roomId)}/messages`).respond(200, function () {
.respond(200, function() { return {
return { chunk: [
chunk: [{ {
// The latest message in the room // The latest message in the room
event_id: initialSyncEventData[2].event_id, event_id: initialSyncEventData[2].event_id,
}], },
}; ],
}); };
});
// `/context` request for `refreshLiveTimeline()` -> // `/context` request for `refreshLiveTimeline()` ->
// `getLatestTimeline()` -> `getEventTimeline()` to construct a new // `getLatestTimeline()` -> `getEventTimeline()` to construct a new
// timeline from. // timeline from.
httpBackend!.when("GET", contextUrl) httpBackend!.when("GET", contextUrl).respond(200, function () {
.respond(200, function() { // The timeline should be cleared at this point in the refresh
// The timeline should be cleared at this point in the refresh expect(room.timeline.length).toEqual(0);
expect(room.timeline.length).toEqual(0);
return contextResponse; return contextResponse;
}); });
// Refresh the timeline again but this time it should pass // Refresh the timeline again but this time it should pass
await Promise.all([ await Promise.all([room.refreshLiveTimeline(), httpBackend!.flushAllExpected()]);
room.refreshLiveTimeline(),
httpBackend!.flushAllExpected(),
]);
// Make sure sync pagination still works by seeing a new message show up // Make sure sync pagination still works by seeing a new message show up
// after refreshing the timeline. // after refreshing the timeline.
const afterRefreshEventData = [ const afterRefreshEventData = [utils.mkMessage({ user: userId, room: roomId })];
utils.mkMessage({ user: userId, room: roomId }),
];
setNextSyncData(afterRefreshEventData); setNextSyncData(afterRefreshEventData);
httpBackend!.when("GET", "/sync").respond(200, function() { httpBackend!.when("GET", "/sync").respond(200, function () {
return NEXT_SYNC_DATA; return NEXT_SYNC_DATA;
}); });
await Promise.all([ await Promise.all([httpBackend!.flushAllExpected(), utils.syncPromise(client!, 1)]);
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 1),
]);
// Make sure the message are visible // Make sure the message are visible
const resultantEventsInTimeline = room.getUnfilteredTimelineSet().getLiveTimeline().getEvents(); const resultantEventsInTimeline = room.getUnfilteredTimelineSet().getLiveTimeline().getEvents();

File diff suppressed because it is too large Load Diff

View File

@ -23,22 +23,23 @@ import { TestClient } from "../TestClient";
import { IEvent } from "../../src"; import { IEvent } from "../../src";
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event"; 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> = { const ENCRYPTED_EVENT: Partial<IEvent> = {
type: 'm.room.encrypted', type: "m.room.encrypted",
content: { content: {
algorithm: 'm.megolm.v1.aes-sha2', algorithm: "m.megolm.v1.aes-sha2",
sender_key: 'SENDER_CURVE25519', sender_key: "SENDER_CURVE25519",
session_id: SESSION_ID, session_id: SESSION_ID,
ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N' ciphertext:
+ 'CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl' "AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N" +
+ 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs', "CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl" +
"mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs",
}, },
room_id: '!ROOM:ID', room_id: "!ROOM:ID",
event_id: '$event1', event_id: "$event1",
origin_server_ts: 1507753886000, origin_server_ts: 1507753886000,
}; };
@ -47,19 +48,20 @@ const CURVE25519_KEY_BACKUP_DATA: IKeyBackupSession = {
forwarded_count: 0, forwarded_count: 0,
is_verified: false, is_verified: false,
session_data: { session_data: {
ciphertext: '2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw' ciphertext:
+ '6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ' "2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw" +
+ 'Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9' "6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ" +
+ 'SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy' "Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9" +
+ 'Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF' "SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy" +
+ 'ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV' "Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF" +
+ '4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv' "ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV" +
+ 'C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe' "4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv" +
+ 'Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf' "C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe" +
+ 'QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy' "Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf" +
+ 'iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg', "QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy" +
mac: '5lxYBHQU80M', "iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg",
ephemeral: '/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14', mac: "5lxYBHQU80M",
ephemeral: "/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14",
}, },
}; };
@ -82,16 +84,14 @@ function createOlmSession(olmAccount: Olm.Account, recipientTestClient: TestClie
const otk = keys[otkId]; const otk = keys[otkId];
const session = new global.Olm.Session(); const session = new global.Olm.Session();
session.create_outbound( session.create_outbound(olmAccount, recipientTestClient.getDeviceKey(), otk.key);
olmAccount, recipientTestClient.getDeviceKey(), otk.key,
);
return session; return session;
}); });
} }
describe("megolm key backups", function() { describe("megolm key backups", function () {
if (!global.Olm) { if (!global.Olm) {
logger.warn('not running megolm tests: Olm not present'); logger.warn("not running megolm tests: Olm not present");
return; return;
} }
const Olm = global.Olm; const Olm = global.Olm;
@ -99,30 +99,28 @@ describe("megolm key backups", function() {
let aliceTestClient: TestClient; let aliceTestClient: TestClient;
const setupTestClient = (): [Account, TestClient] => { const setupTestClient = (): [Account, TestClient] => {
const aliceTestClient = new TestClient( const aliceTestClient = new TestClient("@alice:localhost", "xzcvb", "akjgkrgjs");
"@alice:localhost", "xzcvb", "akjgkrgjs",
);
const testOlmAccount = new Olm.Account(); const testOlmAccount = new Olm.Account();
testOlmAccount!.create(); testOlmAccount!.create();
return [testOlmAccount, aliceTestClient]; return [testOlmAccount, aliceTestClient];
}; };
beforeAll(function() { beforeAll(function () {
return Olm.init(); return Olm.init();
}); });
beforeEach(async function() { beforeEach(async function () {
[testOlmAccount, aliceTestClient] = setupTestClient(); [testOlmAccount, aliceTestClient] = setupTestClient();
await aliceTestClient!.client.initCrypto(); await aliceTestClient!.client.initCrypto();
aliceTestClient!.client.crypto!.backupManager.backupInfo = CURVE25519_BACKUP_INFO; aliceTestClient!.client.crypto!.backupManager.backupInfo = CURVE25519_BACKUP_INFO;
}); });
afterEach(function() { afterEach(function () {
return aliceTestClient!.stop(); 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 = { const syncResponse = {
next_batch: 1, next_batch: 1,
rooms: { rooms: {
@ -136,36 +134,37 @@ describe("megolm key backups", function() {
}, },
}; };
return aliceTestClient!.start().then(() => { return aliceTestClient!
return createOlmSession(testOlmAccount, aliceTestClient); .start()
}).then(() => { .then(() => {
const privkey = decodeRecoveryKey(RECOVERY_KEY); return createOlmSession(testOlmAccount, aliceTestClient);
return aliceTestClient!.client!.crypto!.storeSessionBackupPrivateKey(privkey); })
}).then(() => { .then(() => {
aliceTestClient!.httpBackend.when("GET", "/sync").respond(200, syncResponse); const privkey = decodeRecoveryKey(RECOVERY_KEY);
aliceTestClient!.expectKeyBackupQuery( return aliceTestClient!.client!.crypto!.storeSessionBackupPrivateKey(privkey);
ROOM_ID, })
SESSION_ID, .then(() => {
200, aliceTestClient!.httpBackend.when("GET", "/sync").respond(200, syncResponse);
CURVE25519_KEY_BACKUP_DATA, aliceTestClient!.expectKeyBackupQuery(ROOM_ID, SESSION_ID, 200, CURVE25519_KEY_BACKUP_DATA);
); return aliceTestClient!.httpBackend.flushAllExpected();
return aliceTestClient!.httpBackend.flushAllExpected(); })
}).then(function(): Promise<MatrixEvent> { .then(function (): Promise<MatrixEvent> {
const room = aliceTestClient!.client.getRoom(ROOM_ID)!; const room = aliceTestClient!.client.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0]; const event = room.getLiveTimeline().getEvents()[0];
if (event.getContent()) { if (event.getContent()) {
return Promise.resolve(event); return Promise.resolve(event);
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
event.once(MatrixEventEvent.Decrypted, (ev) => { event.once(MatrixEventEvent.Decrypted, (ev) => {
logger.log(`${Date.now()} event ${event.getId()} now decrypted`); logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
resolve(ev); 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

View File

@ -22,8 +22,20 @@ import { SlidingSync, SlidingSyncEvent, MSC3575RoomData, SlidingSyncState, Exten
import { TestClient } from "../TestClient"; import { TestClient } from "../TestClient";
import { IRoomEvent, IStateEvent } from "../../src/sync-accumulator"; import { IRoomEvent, IStateEvent } from "../../src/sync-accumulator";
import { import {
MatrixClient, MatrixEvent, NotificationCountType, JoinRule, MatrixError, MatrixClient,
EventType, IPushRules, PushRuleKind, TweakName, ClientEvent, RoomMemberEvent, RoomEvent, Room, IRoomTimelineData, MatrixEvent,
NotificationCountType,
JoinRule,
MatrixError,
EventType,
IPushRules,
PushRuleKind,
TweakName,
ClientEvent,
RoomMemberEvent,
RoomEvent,
Room,
IRoomTimelineData,
} from "../../src"; } from "../../src";
import { SlidingSyncSdk } from "../../src/sliding-sync-sdk"; import { SlidingSyncSdk } from "../../src/sliding-sync-sdk";
import { SyncState } from "../../src/sync"; import { SyncState } from "../../src/sync";
@ -67,7 +79,7 @@ describe("SlidingSyncSdk", () => {
event_id: "$" + eventIdCounter, event_id: "$" + eventIdCounter,
}; };
}; };
const mkOwnStateEvent = (evType: string, content: object, stateKey = ''): IStateEvent => { const mkOwnStateEvent = (evType: string, content: object, stateKey = ""): IStateEvent => {
eventIdCounter++; eventIdCounter++;
return { return {
type: evType, type: evType,
@ -97,7 +109,7 @@ describe("SlidingSyncSdk", () => {
}; };
// assign client/httpBackend globals // assign client/httpBackend globals
const setupClient = async (testOpts?: Partial<IStoredClientOpts&{withCrypto: boolean}>) => { const setupClient = async (testOpts?: Partial<IStoredClientOpts & { withCrypto: boolean }>) => {
testOpts = testOpts || {}; testOpts = testOpts || {};
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken); const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
httpBackend = testClient.httpBackend; httpBackend = testClient.httpBackend;
@ -195,7 +207,6 @@ describe("SlidingSyncSdk", () => {
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""), mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello B" }), mkOwnEvent(EventType.RoomMessage, { body: "hello B" }),
mkOwnEvent(EventType.RoomMessage, { body: "world B" }), mkOwnEvent(EventType.RoomMessage, { body: "world B" }),
], ],
initial: true, initial: true,
}, },
@ -294,7 +305,9 @@ describe("SlidingSyncSdk", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]); mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]);
const gotRoom = client!.getRoom(roomA); const gotRoom = client!.getRoom(roomA);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; } if (gotRoom == null) {
return;
}
expect(gotRoom.name).toEqual(data[roomA].name); expect(gotRoom.name).toEqual(data[roomA].name);
expect(gotRoom.getMyMembership()).toEqual("join"); expect(gotRoom.getMyMembership()).toEqual("join");
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline); assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline);
@ -304,7 +317,9 @@ describe("SlidingSyncSdk", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]); mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]);
const gotRoom = client!.getRoom(roomB); const gotRoom = client!.getRoom(roomB);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; } if (gotRoom == null) {
return;
}
expect(gotRoom.name).toEqual(data[roomB].name); expect(gotRoom.name).toEqual(data[roomB].name);
expect(gotRoom.getMyMembership()).toEqual("join"); expect(gotRoom.getMyMembership()).toEqual("join");
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline); assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline);
@ -314,27 +329,33 @@ describe("SlidingSyncSdk", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]); mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]);
const gotRoom = client!.getRoom(roomC); const gotRoom = client!.getRoom(roomC);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; } if (gotRoom == null) {
expect( return;
gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight), }
).toEqual(data[roomC].highlight_count); expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(
data[roomC].highlight_count,
);
}); });
it("can be created with a notification_count", () => { it("can be created with a notification_count", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]); mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]);
const gotRoom = client!.getRoom(roomD); const gotRoom = client!.getRoom(roomD);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; } if (gotRoom == null) {
expect( return;
gotRoom.getUnreadNotificationCount(NotificationCountType.Total), }
).toEqual(data[roomD].notification_count); expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(
data[roomD].notification_count,
);
}); });
it("can be created with an invited/joined_count", () => { it("can be created with an invited/joined_count", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomG, data[roomG]); mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomG, data[roomG]);
const gotRoom = client!.getRoom(roomG); const gotRoom = client!.getRoom(roomG);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; } if (gotRoom == null) {
return;
}
expect(gotRoom.getInvitedMemberCount()).toEqual(data[roomG].invited_count); expect(gotRoom.getInvitedMemberCount()).toEqual(data[roomG].invited_count);
expect(gotRoom.getJoinedMemberCount()).toEqual(data[roomG].joined_count); expect(gotRoom.getJoinedMemberCount()).toEqual(data[roomG].joined_count);
}); });
@ -358,7 +379,9 @@ describe("SlidingSyncSdk", () => {
client!.off(RoomEvent.Timeline, listener); client!.off(RoomEvent.Timeline, listener);
const gotRoom = client!.getRoom(roomH); const gotRoom = client!.getRoom(roomH);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; } if (gotRoom == null) {
return;
}
expect(gotRoom.name).toEqual(data[roomH].name); expect(gotRoom.name).toEqual(data[roomH].name);
expect(gotRoom.getMyMembership()).toEqual("join"); expect(gotRoom.getMyMembership()).toEqual("join");
// check the entire timeline is correct // check the entire timeline is correct
@ -370,7 +393,9 @@ describe("SlidingSyncSdk", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]); mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]);
const gotRoom = client!.getRoom(roomE); const gotRoom = client!.getRoom(roomE);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; } if (gotRoom == null) {
return;
}
expect(gotRoom.getMyMembership()).toEqual("invite"); expect(gotRoom.getMyMembership()).toEqual("invite");
expect(gotRoom.currentState.getJoinRule()).toEqual(JoinRule.Invite); expect(gotRoom.currentState.getJoinRule()).toEqual(JoinRule.Invite);
}); });
@ -379,10 +404,10 @@ describe("SlidingSyncSdk", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]); mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]);
const gotRoom = client!.getRoom(roomF); const gotRoom = client!.getRoom(roomF);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; } if (gotRoom == null) {
expect( return;
gotRoom.name, }
).toEqual(data[roomF].name); expect(gotRoom.name).toEqual(data[roomF].name);
}); });
describe("updating", () => { describe("updating", () => {
@ -395,7 +420,9 @@ describe("SlidingSyncSdk", () => {
}); });
const gotRoom = client!.getRoom(roomA); const gotRoom = client!.getRoom(roomA);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; } if (gotRoom == null) {
return;
}
const newTimeline = data[roomA].timeline; const newTimeline = data[roomA].timeline;
newTimeline.push(newEvent); newTimeline.push(newEvent);
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-3), newTimeline); assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-3), newTimeline);
@ -404,18 +431,20 @@ describe("SlidingSyncSdk", () => {
it("can update with a new required_state event", async () => { it("can update with a new required_state event", async () => {
let gotRoom = client!.getRoom(roomB); let gotRoom = client!.getRoom(roomB);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; } if (gotRoom == null) {
return;
}
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Invite); // default expect(gotRoom.getJoinRule()).toEqual(JoinRule.Invite); // default
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, { mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, {
required_state: [ required_state: [mkOwnStateEvent("m.room.join_rules", { join_rule: "restricted" }, "")],
mkOwnStateEvent("m.room.join_rules", { join_rule: "restricted" }, ""),
],
timeline: [], timeline: [],
name: data[roomB].name, name: data[roomB].name,
}); });
gotRoom = client!.getRoom(roomB); gotRoom = client!.getRoom(roomB);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; } if (gotRoom == null) {
return;
}
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Restricted); expect(gotRoom.getJoinRule()).toEqual(JoinRule.Restricted);
}); });
@ -428,10 +457,10 @@ describe("SlidingSyncSdk", () => {
}); });
const gotRoom = client!.getRoom(roomC); const gotRoom = client!.getRoom(roomC);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; } if (gotRoom == null) {
expect( return;
gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight), }
).toEqual(1); expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(1);
}); });
it("can update with a new notification_count", async () => { it("can update with a new notification_count", async () => {
@ -443,10 +472,10 @@ describe("SlidingSyncSdk", () => {
}); });
const gotRoom = client!.getRoom(roomD); const gotRoom = client!.getRoom(roomD);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; } if (gotRoom == null) {
expect( return;
gotRoom.getUnreadNotificationCount(NotificationCountType.Total), }
).toEqual(1); expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(1);
}); });
it("can update with a new joined_count", () => { it("can update with a new joined_count", () => {
@ -458,7 +487,9 @@ describe("SlidingSyncSdk", () => {
}); });
const gotRoom = client!.getRoom(roomG); const gotRoom = client!.getRoom(roomG);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; } if (gotRoom == null) {
return;
}
expect(gotRoom.getJoinedMemberCount()).toEqual(1); expect(gotRoom.getJoinedMemberCount()).toEqual(1);
}); });
@ -482,11 +513,20 @@ describe("SlidingSyncSdk", () => {
}); });
const gotRoom = client!.getRoom(roomA); const gotRoom = client!.getRoom(roomA);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; } if (gotRoom == null) {
return;
}
logger.log("want:", oldTimeline.map((e) => (e.type + " : " + (e.content || {}).body))); logger.log(
logger.log("got:", gotRoom.getLiveTimeline().getEvents().map( "want:",
(e) => (e.getType() + " : " + e.getContent().body)), 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) // we expect the timeline now to be oldTimeline (so the old events are in fact old)
@ -506,40 +546,54 @@ describe("SlidingSyncSdk", () => {
const FAILED_SYNC_ERROR_THRESHOLD = 3; // would be nice to export the const in the actual class... 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 () => { it("emits SyncState.Reconnecting when < FAILED_SYNC_ERROR_THRESHOLD & SyncState.Error when over", async () => {
mockSlidingSync!.emit( mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, {
SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, pos: "h",
{ pos: "h", lists: [], rooms: {}, extensions: {} }, lists: [],
); rooms: {},
extensions: {},
});
expect(sdk!.getSyncState()).toEqual(SyncState.Syncing); expect(sdk!.getSyncState()).toEqual(SyncState.Syncing);
mockSlidingSync!.emit( mockSlidingSync!.emit(
SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new Error("generic"), SlidingSyncEvent.Lifecycle,
SlidingSyncState.RequestFinished,
null,
new Error("generic"),
); );
expect(sdk!.getSyncState()).toEqual(SyncState.Reconnecting); expect(sdk!.getSyncState()).toEqual(SyncState.Reconnecting);
for (let i = 0; i < FAILED_SYNC_ERROR_THRESHOLD; i++) { for (let i = 0; i < FAILED_SYNC_ERROR_THRESHOLD; i++) {
mockSlidingSync!.emit( mockSlidingSync!.emit(
SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new Error("generic"), SlidingSyncEvent.Lifecycle,
SlidingSyncState.RequestFinished,
null,
new Error("generic"),
); );
} }
expect(sdk!.getSyncState()).toEqual(SyncState.Error); expect(sdk!.getSyncState()).toEqual(SyncState.Error);
}); });
it("emits SyncState.Syncing after a previous SyncState.Error", async () => { it("emits SyncState.Syncing after a previous SyncState.Error", async () => {
mockSlidingSync!.emit( mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, {
SlidingSyncEvent.Lifecycle, pos: "i",
SlidingSyncState.Complete, lists: [],
{ pos: "i", lists: [], rooms: {}, extensions: {} }, rooms: {},
); extensions: {},
});
expect(sdk!.getSyncState()).toEqual(SyncState.Syncing); expect(sdk!.getSyncState()).toEqual(SyncState.Syncing);
}); });
it("emits SyncState.Error immediately when receiving M_UNKNOWN_TOKEN and stops syncing", async () => { it("emits SyncState.Error immediately when receiving M_UNKNOWN_TOKEN and stops syncing", async () => {
expect(mockSlidingSync!.stop).not.toBeCalled(); expect(mockSlidingSync!.stop).not.toBeCalled();
mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new MatrixError({ mockSlidingSync!.emit(
errcode: "M_UNKNOWN_TOKEN", SlidingSyncEvent.Lifecycle,
message: "Oh no your access token is no longer valid", 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(sdk!.getSyncState()).toEqual(SyncState.Error);
expect(mockSlidingSync!.stop).toBeCalled(); expect(mockSlidingSync!.stop).toBeCalled();
}); });
@ -694,7 +748,6 @@ describe("SlidingSyncSdk", () => {
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId), mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""), mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello" }), mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
], ],
initial: true, initial: true,
}); });
@ -743,18 +796,20 @@ describe("SlidingSyncSdk", () => {
const roomId = "!foo:bar"; const roomId = "!foo:bar";
const pushRulesContent: IPushRules = { const pushRulesContent: IPushRules = {
global: { global: {
[PushRuleKind.RoomSpecific]: [{ [PushRuleKind.RoomSpecific]: [
enabled: true, {
default: true, enabled: true,
pattern: "monkey", default: true,
actions: [ pattern: "monkey",
{ actions: [
set_tweak: TweakName.Sound, {
value: "default", set_tweak: TweakName.Sound,
}, value: "default",
], },
rule_id: roomId, ],
}], rule_id: roomId,
},
],
}, },
}; };
let pushRule = client!.getRoomPushRule("global", roomId); let pushRule = client!.getRoomPushRule("global", roomId);
@ -973,7 +1028,11 @@ describe("SlidingSyncSdk", () => {
let ext: Extension<any, any>; let ext: Extension<any, any>;
const generateReceiptResponse = ( const generateReceiptResponse = (
userId: string, roomId: string, eventId: string, recType: string, ts: number, userId: string,
roomId: string,
eventId: string,
recType: string,
ts: number,
) => { ) => {
return { return {
rooms: { rooms: {
@ -1034,9 +1093,7 @@ describe("SlidingSyncSdk", () => {
const room = client!.getRoom(roomId)!; const room = client!.getRoom(roomId)!;
expect(room).toBeDefined(); expect(room).toBeDefined();
expect(room.getReadReceiptForUserId(alice, true)).toBeNull(); expect(room.getReadReceiptForUserId(alice, true)).toBeNull();
ext.onResponse( ext.onResponse(generateReceiptResponse(alice, roomId, lastEvent.event_id, "m.read", 1234567));
generateReceiptResponse(alice, roomId, lastEvent.event_id, "m.read", 1234567),
);
const receipt = room.getReadReceiptForUserId(alice); const receipt = room.getReadReceiptForUserId(alice);
expect(receipt).toBeDefined(); expect(receipt).toBeDefined();
expect(receipt?.eventId).toEqual(lastEvent.event_id); expect(receipt?.eventId).toEqual(lastEvent.event_id);
@ -1048,9 +1105,7 @@ describe("SlidingSyncSdk", () => {
const roomId = "!room:id"; const roomId = "!room:id";
const alice = "@alice:alice"; const alice = "@alice:alice";
const eventId = "$something"; const eventId = "$something";
ext.onResponse( ext.onResponse(generateReceiptResponse(alice, roomId, eventId, "m.read", 1234567));
generateReceiptResponse(alice, roomId, eventId, "m.read", 1234567),
);
// we expect it not to crash // we expect it not to crash
}); });
}); });

File diff suppressed because it is too large Load Diff

View File

@ -15,13 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { logger } from '../src/logger'; import { logger } from "../src/logger";
// try to load the olm library. // try to load the olm library.
try { try {
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
global.Olm = require('@matrix-org/olm'); global.Olm = require("@matrix-org/olm");
logger.log('loaded libolm'); logger.log("loaded libolm");
} catch (e) { } catch (e) {
logger.warn("unable to run crypto tests: libolm not available"); logger.warn("unable to run crypto tests: libolm not available");
} }

View File

@ -38,18 +38,18 @@ class JestSlowTestReporter {
if (isTestSuite) { if (isTestSuite) {
console.log( console.log(
`Top ${slowestTests.length} slowest test suites (${slowTestTime / 1000} seconds,` + `Top ${slowestTests.length} slowest test suites (${slowTestTime / 1000} seconds,` +
` ${percentTime.toFixed(1)}% of total time):`, ` ${percentTime.toFixed(1)}% of total time):`,
); );
} else { } else {
console.log( console.log(
`Top ${slowestTests.length} slowest tests (${slowTestTime / 1000} seconds,` + `Top ${slowestTests.length} slowest tests (${slowTestTime / 1000} seconds,` +
` ${percentTime.toFixed(1)}% of total time):`, ` ${percentTime.toFixed(1)}% of total time):`,
); );
} }
for (let i = 0; i < slowestTests.length; i++) { for (let i = 0; i < slowestTests.length; i++) {
const duration = slowestTests[i].duration; const duration = slowestTests[i].duration;
const filePath = slowestTests[i].filePath.replace(rootPathRegex, '.'); const filePath = slowestTests[i].filePath.replace(rootPathRegex, ".");
if (isTestSuite) { if (isTestSuite) {
console.log(` ${duration / 1000} seconds ${filePath}`); console.log(` ${duration / 1000} seconds ${filePath}`);

View File

@ -17,10 +17,7 @@ limitations under the License.
import { MatrixEvent } from "../../src"; import { MatrixEvent } from "../../src";
import { M_BEACON, M_BEACON_INFO } from "../../src/@types/beacon"; import { M_BEACON, M_BEACON_INFO } from "../../src/@types/beacon";
import { LocationAssetType } from "../../src/@types/location"; import { LocationAssetType } from "../../src/@types/location";
import { import { makeBeaconContent, makeBeaconInfoContent } from "../../src/content-helpers";
makeBeaconContent,
makeBeaconInfoContent,
} from "../../src/content-helpers";
type InfoContentProps = { type InfoContentProps = {
timeout: number; timeout: number;
@ -44,13 +41,7 @@ export const makeBeaconInfoEvent = (
contentProps: Partial<InfoContentProps> = {}, contentProps: Partial<InfoContentProps> = {},
eventId?: string, eventId?: string,
): MatrixEvent => { ): MatrixEvent => {
const { const { timeout, isLive, description, assetType, timestamp } = {
timeout,
isLive,
description,
assetType,
timestamp,
} = {
...DEFAULT_INFO_CONTENT_PROPS, ...DEFAULT_INFO_CONTENT_PROPS,
...contentProps, ...contentProps,
}; };
@ -77,9 +68,9 @@ type ContentProps = {
description?: string; description?: string;
}; };
const DEFAULT_CONTENT_PROPS: ContentProps = { const DEFAULT_CONTENT_PROPS: ContentProps = {
uri: 'geo:-36.24484561954707,175.46884959563613;u=10', uri: "geo:-36.24484561954707,175.46884959563613;u=10",
timestamp: 123, timestamp: 123,
beaconInfoId: '$123', beaconInfoId: "$123",
}; };
/** /**
@ -87,10 +78,7 @@ const DEFAULT_CONTENT_PROPS: ContentProps = {
* all required properties are mocked * all required properties are mocked
* override with contentProps * override with contentProps
*/ */
export const makeBeaconEvent = ( export const makeBeaconEvent = (sender: string, contentProps: Partial<ContentProps> = {}): MatrixEvent => {
sender: string,
contentProps: Partial<ContentProps> = {},
): MatrixEvent => {
const { uri, timestamp, beaconInfoId, description } = { const { uri, timestamp, beaconInfoId, description } = {
...DEFAULT_CONTENT_PROPS, ...DEFAULT_CONTENT_PROPS,
...contentProps, ...contentProps,
@ -107,10 +95,13 @@ export const makeBeaconEvent = (
* Create a mock geolocation position * Create a mock geolocation position
* defaults all required properties * defaults all required properties
*/ */
export const makeGeolocationPosition = ( export const makeGeolocationPosition = ({
{ timestamp, coords }: timestamp,
{ timestamp?: number, coords: Partial<GeolocationCoordinates> }, coords,
): GeolocationPosition => ({ }: {
timestamp?: number;
coords: Partial<GeolocationCoordinates>;
}): GeolocationPosition => ({
timestamp: timestamp ?? 1647256791840, timestamp: timestamp ?? 1647256791840,
coords: { coords: {
accuracy: 1, accuracy: 1,

View File

@ -58,11 +58,11 @@ export const getMockClientWithEventEmitter = (
}); });
* ``` * ```
*/ */
export const mockClientMethodsUser = (userId = '@alice:domain') => ({ export const mockClientMethodsUser = (userId = "@alice:domain") => ({
getUserId: jest.fn().mockReturnValue(userId), getUserId: jest.fn().mockReturnValue(userId),
getUser: jest.fn().mockReturnValue(new User(userId)), getUser: jest.fn().mockReturnValue(new User(userId)),
isGuest: jest.fn().mockReturnValue(false), isGuest: jest.fn().mockReturnValue(false),
mxcUrlToHttp: jest.fn().mockReturnValue('mock-mxcUrlToHttp'), mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
credentials: { userId }, credentials: { userId },
getThreePids: jest.fn().mockResolvedValue({ threepids: [] }), getThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
getAccessToken: jest.fn(), getAccessToken: jest.fn(),
@ -91,4 +91,3 @@ export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixC
getCapabilities: jest.fn().mockReturnValue({}), getCapabilities: jest.fn().mockReturnValue({}),
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false), doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
}); });

View File

@ -22,7 +22,7 @@ limitations under the License.
// and avoids assuming anything about the app's behaviour. // and avoids assuming anything about the app's behaviour.
const realSetTimeout = setTimeout; const realSetTimeout = setTimeout;
export function flushPromises() { export function flushPromises() {
return new Promise(r => { return new Promise((r) => {
realSetTimeout(r, 1); realSetTimeout(r, 1);
}); });
} }

View File

@ -2,9 +2,9 @@
import EventEmitter from "events"; import EventEmitter from "events";
// load olm before the sdk if possible // 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 { IContent, IEvent, IEventRelation, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { ClientEvent, EventType, IPusher, MatrixClient, MsgType } from "../../src"; import { ClientEvent, EventType, IPusher, MatrixClient, MsgType } from "../../src";
import { SyncState } from "../../src/sync"; import { SyncState } from "../../src/sync";
@ -45,16 +45,17 @@ export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
* @param name - The name of the class * @param name - The name of the class
* @returns An instantiated object with spied methods/properties. * @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/ // Based on http://eclipsesource.com/blogs/2014/03/27/mocks-in-jasmine-tests/
const HelperConstr = new Function(); // jshint ignore:line const HelperConstr = new Function(); // jshint ignore:line
HelperConstr.prototype = constr.prototype; HelperConstr.prototype = constr.prototype;
// @ts-ignore // @ts-ignore
const result = new HelperConstr(); const result = new HelperConstr();
result.toString = function() { result.toString = function () {
return "mock" + (name ? " of " + name : ""); 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 { try {
if (constr.prototype[key] instanceof Function) { if (constr.prototype[key] instanceof Function) {
result[key] = jest.fn(); result[key] = jest.fn();
@ -114,15 +115,17 @@ export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixC
}; };
if (opts.skey !== undefined) { if (opts.skey !== undefined) {
event.state_key = opts.skey; event.state_key = opts.skey;
} else if ([ } else if (
EventType.RoomName, [
EventType.RoomTopic, EventType.RoomName,
EventType.RoomCreate, EventType.RoomTopic,
EventType.RoomJoinRules, EventType.RoomCreate,
EventType.RoomPowerLevels, EventType.RoomJoinRules,
EventType.RoomTopic, EventType.RoomPowerLevels,
"com.example.state", EventType.RoomTopic,
].includes(opts.type)) { "com.example.state",
].includes(opts.type)
) {
event.state_key = ""; event.state_key = "";
} }
@ -228,8 +231,8 @@ export function mkMembership(opts: IMembershipOpts & { event?: boolean }): Parti
} }
export function mkMembershipCustom<T>( export function mkMembershipCustom<T>(
base: T & { membership: string, sender: string, content?: IContent }, base: T & { membership: string; sender: string; content?: IContent },
): T & { type: EventType, sender: string, state_key: string, content: IContent } & GeneratedMetadata { ): T & { type: EventType; sender: string; state_key: string; content: IContent } & GeneratedMetadata {
const content = base.content || {}; const content = base.content || {};
return mkEventCustom({ return mkEventCustom({
...base, ...base,
@ -315,7 +318,7 @@ export function mkReplyMessage(
"rel_type": "m.in_reply_to", "rel_type": "m.in_reply_to",
"event_id": opts.replyToMessage.getId(), "event_id": opts.replyToMessage.getId(),
"m.in_reply_to": { "m.in_reply_to": {
"event_id": opts.replyToMessage.getId()!, event_id: opts.replyToMessage.getId()!,
}, },
}, },
}, },
@ -364,7 +367,8 @@ export class MockStorageApi implements Storage {
* @returns 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( export async function awaitDecryption(
event: MatrixEvent, { waitOnDecryptionFailure = false } = {}, event: MatrixEvent,
{ waitOnDecryptionFailure = false } = {},
): Promise<MatrixEvent> { ): Promise<MatrixEvent> {
// An event is not always decrypted ahead of time // An event is not always decrypted ahead of time
// getClearContent is a good signal to know whether an event has been decrypted // getClearContent is a good signal to know whether an event has been decrypted
@ -387,7 +391,7 @@ export async function awaitDecryption(
}); });
} }
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 => ({ export const mkPusher = (extra: Partial<IPusher> = {}): IPusher => ({
app_display_name: "app", app_display_name: "app",

View File

@ -21,18 +21,25 @@ import { Room } from "../../src/models/room";
import { Thread } from "../../src/models/thread"; import { Thread } from "../../src/models/thread";
import { mkMessage } from "./test-utils"; import { mkMessage } from "./test-utils";
export const makeThreadEvent = ({ rootEventId, replyToEventId, ...props }: any & { export const makeThreadEvent = ({
rootEventId: string; replyToEventId: string; event?: boolean; rootEventId,
}): MatrixEvent => mkMessage({ replyToEventId,
...props, ...props
relatesTo: { }: any & {
event_id: rootEventId, rootEventId: string;
rel_type: "m.thread", replyToEventId: string;
['m.in_reply_to']: { event?: boolean;
event_id: replyToEventId, }): MatrixEvent =>
mkMessage({
...props,
relatesTo: {
event_id: rootEventId,
rel_type: "m.thread",
["m.in_reply_to"]: {
event_id: replyToEventId,
},
}, },
}, });
});
type MakeThreadEventsProps = { type MakeThreadEventsProps = {
roomId: Room["roomId"]; roomId: Room["roomId"];
@ -50,12 +57,17 @@ type MakeThreadEventsProps = {
}; };
export const makeThreadEvents = ({ export const makeThreadEvents = ({
roomId, authorId, participantUserIds, length = 2, ts = 1, currentUserId, roomId,
}: MakeThreadEventsProps): { rootEvent: MatrixEvent, events: MatrixEvent[] } => { authorId,
participantUserIds,
length = 2,
ts = 1,
currentUserId,
}: MakeThreadEventsProps): { rootEvent: MatrixEvent; events: MatrixEvent[] } => {
const rootEvent = mkMessage({ const rootEvent = mkMessage({
user: authorId, user: authorId,
room: roomId, room: roomId,
msg: 'root event message ' + Math.random(), msg: "root event message " + Math.random(),
ts, ts,
event: true, event: true,
}); });
@ -67,16 +79,18 @@ export const makeThreadEvents = ({
const prevEvent = events[i - 1]; const prevEvent = events[i - 1];
const replyToEventId = prevEvent.getId(); const replyToEventId = prevEvent.getId();
const user = participantUserIds[i % participantUserIds.length]; const user = participantUserIds[i % participantUserIds.length];
events.push(makeThreadEvent({ events.push(
user, makeThreadEvent({
room: roomId, user,
event: true, room: roomId,
msg: `reply ${i} by ${user}`, event: true,
rootEventId, msg: `reply ${i} by ${user}`,
replyToEventId, rootEventId,
// replies are 1ms after each other replyToEventId,
ts: ts + i, // replies are 1ms after each other
})); ts: ts + i,
}),
);
} }
rootEvent.setUnsigned({ rootEvent.setUnsigned({
@ -108,7 +122,7 @@ export const mkThread = ({
participantUserIds, participantUserIds,
length = 2, length = 2,
ts = 1, ts = 1,
}: MakeThreadProps): { thread: Thread, rootEvent: MatrixEvent, events: MatrixEvent[] } => { }: MakeThreadProps): { thread: Thread; rootEvent: MatrixEvent; events: MatrixEvent[] } => {
const { rootEvent, events } = makeThreadEvents({ const { rootEvent, events } = makeThreadEvents({
roomId: room.roomId, roomId: room.roomId,
authorId, authorId,
@ -120,9 +134,7 @@ export const mkThread = ({
expect(rootEvent).toBeTruthy(); expect(rootEvent).toBeTruthy();
for (const evt of events) { for (const evt of events) {
room?.reEmitter.reEmit(evt, [ room?.reEmitter.reEmit(evt, [MatrixEventEvent.BeforeRedaction]);
MatrixEventEvent.BeforeRedaction,
]);
} }
const thread = room.createThread(rootEvent.getId() ?? "", rootEvent, events, true); const thread = room.createThread(rootEvent.getId() ?? "", rootEvent, events, true);

View File

@ -41,7 +41,7 @@ import { GroupCallEventHandlerMap } from "../../src/webrtc/groupCall";
import { GroupCallEventHandlerEvent } from "../../src/webrtc/groupCallEventHandler"; import { GroupCallEventHandlerEvent } from "../../src/webrtc/groupCallEventHandler";
import { IScreensharingOpts, MediaHandler } from "../../src/webrtc/mediaHandler"; import { IScreensharingOpts, MediaHandler } from "../../src/webrtc/mediaHandler";
export const DUMMY_SDP = ( export const DUMMY_SDP =
"v=0\r\n" + "v=0\r\n" +
"o=- 5022425983810148698 2 IN IP4 127.0.0.1\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" + "s=-\r\nt=0 0\r\na=group:BUNDLE 0\r\n" +
@ -78,8 +78,7 @@ export const DUMMY_SDP = (
"a=rtpmap:112 telephone-event/32000\r\n" + "a=rtpmap:112 telephone-event/32000\r\n" +
"a=rtpmap:113 telephone-event/16000\r\n" + "a=rtpmap:113 telephone-event/16000\r\n" +
"a=rtpmap:126 telephone-event/8000\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 USERMEDIA_STREAM_ID = "mock_stream_from_media_handler";
export const SCREENSHARE_STREAM_ID = "mock_screen_stream_from_media_handler"; export const SCREENSHARE_STREAM_ID = "mock_screen_stream_from_media_handler";
@ -100,13 +99,19 @@ class MockMediaStreamAudioSourceNode {
} }
class MockAnalyser { class MockAnalyser {
public getFloatFrequencyData() { return 0.0; } public getFloatFrequencyData() {
return 0.0;
}
} }
export class MockAudioContext { export class MockAudioContext {
constructor() {} constructor() {}
public createAnalyser() { return new MockAnalyser(); } public createAnalyser() {
public createMediaStreamSource() { return new MockMediaStreamAudioSourceNode(); } return new MockAnalyser();
}
public createMediaStreamSource() {
return new MockMediaStreamAudioSourceNode();
}
public close() {} public close() {}
} }
@ -132,7 +137,7 @@ export class MockRTCPeerConnection {
} }
public static hasAnyPendingNegotiations(): boolean { public static hasAnyPendingNegotiations(): boolean {
return this.instances.some(i => i.needsNegotiation); return this.instances.some((i) => i.needsNegotiation);
} }
public static resetInstances() { public static resetInstances() {
@ -142,11 +147,11 @@ export class MockRTCPeerConnection {
constructor() { constructor() {
this.localDescription = { this.localDescription = {
sdp: DUMMY_SDP, sdp: DUMMY_SDP,
type: 'offer', type: "offer",
toJSON: function() { }, toJSON: function () {},
}; };
this.readyToNegotiate = new Promise<void>(resolve => { this.readyToNegotiate = new Promise<void>((resolve) => {
this.onReadyToNegotiate = resolve; this.onReadyToNegotiate = resolve;
}); });
@ -154,26 +159,28 @@ export class MockRTCPeerConnection {
} }
public addEventListener(type: string, listener: () => void) { public addEventListener(type: string, listener: () => void) {
if (type === 'negotiationneeded') { if (type === "negotiationneeded") {
this.negotiationNeededListener = listener; this.negotiationNeededListener = listener;
} else if (type == 'icecandidate') { } else if (type == "icecandidate") {
this.iceCandidateListener = listener; this.iceCandidateListener = listener;
} else if (type === 'iceconnectionstatechange') { } else if (type === "iceconnectionstatechange") {
this.iceConnectionStateChangeListener = listener; this.iceConnectionStateChangeListener = listener;
} else if (type == 'track') { } else if (type == "track") {
this.onTrackListener = listener; this.onTrackListener = listener;
} }
} }
public createDataChannel(label: string, opts: RTCDataChannelInit) { return { label, ...opts }; } public createDataChannel(label: string, opts: RTCDataChannelInit) {
return { label, ...opts };
}
public createOffer() { public createOffer() {
return Promise.resolve({ return Promise.resolve({
type: 'offer', type: "offer",
sdp: DUMMY_SDP, sdp: DUMMY_SDP,
}); });
} }
public createAnswer() { public createAnswer() {
return Promise.resolve({ return Promise.resolve({
type: 'answer', type: "answer",
sdp: DUMMY_SDP, sdp: DUMMY_SDP,
}); });
} }
@ -183,8 +190,10 @@ export class MockRTCPeerConnection {
public setLocalDescription() { public setLocalDescription() {
return Promise.resolve(); return Promise.resolve();
} }
public close() { } public close() {}
public getStats() { return []; } public getStats() {
return [];
}
public addTransceiver(track: MockMediaStreamTrack): MockRTCRtpTransceiver { public addTransceiver(track: MockMediaStreamTrack): MockRTCRtpTransceiver {
this.needsNegotiation = true; this.needsNegotiation = true;
if (this.onReadyToNegotiate) this.onReadyToNegotiate(); if (this.onReadyToNegotiate) this.onReadyToNegotiate();
@ -209,9 +218,11 @@ export class MockRTCPeerConnection {
if (this.onReadyToNegotiate) this.onReadyToNegotiate(); if (this.onReadyToNegotiate) this.onReadyToNegotiate();
} }
public getTransceivers(): MockRTCRtpTransceiver[] { return this.transceivers; } public getTransceivers(): MockRTCRtpTransceiver[] {
return this.transceivers;
}
public getSenders(): MockRTCRtpSender[] { 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() { public doNegotiation() {
@ -223,13 +234,15 @@ export class MockRTCPeerConnection {
} }
export class MockRTCRtpSender { 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 { export class MockRTCRtpReceiver {
constructor(public track: MockMediaStreamTrack) { } constructor(public track: MockMediaStreamTrack) {}
} }
export class MockRTCRtpTransceiver { export class MockRTCRtpTransceiver {
@ -246,7 +259,7 @@ export class MockRTCRtpTransceiver {
} }
export class MockMediaStreamTrack { 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, []>(); public stop = jest.fn<void, []>();
@ -254,7 +267,9 @@ export class MockMediaStreamTrack {
public isStopped = false; public isStopped = false;
public settings?: MediaTrackSettings; 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 // XXX: Using EventTarget in jest doesn't seem to work, so we write our own
// implementation // implementation
@ -273,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 // XXX: Using EventTarget in jest doesn't seem to work, so we write our own
// implementation // implementation
export class MockMediaStream { export class MockMediaStream {
constructor( constructor(public id: string, private tracks: MockMediaStreamTrack[] = []) {}
public id: string,
private tracks: MockMediaStreamTrack[] = [],
) {}
public listeners: [string, (...args: any[]) => any][] = []; public listeners: [string, (...args: any[]) => any][] = [];
public isStopped = false; public isStopped = false;
@ -293,9 +307,15 @@ export class MockMediaStream {
c(); c();
}); });
} }
public getTracks() { return this.tracks; } public getTracks() {
public getAudioTracks() { return this.tracks.filter((track) => track.kind === "audio"); } return this.tracks;
public getVideoTracks() { return this.tracks.filter((track) => track.kind === "video"); } }
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) { public addEventListener(eventType: string, callback: (...args: any[]) => any) {
this.listeners.push([eventType, callback]); this.listeners.push([eventType, callback]);
} }
@ -308,7 +328,9 @@ export class MockMediaStream {
this.tracks.push(track); this.tracks.push(track);
this.dispatchEvent("addtrack"); 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 { public clone(): MediaStream {
return new MockMediaStream(this.id + ".clone", this.tracks).typed(); return new MockMediaStream(this.id + ".clone", this.tracks).typed();
@ -325,11 +347,11 @@ export class MockMediaStream {
} }
export class MockMediaDeviceInfo { export class MockMediaDeviceInfo {
constructor( constructor(public kind: "audioinput" | "videoinput" | "audiooutput") {}
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 { export class MockMediaHandler {
@ -359,28 +381,38 @@ export class MockMediaHandler {
public stopScreensharingStream(stream: MockMediaStream) { public stopScreensharingStream(stream: MockMediaStream) {
stream.isStopped = true; stream.isStopped = true;
} }
public hasAudioDevice() { return true; } public hasAudioDevice() {
public hasVideoDevice() { return true; } return true;
}
public hasVideoDevice() {
return true;
}
public stopAllStreams() {} public stopAllStreams() {}
public typed(): MediaHandler { return this as unknown as MediaHandler; } public typed(): MediaHandler {
return this as unknown as MediaHandler;
}
} }
export class MockMediaDevices { export class MockMediaDevices {
public enumerateDevices = jest.fn<Promise<MediaDeviceInfo[]>, []>().mockResolvedValue([ public enumerateDevices = jest
new MockMediaDeviceInfo("audioinput").typed(), .fn<Promise<MediaDeviceInfo[]>, []>()
new MockMediaDeviceInfo("videoinput").typed(), .mockResolvedValue([
]); new MockMediaDeviceInfo("audioinput").typed(),
new MockMediaDeviceInfo("videoinput").typed(),
]);
public getUserMedia = jest.fn<Promise<MediaStream>, [MediaStreamConstraints]>().mockReturnValue( public getUserMedia = jest
Promise.resolve(new MockMediaStream("local_stream").typed()), .fn<Promise<MediaStream>, [MediaStreamConstraints]>()
); .mockReturnValue(Promise.resolve(new MockMediaStream("local_stream").typed()));
public getDisplayMedia = jest.fn<Promise<MediaStream>, [MediaStreamConstraints]>().mockReturnValue( public getDisplayMedia = jest
Promise.resolve(new MockMediaStream("local_display_stream").typed()), .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; type EmittedEvents = CallEventHandlerEvent | CallEvent | ClientEvent | RoomStateEvent | GroupCallEventHandlerEvent;
@ -405,21 +437,33 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
calls: new Map<string, MatrixCall>(), calls: new Map<string, MatrixCall>(),
}; };
public sendStateEvent = jest.fn<Promise<ISendEventResponse>, [ public sendStateEvent = jest.fn<
roomId: string, eventType: EventType, content: any, statekey: string, Promise<ISendEventResponse>,
]>(); [roomId: string, eventType: EventType, content: any, statekey: string]
public sendToDevice = jest.fn<Promise<{}>, [ >();
eventType: string, public sendToDevice = jest.fn<
contentMap: { [userId: string]: { [deviceId: string]: Record<string, any> } }, Promise<{}>,
txnId?: string, [
]>(); 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 getDeviceId(): string {
public getSessionId(): string { return this.sessionId; } return this.deviceId;
}
public getSessionId(): string {
return this.sessionId;
}
public getTurnServers = () => []; public getTurnServers = () => [];
public isFallbackICEServerAllowed = () => false; public isFallbackICEServerAllowed = () => false;
@ -432,18 +476,17 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
public getRooms = jest.fn<Room[], []>().mockReturnValue([]); public getRooms = jest.fn<Room[], []>().mockReturnValue([]);
public getRoom = jest.fn(); public getRoom = jest.fn();
public supportsExperimentalThreads(): boolean { return true; } public supportsExperimentalThreads(): boolean {
return true;
}
public async decryptEventIfNeeded(): Promise<void> {} public async decryptEventIfNeeded(): Promise<void> {}
public typed(): MatrixClient { return this as unknown as MatrixClient; } public typed(): MatrixClient {
return this as unknown as MatrixClient;
}
public emitRoomState(event: MatrixEvent, state: RoomState): void { public emitRoomState(event: MatrixEvent, state: RoomState): void {
this.emit( this.emit(RoomStateEvent.Events, event, state, null);
RoomStateEvent.Events,
event,
state,
null,
);
} }
} }
@ -481,15 +524,13 @@ export class MockMatrixCall extends TypedEventEmitter<CallEvent, CallEventHandle
return this.opponentDeviceId; return this.opponentDeviceId;
} }
public typed(): MatrixCall { return this as unknown as MatrixCall; } public typed(): MatrixCall {
return this as unknown as MatrixCall;
}
} }
export class MockCallFeed { export class MockCallFeed {
constructor( constructor(public userId: string, public deviceId: string | undefined, public stream: MockMediaStream) {}
public userId: string,
public deviceId: string | undefined,
public stream: MockMediaStream,
) {}
public measureVolumeActivity(val: boolean) {} public measureVolumeActivity(val: boolean) {}
public dispose() {} public dispose() {}
@ -536,10 +577,14 @@ export function installWebRTCMocks() {
}; };
} }
export function makeMockGroupCallStateEvent(roomId: string, groupCallId: string, content: IContent = { export function makeMockGroupCallStateEvent(
"m.type": GroupCallType.Video, roomId: string,
"m.intent": GroupCallIntent.Prompt, groupCallId: string,
}): MatrixEvent { content: IContent = {
"m.type": GroupCallType.Video,
"m.intent": GroupCallIntent.Prompt,
},
): MatrixEvent {
return { return {
getType: jest.fn().mockReturnValue(EventType.GroupCallPrefix), getType: jest.fn().mockReturnValue(EventType.GroupCallPrefix),
getRoomId: jest.fn().mockReturnValue(roomId), getRoomId: jest.fn().mockReturnValue(roomId),

View File

@ -27,16 +27,14 @@ class EventSource extends EventEmitter {
} }
doAnError() { 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 src = new EventSource();
const tgt = new EventTarget(); const tgt = new EventTarget();
@ -53,18 +51,18 @@ describe("ReEmitter", function() {
expect(handler).toHaveBeenCalledWith("foo", "bar", src); 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 src = new EventSource();
const tgt = new EventTarget(); const tgt = new EventTarget();
const reEmitter = new ReEmitter(tgt); const reEmitter = new ReEmitter(tgt);
reEmitter.reEmit(src, ['error']); reEmitter.reEmit(src, ["error"]);
// without the workaround in ReEmitter, this would throw // without the workaround in ReEmitter, this would throw
src.doAnError(); src.doAnError();
const handler = jest.fn(); const handler = jest.fn();
tgt.on('error', handler); tgt.on("error", handler);
src.doAnError(); src.doAnError();

View File

@ -19,44 +19,56 @@ import MockHttpBackend from "matrix-mock-request";
import { AutoDiscovery } from "../../src/autodiscovery"; import { AutoDiscovery } from "../../src/autodiscovery";
describe("AutoDiscovery", function() { describe("AutoDiscovery", function () {
const getHttpBackend = (): MockHttpBackend => { const getHttpBackend = (): MockHttpBackend => {
const httpBackend = new MockHttpBackend(); const httpBackend = new MockHttpBackend();
AutoDiscovery.setFetchFn(httpBackend.fetchFn as typeof global.fetch); AutoDiscovery.setFetchFn(httpBackend.fetchFn as typeof global.fetch);
return httpBackend; 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(); getHttpBackend();
return Promise.all([ return Promise.all([
// @ts-ignore testing no args // @ts-ignore testing no args
AutoDiscovery.findClientConfig(/* no args */).then(() => { AutoDiscovery.findClientConfig(/* no args */).then(
throw new Error("Expected a failure, not success with no args"); () => {
}, () => { throw new Error("Expected a failure, not success with no args");
return true; },
}), () => {
return true;
},
),
AutoDiscovery.findClientConfig("").then(() => { AutoDiscovery.findClientConfig("").then(
throw new Error("Expected a failure, not success with an empty string"); () => {
}, () => { throw new Error("Expected a failure, not success with an empty string");
return true; },
}), () => {
return true;
},
),
AutoDiscovery.findClientConfig(null as any).then(() => { AutoDiscovery.findClientConfig(null as any).then(
throw new Error("Expected a failure, not success with null"); () => {
}, () => { throw new Error("Expected a failure, not success with null");
return true; },
}), () => {
return true;
},
),
AutoDiscovery.findClientConfig(true as any).then(() => { AutoDiscovery.findClientConfig(true as any).then(
throw new Error("Expected a failure, not success with a non-string"); () => {
}, () => { throw new Error("Expected a failure, not success with a non-string");
return true; },
}), () => {
return true;
},
),
]); ]);
}); });
it("should return PROMPT when .well-known 404s", function() { it("should return PROMPT when .well-known 404s", function () {
const httpBackend = getHttpBackend(); const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(404, {}); httpBackend.when("GET", "/.well-known/matrix/client").respond(404, {});
return Promise.all([ 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(); const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(500, {}); httpBackend.when("GET", "/.well-known/matrix/client").respond(500, {});
return Promise.all([ 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(); const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(400, {}); httpBackend.when("GET", "/.well-known/matrix/client").respond(400, {});
return Promise.all([ 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(); const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, ""); httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "");
return Promise.all([ return Promise.all([
@ -169,9 +181,7 @@ describe("AutoDiscovery", function() {
}; };
return Promise.all([ return Promise.all([
httpBackend.flushAllExpected(), httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then( AutoDiscovery.findClientConfig("example.org").then(expect(expected).toEqual),
expect(expected).toEqual,
),
]); ]);
}); });
@ -257,106 +267,117 @@ describe("AutoDiscovery", function() {
]); ]);
}); });
it("should return FAIL_ERROR when .well-known has an invalid base_url for " + it(
"m.homeserver (verification failure: 404)", function() { "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(); const httpBackend = getHttpBackend();
httpBackend.when("GET", "/_matrix/client/versions").respond(404, {}); httpBackend
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { .when("GET", "/_matrix/client/versions")
"m.homeserver": { .check((req) => {
base_url: "https://example.org", expect(req.path).toEqual("https://example.org/_matrix/client/versions");
}, })
}); .respond(200, {
return Promise.all([ versions: ["r0.0.1"],
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", "/.well-known/matrix/client").respond(200, { httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": { "m.homeserver": {
base_url: "https://example.org", 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(); const httpBackend = getHttpBackend();
httpBackend.when("GET", "/_matrix/client/versions").check((req) => { httpBackend
expect(req.path) .when("GET", "/_matrix/client/versions")
.toEqual("https://chat.example.org/_matrix/client/versions"); .check((req) => {
}).respond(200, { expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
versions: ["r0.0.1"], })
}); .respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": { "m.homeserver": {
// Note: we also expect this test to trim the trailing slash // 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 " + it(
"is wrong (missing base_url)", function() { "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/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(); const httpBackend = getHttpBackend();
httpBackend.when("GET", "/_matrix/client/versions").check((req) => { httpBackend
expect(req.path) .when("GET", "/_matrix/client/versions")
.toEqual("https://chat.example.org/_matrix/client/versions"); .check((req) => {
}).respond(200, { expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
versions: ["r0.0.1"], })
}); .respond(200, {
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { versions: ["r0.0.1"],
"m.homeserver": { });
// Note: we also expect this test to trim the trailing slash httpBackend
base_url: "https://chat.example.org/", .when("GET", "/_matrix/identity/api/v1")
}, .check((req) => {
"m.identity_server": { expect(req.path).toEqual("https://identity.example.org/_matrix/identity/api/v1");
not_base_url: "https://identity.example.org", })
}, .respond(200, {});
});
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", "/.well-known/matrix/client").respond(200, { httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": { "m.homeserver": {
// Note: we also expect this test to trim the trailing slash // 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 " + it("should return SUCCESS and preserve non-standard keys from the " + ".well-known response", function () {
".well-known response", function() {
const httpBackend = getHttpBackend(); const httpBackend = getHttpBackend();
httpBackend.when("GET", "/_matrix/client/versions").check((req) => { httpBackend
expect(req.path) .when("GET", "/_matrix/client/versions")
.toEqual("https://chat.example.org/_matrix/client/versions"); .check((req) => {
}).respond(200, { expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
versions: ["r0.0.1"], })
}); .respond(200, {
httpBackend.when("GET", "/_matrix/identity/api/v1").check((req) => { versions: ["r0.0.1"],
expect(req.path) });
.toEqual("https://identity.example.org/_matrix/identity/api/v1"); httpBackend
}).respond(200, {}); .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", "/.well-known/matrix/client").respond(200, { httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": { "m.homeserver": {
// Note: we also expect this test to trim the trailing slash // Note: we also expect this test to trim the trailing slash

View File

@ -26,23 +26,18 @@ import {
parseTopicContent, parseTopicContent,
} from "../../src/content-helpers"; } from "../../src/content-helpers";
describe('Beacon content helpers', () => { describe("Beacon content helpers", () => {
describe('makeBeaconInfoContent()', () => { describe("makeBeaconInfoContent()", () => {
const mockDateNow = 123456789; const mockDateNow = 123456789;
beforeEach(() => { beforeEach(() => {
jest.spyOn(global.Date, 'now').mockReturnValue(mockDateNow); jest.spyOn(global.Date, "now").mockReturnValue(mockDateNow);
}); });
afterAll(() => { afterAll(() => {
jest.spyOn(global.Date, 'now').mockRestore(); jest.spyOn(global.Date, "now").mockRestore();
}); });
it('create fully defined event content', () => { it("create fully defined event content", () => {
expect(makeBeaconInfoContent( expect(makeBeaconInfoContent(1234, true, "nice beacon_info", LocationAssetType.Pin)).toEqual({
1234, description: "nice beacon_info",
true,
'nice beacon_info',
LocationAssetType.Pin,
)).toEqual({
description: 'nice beacon_info',
timeout: 1234, timeout: 1234,
live: true, live: true,
[M_TIMESTAMP.name]: mockDateNow, [M_TIMESTAMP.name]: mockDateNow,
@ -52,78 +47,72 @@ describe('Beacon content helpers', () => {
}); });
}); });
it('defaults timestamp to current time', () => { it("defaults timestamp to current time", () => {
expect(makeBeaconInfoContent( expect(makeBeaconInfoContent(1234, true, "nice beacon_info", LocationAssetType.Pin)).toEqual(
1234, expect.objectContaining({
true, [M_TIMESTAMP.name]: mockDateNow,
'nice beacon_info', }),
LocationAssetType.Pin, );
)).toEqual(expect.objectContaining({
[M_TIMESTAMP.name]: mockDateNow,
}));
}); });
it('uses timestamp when provided', () => { it("uses timestamp when provided", () => {
expect(makeBeaconInfoContent( expect(makeBeaconInfoContent(1234, true, "nice beacon_info", LocationAssetType.Pin, 99999)).toEqual(
1234, expect.objectContaining({
true, [M_TIMESTAMP.name]: 99999,
'nice beacon_info', }),
LocationAssetType.Pin, );
99999,
)).toEqual(expect.objectContaining({
[M_TIMESTAMP.name]: 99999,
}));
}); });
it('defaults asset type to self when not set', () => { it("defaults asset type to self when not set", () => {
expect(makeBeaconInfoContent( expect(
1234, makeBeaconInfoContent(
true, 1234,
'nice beacon_info', true,
// no assetType passed "nice beacon_info",
)).toEqual(expect.objectContaining({ // no assetType passed
[M_ASSET.name]: { ),
type: LocationAssetType.Self, ).toEqual(
}, expect.objectContaining({
})); [M_ASSET.name]: {
type: LocationAssetType.Self,
},
}),
);
}); });
}); });
describe('makeBeaconContent()', () => { describe("makeBeaconContent()", () => {
it('creates event content without description', () => { it("creates event content without description", () => {
expect(makeBeaconContent( expect(
'geo:foo', makeBeaconContent(
123, "geo:foo",
'$1234', 123,
// no description "$1234",
)).toEqual({ // no description
),
).toEqual({
[M_LOCATION.name]: { [M_LOCATION.name]: {
description: undefined, description: undefined,
uri: 'geo:foo', uri: "geo:foo",
}, },
[M_TIMESTAMP.name]: 123, [M_TIMESTAMP.name]: 123,
"m.relates_to": { "m.relates_to": {
rel_type: REFERENCE_RELATION.name, rel_type: REFERENCE_RELATION.name,
event_id: '$1234', event_id: "$1234",
}, },
}); });
}); });
it('creates event content with description', () => { it("creates event content with description", () => {
expect(makeBeaconContent( expect(makeBeaconContent("geo:foo", 123, "$1234", "test description")).toEqual({
'geo:foo',
123,
'$1234',
'test description',
)).toEqual({
[M_LOCATION.name]: { [M_LOCATION.name]: {
description: 'test description', description: "test description",
uri: 'geo:foo', uri: "geo:foo",
}, },
[M_TIMESTAMP.name]: 123, [M_TIMESTAMP.name]: 123,
"m.relates_to": { "m.relates_to": {
rel_type: REFERENCE_RELATION.name, rel_type: REFERENCE_RELATION.name,
event_id: '$1234', event_id: "$1234",
}, },
}); });
}); });
@ -190,64 +179,81 @@ describe('Beacon content helpers', () => {
}); });
}); });
describe('Topic content helpers', () => { describe("Topic content helpers", () => {
describe('makeTopicContent()', () => { describe("makeTopicContent()", () => {
it('creates fully defined event content without html', () => { it("creates fully defined event content without html", () => {
expect(makeTopicContent("pizza")).toEqual({ expect(makeTopicContent("pizza")).toEqual({
topic: "pizza", topic: "pizza",
[M_TOPIC.name]: [{ [M_TOPIC.name]: [
body: "pizza", {
mimetype: "text/plain", 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({ expect(makeTopicContent("pizza", "<b>pizza</b>")).toEqual({
topic: "pizza", topic: "pizza",
[M_TOPIC.name]: [{ [M_TOPIC.name]: [
body: "pizza", {
mimetype: "text/plain", body: "pizza",
}, { mimetype: "text/plain",
body: "<b>pizza</b>", },
mimetype: "text/html", {
}], body: "<b>pizza</b>",
mimetype: "text/html",
},
],
}); });
}); });
}); });
describe('parseTopicContent()', () => { describe("parseTopicContent()", () => {
it('parses event content with plain text topic without mimetype', () => { it("parses event content with plain text topic without mimetype", () => {
expect(parseTopicContent({ expect(
topic: "pizza", parseTopicContent({
[M_TOPIC.name]: [{ topic: "pizza",
body: "pizza", [M_TOPIC.name]: [
}], {
})).toEqual({ body: "pizza",
},
],
}),
).toEqual({
text: "pizza", text: "pizza",
}); });
}); });
it('parses event content with plain text topic', () => { it("parses event content with plain text topic", () => {
expect(parseTopicContent({ expect(
topic: "pizza", parseTopicContent({
[M_TOPIC.name]: [{ topic: "pizza",
body: "pizza", [M_TOPIC.name]: [
mimetype: "text/plain", {
}], body: "pizza",
})).toEqual({ mimetype: "text/plain",
},
],
}),
).toEqual({
text: "pizza", text: "pizza",
}); });
}); });
it('parses event content with html topic', () => { it("parses event content with html topic", () => {
expect(parseTopicContent({ expect(
topic: "pizza", parseTopicContent({
[M_TOPIC.name]: [{ topic: "pizza",
body: "<b>pizza</b>", [M_TOPIC.name]: [
mimetype: "text/html", {
}], body: "<b>pizza</b>",
})).toEqual({ mimetype: "text/html",
},
],
}),
).toEqual({
text: "pizza", text: "pizza",
html: "<b>pizza</b>", html: "<b>pizza</b>",
}); });

View File

@ -16,60 +16,50 @@ limitations under the License.
import { getHttpUriForMxc } from "../../src/content-repo"; import { getHttpUriForMxc } from "../../src/content-repo";
describe("ContentRepo", function() { describe("ContentRepo", function () {
const baseUrl = "https://my.home.server"; const baseUrl = "https://my.home.server";
describe("getHttpUriForMxc", function() { describe("getHttpUriForMxc", function () {
it("should do nothing to HTTP URLs when allowing direct links", function() { it("should do nothing to HTTP URLs when allowing direct links", function () {
const httpUrl = "http://example.com/image.jpeg"; const httpUrl = "http://example.com/image.jpeg";
expect( expect(getHttpUriForMxc(baseUrl, httpUrl, undefined, undefined, undefined, true)).toEqual(httpUrl);
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"; const httpUrl = "http://example.com/image.jpeg";
expect(getHttpUriForMxc(baseUrl, httpUrl)).toEqual(""); expect(getHttpUriForMxc(baseUrl, httpUrl)).toEqual("");
}); });
it("should return a download URL if no width/height/resize are specified", it("should return a download URL if no width/height/resize are specified", function () {
function() { const mxcUri = "mxc://server.name/resourceid";
const mxcUri = "mxc://server.name/resourceid"; expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual( baseUrl + "/_matrix/media/r0/download/server.name/resourceid",
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 thumbnail URL if a width/height/resize is specified", it("should return the empty string for null input", function () {
function() { expect(getHttpUriForMxc(null as any, "")).toEqual("");
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 after any query parameters", it("should return a thumbnail URL if a width/height/resize is specified", function () {
function() { const mxcUri = "mxc://server.name/resourceid";
const mxcUri = "mxc://server.name/resourceid#automade"; expect(getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
expect(getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual( baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" + "?width=32&height=64&method=crop",
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", it("should put fragments from mxc:// URIs after any query parameters", function () {
function() { const mxcUri = "mxc://server.name/resourceid#automade";
const mxcUri = "mxc://server.name/resourceid#automade"; expect(getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual( baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" + "?width=32#automade",
baseUrl + "/_matrix/media/r0/download/server.name/resourceid#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",
);
});
}); });
}); });

View File

@ -1,4 +1,4 @@
import '../olm-loader'; import "../olm-loader";
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { EventEmitter } from "events"; import { EventEmitter } from "events";
@ -14,11 +14,11 @@ import * as olmlib from "../../src/crypto/olmlib";
import { sleep } from "../../src/utils"; import { sleep } from "../../src/utils";
import { CRYPTO_ENABLED } from "../../src/client"; import { CRYPTO_ENABLED } from "../../src/client";
import { DeviceInfo } from "../../src/crypto/deviceinfo"; import { DeviceInfo } from "../../src/crypto/deviceinfo";
import { logger } from '../../src/logger'; import { logger } from "../../src/logger";
import { MemoryStore } from "../../src"; import { MemoryStore } from "../../src";
import { RoomKeyRequestState } from '../../src/crypto/OutgoingRoomKeyRequestManager'; import { RoomKeyRequestState } from "../../src/crypto/OutgoingRoomKeyRequestManager";
import { RoomMember } from '../../src/models/room-member'; import { RoomMember } from "../../src/models/room-member";
import { IStore } from '../../src/store'; import { IStore } from "../../src/store";
import { IRoomEncryption, RoomList } from "../../src/crypto/RoomList"; import { IRoomEncryption, RoomList } from "../../src/crypto/RoomList";
const Olm = global.Olm; const Olm = global.Olm;
@ -75,10 +75,10 @@ function roomKeyEventForEvent(client: MatrixClient, event: MatrixEvent): MatrixE
type: "m.room_key", type: "m.room_key",
sender: client.getUserId()!, sender: client.getUserId()!,
content: { content: {
"algorithm": olmlib.MEGOLM_ALGORITHM, algorithm: olmlib.MEGOLM_ALGORITHM,
"room_id": roomId, room_id: roomId,
"session_id": eventContent.session_id, session_id: eventContent.session_id,
"session_key": key.key, session_key: key.key,
}, },
}); });
// make onRoomKeyEvent think this was an encrypted event // make onRoomKeyEvent think this was an encrypted event
@ -93,12 +93,12 @@ function roomKeyEventForEvent(client: MatrixClient, event: MatrixEvent): MatrixE
return ksEvent; return ksEvent;
} }
describe("Crypto", function() { describe("Crypto", function () {
if (!CRYPTO_ENABLED) { if (!CRYPTO_ENABLED) {
return; return;
} }
beforeAll(function() { beforeAll(function () {
return Olm.init(); return Olm.init();
}); });
@ -106,34 +106,35 @@ describe("Crypto", function() {
jest.useRealTimers(); jest.useRealTimers();
}); });
it("Crypto exposes the correct olm library version", function() { it("Crypto exposes the correct olm library version", function () {
expect(Crypto.getOlmVersion()[0]).toEqual(3); expect(Crypto.getOlmVersion()[0]).toEqual(3);
}); });
describe("encrypted events", function() { describe("encrypted events", function () {
it("provides encryption information", async function() { it("provides encryption information", async function () {
const client = (new TestClient( const client = new TestClient("@alice:example.com", "deviceid").client;
"@alice:example.com", "deviceid",
)).client;
await client.initCrypto(); await client.initCrypto();
// unencrypted event // unencrypted event
const event = { const event = {
getId: () => "$event_id", getId: () => "$event_id",
getSenderKey: () => null, getSenderKey: () => null,
getWireContent: () => {return {};}, getWireContent: () => {
return {};
},
} as unknown as MatrixEvent; } as unknown as MatrixEvent;
let encryptionInfo = client.getEventEncryptionInfo(event); let encryptionInfo = client.getEventEncryptionInfo(event);
expect(encryptionInfo.encrypted).toBeFalsy(); expect(encryptionInfo.encrypted).toBeFalsy();
// unknown sender (e.g. deleted device), forwarded megolm key (untrusted) // unknown sender (e.g. deleted device), forwarded megolm key (untrusted)
event.getSenderKey = () => 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI'; event.getSenderKey = () => "YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI";
event.getWireContent = () => {return { algorithm: olmlib.MEGOLM_ALGORITHM };}; event.getWireContent = () => {
return { algorithm: olmlib.MEGOLM_ALGORITHM };
};
event.getForwardingCurve25519KeyChain = () => ["not empty"]; event.getForwardingCurve25519KeyChain = () => ["not empty"];
event.isKeySourceUntrusted = () => true; event.isKeySourceUntrusted = () => true;
event.getClaimedEd25519Key = event.getClaimedEd25519Key = () => "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
() => 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
encryptionInfo = client.getEventEncryptionInfo(event); encryptionInfo = client.getEventEncryptionInfo(event);
expect(encryptionInfo.encrypted).toBeTruthy(); expect(encryptionInfo.encrypted).toBeTruthy();
@ -144,10 +145,8 @@ describe("Crypto", function() {
event.getForwardingCurve25519KeyChain = () => []; event.getForwardingCurve25519KeyChain = () => [];
event.isKeySourceUntrusted = () => true; event.isKeySourceUntrusted = () => true;
const device = new DeviceInfo("FLIBBLE"); const device = new DeviceInfo("FLIBBLE");
device.keys["curve25519:FLIBBLE"] = device.keys["curve25519:FLIBBLE"] = "YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI";
'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI'; device.keys["ed25519:FLIBBLE"] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
device.keys["ed25519:FLIBBLE"] =
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
client.crypto!.deviceList.getDeviceByIdentityKey = () => device; client.crypto!.deviceList.getDeviceByIdentityKey = () => device;
encryptionInfo = client.getEventEncryptionInfo(event); encryptionInfo = client.getEventEncryptionInfo(event);
@ -158,8 +157,7 @@ describe("Crypto", function() {
// known sender, trusted megolm key, but bad ed25519key // known sender, trusted megolm key, but bad ed25519key
event.isKeySourceUntrusted = () => false; event.isKeySourceUntrusted = () => false;
device.keys["ed25519:FLIBBLE"] = device.keys["ed25519:FLIBBLE"] = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB';
encryptionInfo = client.getEventEncryptionInfo(event); encryptionInfo = client.getEventEncryptionInfo(event);
expect(encryptionInfo.encrypted).toBeTruthy(); expect(encryptionInfo.encrypted).toBeTruthy();
@ -171,17 +169,17 @@ describe("Crypto", function() {
}); });
}); });
describe('Session management', function() { describe("Session management", function () {
const otkResponse: IClaimOTKsResult = { const otkResponse: IClaimOTKsResult = {
failures: {}, failures: {},
one_time_keys: { one_time_keys: {
'@alice:home.server': { "@alice:home.server": {
aliceDevice: { aliceDevice: {
'signed_curve25519:FLIBBLE': { "signed_curve25519:FLIBBLE": {
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI', key: "YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI",
signatures: { signatures: {
'@alice:home.server': { "@alice:home.server": {
'ed25519:aliceDevice': 'totally a valid signature', "ed25519:aliceDevice": "totally a valid signature",
}, },
}, },
}, },
@ -196,26 +194,29 @@ describe("Crypto", function() {
let fakeEmitter: EventEmitter; let fakeEmitter: EventEmitter;
beforeEach(async function() { beforeEach(async function () {
const mockStorage = new MockStorageApi() as unknown as Storage; const mockStorage = new MockStorageApi() as unknown as Storage;
const clientStore = new MemoryStore({ localStorage: mockStorage }) as unknown as IStore; const clientStore = new MemoryStore({ localStorage: mockStorage }) as unknown as IStore;
const cryptoStore = new MemoryCryptoStore(); const cryptoStore = new MemoryCryptoStore();
cryptoStore.storeEndToEndDeviceData({ cryptoStore.storeEndToEndDeviceData(
devices: { {
'@bob:home.server': { devices: {
'BOBDEVICE': { "@bob:home.server": {
algorithms: [], BOBDEVICE: {
verified: 1, algorithms: [],
known: false, verified: 1,
keys: { known: false,
'curve25519:BOBDEVICE': 'this is a key', keys: {
"curve25519:BOBDEVICE": "this is a key",
},
}, },
}, },
}, },
trackingStatus: {},
}, },
trackingStatus: {}, {},
}, {}); );
mockBaseApis = { mockBaseApis = {
sendToDevice: jest.fn(), sendToDevice: jest.fn(),
@ -239,76 +240,72 @@ describe("Crypto", function() {
await crypto.init(); await crypto.init();
}); });
afterEach(async function() { afterEach(async function () {
await crypto.stop(); await crypto.stop();
}); });
it("restarts wedged Olm sessions", async function() { it("restarts wedged Olm sessions", async function () {
const prom = new Promise<void>((resolve) => { const prom = new Promise<void>((resolve) => {
mockBaseApis.claimOneTimeKeys = function() { mockBaseApis.claimOneTimeKeys = function () {
resolve(); resolve();
return Promise.resolve(otkResponse); return Promise.resolve(otkResponse);
}; };
}); });
fakeEmitter.emit('toDeviceEvent', { fakeEmitter.emit("toDeviceEvent", {
getId: jest.fn().mockReturnValue("$wedged"), getId: jest.fn().mockReturnValue("$wedged"),
getType: jest.fn().mockReturnValue('m.room.message'), getType: jest.fn().mockReturnValue("m.room.message"),
getContent: jest.fn().mockReturnValue({ getContent: jest.fn().mockReturnValue({
msgtype: 'm.bad.encrypted', msgtype: "m.bad.encrypted",
}), }),
getWireContent: jest.fn().mockReturnValue({ getWireContent: jest.fn().mockReturnValue({
algorithm: 'm.olm.v1.curve25519-aes-sha2', algorithm: "m.olm.v1.curve25519-aes-sha2",
sender_key: 'this is a key', sender_key: "this is a key",
}), }),
getSender: jest.fn().mockReturnValue('@bob:home.server'), getSender: jest.fn().mockReturnValue("@bob:home.server"),
}); });
await prom; await prom;
}); });
}); });
describe('Key requests', function() { describe("Key requests", function () {
let aliceClient: MatrixClient; let aliceClient: MatrixClient;
let bobClient: MatrixClient; let bobClient: MatrixClient;
let claraClient: MatrixClient; let claraClient: MatrixClient;
beforeEach(async function() { beforeEach(async function () {
aliceClient = (new TestClient( aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
"@alice:example.com", "alicedevice", bobClient = new TestClient("@bob:example.com", "bobdevice").client;
)).client; claraClient = new TestClient("@clara:example.com", "claradevice").client;
bobClient = (new TestClient(
"@bob:example.com", "bobdevice",
)).client;
claraClient = (new TestClient(
"@clara:example.com", "claradevice",
)).client;
await aliceClient.initCrypto(); await aliceClient.initCrypto();
await bobClient.initCrypto(); await bobClient.initCrypto();
await claraClient.initCrypto(); await claraClient.initCrypto();
}); });
afterEach(async function() { afterEach(async function () {
aliceClient.stopClient(); aliceClient.stopClient();
bobClient.stopClient(); bobClient.stopClient();
claraClient.stopClient(); claraClient.stopClient();
}); });
it("does not cancel keyshare requests until all messages are decrypted with trusted keys", async function() { it("does not cancel keyshare requests until all messages are decrypted with trusted keys", async function () {
const encryptionCfg = { const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2", algorithm: "m.megolm.v1.aes-sha2",
}; };
const roomId = "!someroom"; const roomId = "!someroom";
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {}); const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {}); const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
// Make Bob invited by Alice so Bob will accept Alice's forwarded keys // Make Bob invited by Alice so Bob will accept Alice's forwarded keys
bobRoom.currentState.setStateEvents([new MatrixEvent({ bobRoom.currentState.setStateEvents([
type: "m.room.member", new MatrixEvent({
sender: "@alice:example.com", type: "m.room.member",
room_id: roomId, sender: "@alice:example.com",
content: { membership: "invite" }, room_id: roomId,
state_key: "@bob:example.com", content: { membership: "invite" },
})]); state_key: "@bob:example.com",
}),
]);
aliceClient.store.storeRoom(aliceRoom); aliceClient.store.storeRoom(aliceRoom);
bobClient.store.storeRoom(bobRoom); bobClient.store.storeRoom(bobRoom);
await aliceClient.setRoomEncryption(roomId, encryptionCfg); await aliceClient.setRoomEncryption(roomId, encryptionCfg);
@ -335,35 +332,37 @@ describe("Crypto", function() {
}, },
}), }),
]; ];
await Promise.all(events.map(async (event) => { await Promise.all(
// alice encrypts each event, and then bob tries to decrypt events.map(async (event) => {
// them without any keys, so that they'll be in pending // alice encrypts each event, and then bob tries to decrypt
await aliceClient.crypto!.encryptEvent(event, aliceRoom); // them without any keys, so that they'll be in pending
// remove keys from the event await aliceClient.crypto!.encryptEvent(event, aliceRoom);
// @ts-ignore private properties // remove keys from the event
event.clearEvent = undefined; // @ts-ignore private properties
// @ts-ignore private properties event.clearEvent = undefined;
event.senderCurve25519Key = null; // @ts-ignore private properties
// @ts-ignore private properties event.senderCurve25519Key = null;
event.claimedEd25519Key = null; // @ts-ignore private properties
try { event.claimedEd25519Key = null;
await bobClient.crypto!.decryptEvent(event); try {
} catch (e) { await bobClient.crypto!.decryptEvent(event);
// we expect this to fail because we don't have the } catch (e) {
// decryption keys yet // we expect this to fail because we don't have the
} // decryption keys yet
})); }
}),
);
const device = new DeviceInfo(aliceClient.deviceId!); const device = new DeviceInfo(aliceClient.deviceId!);
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device; bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
const bobDecryptor = bobClient.crypto!.getRoomDecryptor( const bobDecryptor = bobClient.crypto!.getRoomDecryptor(roomId, olmlib.MEGOLM_ALGORITHM);
roomId, olmlib.MEGOLM_ALGORITHM,
);
const decryptEventsPromise = Promise.all(events.map((ev) => { const decryptEventsPromise = Promise.all(
return awaitEvent(ev, "Event.decrypted"); events.map((ev) => {
})); return awaitEvent(ev, "Event.decrypted");
}),
);
// keyshare the session key starting at the second message, so // keyshare the session key starting at the second message, so
// the first message can't be decrypted yet, but the second one // the first message can't be decrypted yet, but the second one
@ -418,9 +417,9 @@ describe("Crypto", function() {
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeFalsy(); expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeFalsy();
}); });
it("should error if a forwarded room key lacks a content.sender_key", async function() { it("should error if a forwarded room key lacks a content.sender_key", async function () {
const encryptionCfg = { const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2", algorithm: "m.megolm.v1.aes-sha2",
}; };
const roomId = "!someroom"; const roomId = "!someroom";
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {}); const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
@ -459,9 +458,7 @@ describe("Crypto", function() {
const device = new DeviceInfo(aliceClient.deviceId!); const device = new DeviceInfo(aliceClient.deviceId!);
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device; bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
const bobDecryptor = bobClient.crypto!.getRoomDecryptor( const bobDecryptor = bobClient.crypto!.getRoomDecryptor(roomId, olmlib.MEGOLM_ALGORITHM);
roomId, olmlib.MEGOLM_ALGORITHM,
);
const ksEvent = await keyshareEventForEvent(aliceClient, event, 1); const ksEvent = await keyshareEventForEvent(aliceClient, event, 1);
ksEvent.getContent().sender_key = undefined; // test ksEvent.getContent().sender_key = undefined; // test
@ -470,7 +467,7 @@ describe("Crypto", function() {
expect(bobClient.crypto!.olmDevice.addInboundGroupSession).not.toHaveBeenCalled(); expect(bobClient.crypto!.olmDevice.addInboundGroupSession).not.toHaveBeenCalled();
}); });
it("creates a new keyshare request if we request a keyshare", async function() { it("creates a new keyshare request if we request a keyshare", async function () {
// make sure that cancelAndResend... creates a new keyshare request // make sure that cancelAndResend... creates a new keyshare request
// if there wasn't an already-existing one // if there wasn't an already-existing one
const event = new MatrixEvent({ const event = new MatrixEvent({
@ -490,11 +487,10 @@ describe("Crypto", function() {
session_id: "sessionid", session_id: "sessionid",
sender_key: "senderkey", sender_key: "senderkey",
}; };
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)) expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeDefined();
.toBeDefined();
}); });
it("uses a new txnid for re-requesting keys", async function() { it("uses a new txnid for re-requesting keys", async function () {
jest.useFakeTimers(); jest.useFakeTimers();
const event = new MatrixEvent({ const event = new MatrixEvent({
@ -539,9 +535,9 @@ describe("Crypto", function() {
expect(aliceSendToDevice.mock.calls[2][2]).not.toBe(txnId); expect(aliceSendToDevice.mock.calls[2][2]).not.toBe(txnId);
}); });
it("should accept forwarded keys which it requested", async function() { it("should accept forwarded keys which it requested", async function () {
const encryptionCfg = { const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2", algorithm: "m.megolm.v1.aes-sha2",
}; };
const roomId = "!someroom"; const roomId = "!someroom";
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {}); const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
@ -572,24 +568,26 @@ describe("Crypto", function() {
}, },
}), }),
]; ];
await Promise.all(events.map(async (event) => { await Promise.all(
// alice encrypts each event, and then bob tries to decrypt events.map(async (event) => {
// them without any keys, so that they'll be in pending // alice encrypts each event, and then bob tries to decrypt
await aliceClient.crypto!.encryptEvent(event, aliceRoom); // them without any keys, so that they'll be in pending
// remove keys from the event await aliceClient.crypto!.encryptEvent(event, aliceRoom);
// @ts-ignore private properties // remove keys from the event
event.clearEvent = undefined; // @ts-ignore private properties
// @ts-ignore private properties event.clearEvent = undefined;
event.senderCurve25519Key = null; // @ts-ignore private properties
// @ts-ignore private properties event.senderCurve25519Key = null;
event.claimedEd25519Key = null; // @ts-ignore private properties
try { event.claimedEd25519Key = null;
await bobClient.crypto!.decryptEvent(event); try {
} catch (e) { await bobClient.crypto!.decryptEvent(event);
// we expect this to fail because we don't have the } catch (e) {
// decryption keys yet // we expect this to fail because we don't have the
} // decryption keys yet
})); }
}),
);
const device = new DeviceInfo(aliceClient.deviceId!); const device = new DeviceInfo(aliceClient.deviceId!);
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device; bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
@ -607,18 +605,17 @@ describe("Crypto", function() {
}; };
const outgoingReq = await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody); const outgoingReq = await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody);
expect(outgoingReq).toBeDefined(); expect(outgoingReq).toBeDefined();
await cryptoStore.updateOutgoingRoomKeyRequest( await cryptoStore.updateOutgoingRoomKeyRequest(outgoingReq!.requestId, RoomKeyRequestState.Unsent, {
outgoingReq!.requestId, RoomKeyRequestState.Unsent, state: RoomKeyRequestState.Sent,
{ state: RoomKeyRequestState.Sent }, });
);
const bobDecryptor = bobClient.crypto!.getRoomDecryptor( const bobDecryptor = bobClient.crypto!.getRoomDecryptor(roomId, olmlib.MEGOLM_ALGORITHM);
roomId, olmlib.MEGOLM_ALGORITHM,
);
const decryptEventsPromise = Promise.all(events.map((ev) => { const decryptEventsPromise = Promise.all(
return awaitEvent(ev, "Event.decrypted"); events.map((ev) => {
})); return awaitEvent(ev, "Event.decrypted");
}),
);
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0); const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
await bobDecryptor.onRoomKeyEvent(ksEvent); await bobDecryptor.onRoomKeyEvent(ksEvent);
const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey( const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
@ -632,22 +629,24 @@ describe("Crypto", function() {
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted"); expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
}); });
it("should accept forwarded keys from the user who invited it to the room", async function() { it("should accept forwarded keys from the user who invited it to the room", async function () {
const encryptionCfg = { const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2", algorithm: "m.megolm.v1.aes-sha2",
}; };
const roomId = "!someroom"; const roomId = "!someroom";
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {}); const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {}); const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
const claraRoom = new Room(roomId, claraClient, "@clara:example.com", {}); const claraRoom = new Room(roomId, claraClient, "@clara:example.com", {});
// Make Bob invited by Clara // Make Bob invited by Clara
bobRoom.currentState.setStateEvents([new MatrixEvent({ bobRoom.currentState.setStateEvents([
type: "m.room.member", new MatrixEvent({
sender: "@clara:example.com", type: "m.room.member",
room_id: roomId, sender: "@clara:example.com",
content: { membership: "invite" }, room_id: roomId,
state_key: "@bob:example.com", content: { membership: "invite" },
})]); state_key: "@bob:example.com",
}),
]);
aliceClient.store.storeRoom(aliceRoom); aliceClient.store.storeRoom(aliceRoom);
bobClient.store.storeRoom(bobRoom); bobClient.store.storeRoom(bobRoom);
claraClient.store.storeRoom(claraRoom); claraClient.store.storeRoom(claraRoom);
@ -676,36 +675,38 @@ describe("Crypto", function() {
}, },
}), }),
]; ];
await Promise.all(events.map(async (event) => { await Promise.all(
// alice encrypts each event, and then bob tries to decrypt events.map(async (event) => {
// them without any keys, so that they'll be in pending // alice encrypts each event, and then bob tries to decrypt
await aliceClient.crypto!.encryptEvent(event, aliceRoom); // them without any keys, so that they'll be in pending
// remove keys from the event await aliceClient.crypto!.encryptEvent(event, aliceRoom);
// @ts-ignore private properties // remove keys from the event
event.clearEvent = undefined; // @ts-ignore private properties
// @ts-ignore private properties event.clearEvent = undefined;
event.senderCurve25519Key = null; // @ts-ignore private properties
// @ts-ignore private properties event.senderCurve25519Key = null;
event.claimedEd25519Key = null; // @ts-ignore private properties
try { event.claimedEd25519Key = null;
await bobClient.crypto!.decryptEvent(event); try {
} catch (e) { await bobClient.crypto!.decryptEvent(event);
// we expect this to fail because we don't have the } catch (e) {
// decryption keys yet // we expect this to fail because we don't have the
} // decryption keys yet
})); }
}),
);
const device = new DeviceInfo(claraClient.deviceId!); const device = new DeviceInfo(claraClient.deviceId!);
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device; bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@clara:example.com"; bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@clara:example.com";
const bobDecryptor = bobClient.crypto!.getRoomDecryptor( const bobDecryptor = bobClient.crypto!.getRoomDecryptor(roomId, olmlib.MEGOLM_ALGORITHM);
roomId, olmlib.MEGOLM_ALGORITHM,
);
const decryptEventsPromise = Promise.all(events.map((ev) => { const decryptEventsPromise = Promise.all(
return awaitEvent(ev, "Event.decrypted"); events.map((ev) => {
})); return awaitEvent(ev, "Event.decrypted");
}),
);
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0); const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
ksEvent.event.sender = claraClient.getUserId()!; ksEvent.event.sender = claraClient.getUserId()!;
ksEvent.sender = new RoomMember(roomId, claraClient.getUserId()!); ksEvent.sender = new RoomMember(roomId, claraClient.getUserId()!);
@ -721,9 +722,9 @@ describe("Crypto", function() {
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted"); expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
}); });
it("should accept forwarded keys from one of its own user's other devices", async function() { it("should accept forwarded keys from one of its own user's other devices", async function () {
const encryptionCfg = { const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2", algorithm: "m.megolm.v1.aes-sha2",
}; };
const roomId = "!someroom"; const roomId = "!someroom";
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {}); const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
@ -754,37 +755,39 @@ describe("Crypto", function() {
}, },
}), }),
]; ];
await Promise.all(events.map(async (event) => { await Promise.all(
// alice encrypts each event, and then bob tries to decrypt events.map(async (event) => {
// them without any keys, so that they'll be in pending // alice encrypts each event, and then bob tries to decrypt
await aliceClient.crypto!.encryptEvent(event, aliceRoom); // them without any keys, so that they'll be in pending
// remove keys from the event await aliceClient.crypto!.encryptEvent(event, aliceRoom);
// @ts-ignore private properties // remove keys from the event
event.clearEvent = undefined; // @ts-ignore private properties
// @ts-ignore private properties event.clearEvent = undefined;
event.senderCurve25519Key = null; // @ts-ignore private properties
// @ts-ignore private properties event.senderCurve25519Key = null;
event.claimedEd25519Key = null; // @ts-ignore private properties
try { event.claimedEd25519Key = null;
await bobClient.crypto!.decryptEvent(event); try {
} catch (e) { await bobClient.crypto!.decryptEvent(event);
// we expect this to fail because we don't have the } catch (e) {
// decryption keys yet // we expect this to fail because we don't have the
} // decryption keys yet
})); }
}),
);
const device = new DeviceInfo(claraClient.deviceId!); const device = new DeviceInfo(claraClient.deviceId!);
device.verified = DeviceInfo.DeviceVerification.VERIFIED; device.verified = DeviceInfo.DeviceVerification.VERIFIED;
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device; bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@bob:example.com"; bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@bob:example.com";
const bobDecryptor = bobClient.crypto!.getRoomDecryptor( const bobDecryptor = bobClient.crypto!.getRoomDecryptor(roomId, olmlib.MEGOLM_ALGORITHM);
roomId, olmlib.MEGOLM_ALGORITHM,
);
const decryptEventsPromise = Promise.all(events.map((ev) => { const decryptEventsPromise = Promise.all(
return awaitEvent(ev, "Event.decrypted"); events.map((ev) => {
})); return awaitEvent(ev, "Event.decrypted");
}),
);
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0); const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
ksEvent.event.sender = bobClient.getUserId()!; ksEvent.event.sender = bobClient.getUserId()!;
ksEvent.sender = new RoomMember(roomId, bobClient.getUserId()!); ksEvent.sender = new RoomMember(roomId, bobClient.getUserId()!);
@ -800,9 +803,9 @@ describe("Crypto", function() {
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted"); expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
}); });
it("should not accept unexpected forwarded keys for a room it's in", async function() { it("should not accept unexpected forwarded keys for a room it's in", async function () {
const encryptionCfg = { const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2", algorithm: "m.megolm.v1.aes-sha2",
}; };
const roomId = "!someroom"; const roomId = "!someroom";
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {}); const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
@ -836,32 +839,32 @@ describe("Crypto", function() {
}, },
}), }),
]; ];
await Promise.all(events.map(async (event) => { await Promise.all(
// alice encrypts each event, and then bob tries to decrypt events.map(async (event) => {
// them without any keys, so that they'll be in pending // alice encrypts each event, and then bob tries to decrypt
await aliceClient.crypto!.encryptEvent(event, aliceRoom); // them without any keys, so that they'll be in pending
// remove keys from the event await aliceClient.crypto!.encryptEvent(event, aliceRoom);
// @ts-ignore private properties // remove keys from the event
event.clearEvent = undefined; // @ts-ignore private properties
// @ts-ignore private properties event.clearEvent = undefined;
event.senderCurve25519Key = null; // @ts-ignore private properties
// @ts-ignore private properties event.senderCurve25519Key = null;
event.claimedEd25519Key = null; // @ts-ignore private properties
try { event.claimedEd25519Key = null;
await bobClient.crypto!.decryptEvent(event); try {
} catch (e) { await bobClient.crypto!.decryptEvent(event);
// we expect this to fail because we don't have the } catch (e) {
// decryption keys yet // we expect this to fail because we don't have the
} // decryption keys yet
})); }
}),
);
const device = new DeviceInfo(claraClient.deviceId!); const device = new DeviceInfo(claraClient.deviceId!);
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device; bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com"; bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
const bobDecryptor = bobClient.crypto!.getRoomDecryptor( const bobDecryptor = bobClient.crypto!.getRoomDecryptor(roomId, olmlib.MEGOLM_ALGORITHM);
roomId, olmlib.MEGOLM_ALGORITHM,
);
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0); const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
ksEvent.event.sender = claraClient.getUserId()!; ksEvent.event.sender = claraClient.getUserId()!;
@ -875,9 +878,9 @@ describe("Crypto", function() {
expect(key).toBeNull(); expect(key).toBeNull();
}); });
it("should park forwarded keys for a room it's not in", async function() { it("should park forwarded keys for a room it's not in", async function () {
const encryptionCfg = { const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2", algorithm: "m.megolm.v1.aes-sha2",
}; };
const roomId = "!someroom"; const roomId = "!someroom";
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {}); const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
@ -905,26 +908,26 @@ describe("Crypto", function() {
}, },
}), }),
]; ];
await Promise.all(events.map(async (event) => { await Promise.all(
// alice encrypts each event, and then bob tries to decrypt events.map(async (event) => {
// them without any keys, so that they'll be in pending // alice encrypts each event, and then bob tries to decrypt
await aliceClient.crypto!.encryptEvent(event, aliceRoom); // them without any keys, so that they'll be in pending
// remove keys from the event await aliceClient.crypto!.encryptEvent(event, aliceRoom);
// @ts-ignore private properties // remove keys from the event
event.clearEvent = undefined; // @ts-ignore private properties
// @ts-ignore private properties event.clearEvent = undefined;
event.senderCurve25519Key = null; // @ts-ignore private properties
// @ts-ignore private properties event.senderCurve25519Key = null;
event.claimedEd25519Key = null; // @ts-ignore private properties
})); event.claimedEd25519Key = null;
}),
);
const device = new DeviceInfo(aliceClient.deviceId!); const device = new DeviceInfo(aliceClient.deviceId!);
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device; bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com"; bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
const bobDecryptor = bobClient.crypto!.getRoomDecryptor( const bobDecryptor = bobClient.crypto!.getRoomDecryptor(roomId, olmlib.MEGOLM_ALGORITHM);
roomId, olmlib.MEGOLM_ALGORITHM,
);
const content = events[0].getWireContent(); const content = events[0].getWireContent();
@ -943,22 +946,24 @@ describe("Crypto", function() {
content.session_id, content.session_id,
); );
const parked = await bobClient.crypto!.cryptoStore.takeParkedSharedHistory(roomId); const parked = await bobClient.crypto!.cryptoStore.takeParkedSharedHistory(roomId);
expect(parked).toEqual([{ expect(parked).toEqual([
senderId: aliceClient.getUserId(), {
senderKey: content.sender_key, senderId: aliceClient.getUserId(),
sessionId: content.session_id, senderKey: content.sender_key,
sessionKey: aliceKey!.key, sessionId: content.session_id,
keysClaimed: { ed25519: aliceKey!.sender_claimed_ed25519_key }, sessionKey: aliceKey!.key,
forwardingCurve25519KeyChain: ["akey"], keysClaimed: { ed25519: aliceKey!.sender_claimed_ed25519_key },
}]); forwardingCurve25519KeyChain: ["akey"],
},
]);
}); });
}); });
describe('Secret storage', function() { describe("Secret storage", function () {
it("creates secret storage even if there is no keyInfo", async function() { it("creates secret storage even if there is no keyInfo", async function () {
jest.spyOn(logger, 'log').mockImplementation(() => {}); jest.spyOn(logger, "log").mockImplementation(() => {});
jest.setTimeout(10000); jest.setTimeout(10000);
const client = (new TestClient("@a:example.com", "dev")).client; const client = new TestClient("@a:example.com", "dev").client;
await client.initCrypto(); await client.initCrypto();
client.crypto!.getSecretStorageKey = jest.fn().mockResolvedValue(null); client.crypto!.getSecretStorageKey = jest.fn().mockResolvedValue(null);
client.crypto!.isCrossSigningReady = async () => false; client.crypto!.isCrossSigningReady = async () => false;
@ -990,7 +995,7 @@ describe("Crypto", function() {
ensureOlmSessionsForDevices = jest.spyOn(olmlib, "ensureOlmSessionsForDevices"); ensureOlmSessionsForDevices = jest.spyOn(olmlib, "ensureOlmSessionsForDevices");
ensureOlmSessionsForDevices.mockResolvedValue({}); ensureOlmSessionsForDevices.mockResolvedValue({});
encryptMessageForDevice = jest.spyOn(olmlib, "encryptMessageForDevice"); encryptMessageForDevice = jest.spyOn(olmlib, "encryptMessageForDevice");
encryptMessageForDevice.mockImplementation(async (...[result,,,,,, payload]) => { encryptMessageForDevice.mockImplementation(async (...[result, , , , , , payload]) => {
result.plaintext = { type: 0, body: JSON.stringify(payload) }; result.plaintext = { type: 0, body: JSON.stringify(payload) };
}); });
@ -998,10 +1003,7 @@ describe("Crypto", function() {
// running initCrypto should trigger a key upload // running initCrypto should trigger a key upload
client.httpBackend.when("POST", "/keys/upload").respond(200, {}); client.httpBackend.when("POST", "/keys/upload").respond(200, {});
await Promise.all([ await Promise.all([client.client.initCrypto(), client.httpBackend.flush("/keys/upload", 1)]);
client.client.initCrypto(),
client.httpBackend.flush("/keys/upload", 1),
]);
encryptedPayload = { encryptedPayload = {
algorithm: "m.olm.v1.curve25519-aes-sha2", algorithm: "m.olm.v1.curve25519-aes-sha2",
@ -1035,7 +1037,8 @@ describe("Crypto", function() {
}, },
}, },
}); });
}).respond(200, {}); })
.respond(200, {});
await Promise.all([ await Promise.all([
client.client.encryptAndSendToDevices( client.client.encryptAndSendToDevices(
@ -1051,7 +1054,7 @@ describe("Crypto", function() {
}); });
it("sends nothing to devices that couldn't be encrypted to", async () => { it("sends nothing to devices that couldn't be encrypted to", async () => {
encryptMessageForDevice.mockImplementation(async (...[result,,,, userId, device, payload]) => { encryptMessageForDevice.mockImplementation(async (...[result, , , , userId, device, payload]) => {
// Refuse to encrypt to Carol's desktop device // Refuse to encrypt to Carol's desktop device
if (userId === "@carol:example.org" && device.deviceId === "caroldesktop") return; if (userId === "@carol:example.org" && device.deviceId === "caroldesktop") return;
result.plaintext = { type: 0, body: JSON.stringify(payload) }; result.plaintext = { type: 0, body: JSON.stringify(payload) };
@ -1111,10 +1114,13 @@ describe("Crypto", function() {
it("should free PkDecryption", () => { it("should free PkDecryption", () => {
const free = jest.fn(); const free = jest.fn();
jest.spyOn(Olm, "PkDecryption").mockImplementation(() => ({ jest.spyOn(Olm, "PkDecryption").mockImplementation(
init_with_private_key: jest.fn(), () =>
free, ({
}) as unknown as PkDecryption); init_with_private_key: jest.fn(),
free,
} as unknown as PkDecryption),
);
client.client.checkSecretStoragePrivateKey(new Uint8Array(), ""); client.client.checkSecretStoragePrivateKey(new Uint8Array(), "");
expect(free).toHaveBeenCalled(); expect(free).toHaveBeenCalled();
}); });
@ -1134,10 +1140,13 @@ describe("Crypto", function() {
it("should free PkSigning", () => { it("should free PkSigning", () => {
const free = jest.fn(); const free = jest.fn();
jest.spyOn(Olm, "PkSigning").mockImplementation(() => ({ jest.spyOn(Olm, "PkSigning").mockImplementation(
init_with_seed: jest.fn(), () =>
free, ({
}) as unknown as PkSigning); init_with_seed: jest.fn(),
free,
} as unknown as PkSigning),
);
client.client.checkCrossSigningPrivateKey(new Uint8Array(), ""); client.client.checkCrossSigningPrivateKey(new Uint8Array(), "");
expect(free).toHaveBeenCalled(); expect(free).toHaveBeenCalled();
}); });
@ -1151,7 +1160,7 @@ describe("Crypto", function() {
await client.client.initCrypto(); await client.client.initCrypto();
}); });
afterEach(async function() { afterEach(async function () {
await client!.stop(); await client!.stop();
}); });
@ -1167,7 +1176,7 @@ describe("Crypto", function() {
let clientStore: IStore; let clientStore: IStore;
let crypto: Crypto; let crypto: Crypto;
beforeEach(async function() { beforeEach(async function () {
mockClient = {} as MatrixClient; mockClient = {} as MatrixClient;
const mockStorage = new MockStorageApi() as unknown as Storage; const mockStorage = new MockStorageApi() as unknown as Storage;
clientStore = new MemoryStore({ localStorage: mockStorage }) as unknown as IStore; clientStore = new MemoryStore({ localStorage: mockStorage }) as unknown as IStore;

View File

@ -14,28 +14,21 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import '../../olm-loader'; import "../../olm-loader";
import { import { CrossSigningInfo, createCryptoStoreCacheCallbacks } from "../../../src/crypto/CrossSigning";
CrossSigningInfo, import { IndexedDBCryptoStore } from "../../../src/crypto/store/indexeddb-crypto-store";
createCryptoStoreCacheCallbacks, import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store";
} from '../../../src/crypto/CrossSigning'; import "fake-indexeddb/auto";
import { import "jest-localstorage-mock";
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 { OlmDevice } from "../../../src/crypto/OlmDevice";
import { logger } from '../../../src/logger'; import { logger } from "../../../src/logger";
const userId = "@alice:example.com"; const userId = "@alice:example.com";
// Private key for tests only // Private key for tests only
const testKey = new Uint8Array([ const testKey = new Uint8Array([
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82, 0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82, 0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef, 0xae, 0xb1, 0x05,
0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef, 0xc1, 0xe7, 0x62, 0x78, 0xa6, 0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
0xae, 0xb1, 0x05, 0xc1, 0xe7, 0x62, 0x78, 0xa6,
0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
]); ]);
const types = [ const types = [
@ -50,13 +43,13 @@ badKey[0] ^= 1;
const masterKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"; const masterKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
describe("CrossSigningInfo.getCrossSigningKey", function() { describe("CrossSigningInfo.getCrossSigningKey", function () {
if (!global.Olm) { 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; return;
} }
beforeAll(function() { beforeAll(function () {
return global.Olm.init(); return global.Olm.init();
}); });
@ -65,13 +58,12 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
await expect(info.getCrossSigningKey("master")).rejects.toThrow(); await expect(info.getCrossSigningKey("master")).rejects.toThrow();
}); });
it.each(types)("should throw if the callback returns falsey", it.each(types)("should throw if the callback returns falsey", async ({ type, shouldCache }) => {
async ({ type, shouldCache }) => { const info = new CrossSigningInfo(userId, {
const info = new CrossSigningInfo(userId, { getCrossSigningKey: async () => false as unknown as Uint8Array,
getCrossSigningKey: async () => false as unknown as Uint8Array,
});
await expect(info.getCrossSigningKey(type)).rejects.toThrow("falsey");
}); });
await expect(info.getCrossSigningKey(type)).rejects.toThrow("falsey");
});
it("should throw if the expected key doesn't come back", async () => { it("should throw if the expected key doesn't come back", async () => {
const info = new CrossSigningInfo(userId, { 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)" + it.each(types)(
" and does not call app if one is found" + "should request a key from the cache callback (if set)" + " and does not call app if one is found" + " %o",
" %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",
async ({ type, shouldCache }) => { async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockImplementation(() => { const getCrossSigningKey = jest.fn().mockImplementation(() => {
if (shouldCache) { if (shouldCache) {
@ -162,56 +99,98 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
} }
}); });
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(testKey); const getCrossSigningKeyCache = jest.fn().mockResolvedValue(testKey);
const storeCrossSigningKeyCache = jest.fn().mockRejectedValue( const info = new CrossSigningInfo(userId, { getCrossSigningKey }, { getCrossSigningKeyCache });
new Error("Tried to store a value from cache"), 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( const info = new CrossSigningInfo(
userId, userId,
{ getCrossSigningKey }, { getCrossSigningKey },
{ getCrossSigningKeyCache, storeCrossSigningKeyCache }, { getCrossSigningKeyCache, storeCrossSigningKeyCache },
); );
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(0);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub); const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(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" + /* Also expect that the cache gets updated */
" if one is not found", async ({ type, shouldCache }) => { expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
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 */ it.each(types)(
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0); "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" + /* Also expect that the cache gets updated */
" calls app if that key doesn't match", async ({ type, shouldCache }) => { expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
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);
});
}); });
/* /*
@ -219,19 +198,20 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
* it's not possible to get one in normal execution unless you hack as we do here. * it's not possible to get one in normal execution unless you hack as we do here.
*/ */
describe.each([ describe.each([
["IndexedDBCryptoStore", ["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(global.indexedDB, "tests")],
() => new IndexedDBCryptoStore(global.indexedDB, "tests")], ["LocalStorageCryptoStore", () => new IndexedDBCryptoStore(undefined!, "tests")],
["LocalStorageCryptoStore", [
() => new IndexedDBCryptoStore(undefined!, "tests")], "MemoryCryptoStore",
["MemoryCryptoStore", () => { () => {
const store = new IndexedDBCryptoStore(undefined!, "tests"); const store = new IndexedDBCryptoStore(undefined!, "tests");
// @ts-ignore set private properties // @ts-ignore set private properties
store._backend = new MemoryCryptoStore(); store._backend = new MemoryCryptoStore();
// @ts-ignore // @ts-ignore
store._backendPromise = Promise.resolve(store._backend); store._backendPromise = Promise.resolve(store._backend);
return store; return store;
}], },
])("CrossSigning > createCryptoStoreCacheCallbacks [%s]", function(name, dbFactory) { ],
])("CrossSigning > createCryptoStoreCacheCallbacks [%s]", function (name, dbFactory) {
let store: IndexedDBCryptoStore; let store: IndexedDBCryptoStore;
beforeAll(() => { beforeAll(() => {
@ -245,8 +225,10 @@ describe.each([
it("should cache data to the store and retrieve it", async () => { it("should cache data to the store and retrieve it", async () => {
await store.startup(); await store.startup();
const olmDevice = new OlmDevice(store); const olmDevice = new OlmDevice(store);
const { getCrossSigningKeyCache, storeCrossSigningKeyCache } = const { getCrossSigningKeyCache, storeCrossSigningKeyCache } = createCryptoStoreCacheCallbacks(
createCryptoStoreCacheCallbacks(store, olmDevice); store,
olmDevice,
);
await storeCrossSigningKeyCache!("self_signing", testKey); await storeCrossSigningKeyCache!("self_signing", testKey);
// If we've not saved anything, don't expect anything // If we've not saved anything, don't expect anything

View File

@ -25,31 +25,26 @@ import { OlmDevice } from "../../../src/crypto/OlmDevice";
import { CryptoStore } from "../../../src/crypto/store/base"; import { CryptoStore } from "../../../src/crypto/store/base";
const signedDeviceList: IDownloadKeyResult = { const signedDeviceList: IDownloadKeyResult = {
"failures": {}, failures: {},
"device_keys": { device_keys: {
"@test1:sw1v.org": { "@test1:sw1v.org": {
"HGKAWHRVJQ": { HGKAWHRVJQ: {
"signatures": { signatures: {
"@test1:sw1v.org": { "@test1:sw1v.org": {
"ed25519:HGKAWHRVJQ": "ed25519:HGKAWHRVJQ":
"8PB450fxKDn5s8IiRZ2N2t6MiueQYVRLHFEzqIi1eLdxx1w" + "8PB450fxKDn5s8IiRZ2N2t6MiueQYVRLHFEzqIi1eLdxx1w" +
"XEPC1/1Uz9T4gwnKlMVAKkhB5hXQA/3kjaeLABw", "XEPC1/1Uz9T4gwnKlMVAKkhB5hXQA/3kjaeLABw",
}, },
}, },
"user_id": "@test1:sw1v.org", user_id: "@test1:sw1v.org",
"keys": { keys: {
"ed25519:HGKAWHRVJQ": "ed25519:HGKAWHRVJQ": "0gI/T6C+mn1pjtvnnW2yB2l1IIBb/5ULlBXi/LXFSEQ",
"0gI/T6C+mn1pjtvnnW2yB2l1IIBb/5ULlBXi/LXFSEQ", "curve25519:HGKAWHRVJQ": "mbIZED1dBsgIgkgzxDpxKkJmsr4hiWlGzQTvUnQe3RY",
"curve25519:HGKAWHRVJQ":
"mbIZED1dBsgIgkgzxDpxKkJmsr4hiWlGzQTvUnQe3RY",
}, },
"algorithms": [ algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
"m.olm.v1.curve25519-aes-sha2", device_id: "HGKAWHRVJQ",
"m.megolm.v1.aes-sha2", unsigned: {
], device_display_name: "",
"device_id": "HGKAWHRVJQ",
"unsigned": {
"device_display_name": "",
}, },
}, },
}, },
@ -57,50 +52,45 @@ const signedDeviceList: IDownloadKeyResult = {
}; };
const signedDeviceList2: IDownloadKeyResult = { const signedDeviceList2: IDownloadKeyResult = {
"failures": {}, failures: {},
"device_keys": { device_keys: {
"@test2:sw1v.org": { "@test2:sw1v.org": {
"QJVRHWAKGH": { QJVRHWAKGH: {
"signatures": { signatures: {
"@test2:sw1v.org": { "@test2:sw1v.org": {
"ed25519:QJVRHWAKGH": "ed25519:QJVRHWAKGH":
"w1xxdLe1iIqzEFHLRVYQeuiM6t2N2ZRiI8s5nDKxf054BP8" + "w1xxdLe1iIqzEFHLRVYQeuiM6t2N2ZRiI8s5nDKxf054BP8" +
"1CPEX/AQXh5BhkKAVMlKnwg4T9zU1/wBALeajk3", "1CPEX/AQXh5BhkKAVMlKnwg4T9zU1/wBALeajk3",
}, },
}, },
"user_id": "@test2:sw1v.org", user_id: "@test2:sw1v.org",
"keys": { keys: {
"ed25519:QJVRHWAKGH": "ed25519:QJVRHWAKGH": "Ig0/C6T+bBII1l2By2Wnnvtjp1nm/iXBlLU5/QESFXL",
"Ig0/C6T+bBII1l2By2Wnnvtjp1nm/iXBlLU5/QESFXL", "curve25519:QJVRHWAKGH": "YR3eQnUvTQzGlWih4rsmJkKxpDxzgkgIgsBd1DEZIbm",
"curve25519:QJVRHWAKGH":
"YR3eQnUvTQzGlWih4rsmJkKxpDxzgkgIgsBd1DEZIbm",
}, },
"algorithms": [ algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
"m.olm.v1.curve25519-aes-sha2", device_id: "QJVRHWAKGH",
"m.megolm.v1.aes-sha2", unsigned: {
], device_display_name: "",
"device_id": "QJVRHWAKGH",
"unsigned": {
"device_display_name": "",
}, },
}, },
}, },
}, },
}; };
describe('DeviceList', function() { describe("DeviceList", function () {
let downloadSpy: jest.Mock; let downloadSpy: jest.Mock;
let cryptoStore: CryptoStore; let cryptoStore: CryptoStore;
let deviceLists: DeviceList[] = []; let deviceLists: DeviceList[] = [];
beforeEach(function() { beforeEach(function () {
deviceLists = []; deviceLists = [];
downloadSpy = jest.fn(); downloadSpy = jest.fn();
cryptoStore = new MemoryCryptoStore(); cryptoStore = new MemoryCryptoStore();
}); });
afterEach(function() { afterEach(function () {
for (const dl of deviceLists) { for (const dl of deviceLists) {
dl.stop(); dl.stop();
} }
@ -109,94 +99,96 @@ describe('DeviceList', function() {
function createTestDeviceList(keyDownloadChunkSize = 250) { function createTestDeviceList(keyDownloadChunkSize = 250) {
const baseApis = { const baseApis = {
downloadKeysForUsers: downloadSpy, downloadKeysForUsers: downloadSpy,
getUserId: () => '@test1:sw1v.org', getUserId: () => "@test1:sw1v.org",
deviceId: 'HGKAWHRVJQ', deviceId: "HGKAWHRVJQ",
} as unknown as MatrixClient; } as unknown as MatrixClient;
const mockOlm = { const mockOlm = {
verifySignature: function(key: string, message: string, signature: string) {}, verifySignature: function (key: string, message: string, signature: string) {},
} as unknown as OlmDevice; } as unknown as OlmDevice;
const dl = new DeviceList(baseApis, cryptoStore, mockOlm, keyDownloadChunkSize); const dl = new DeviceList(baseApis, cryptoStore, mockOlm, keyDownloadChunkSize);
deviceLists.push(dl); deviceLists.push(dl);
return dl; return dl;
} }
it("should successfully download and store device keys", function() { it("should successfully download and store device keys", function () {
const dl = createTestDeviceList(); const dl = createTestDeviceList();
dl.startTrackingDeviceList('@test1:sw1v.org'); dl.startTrackingDeviceList("@test1:sw1v.org");
const queryDefer1 = utils.defer<IDownloadKeyResult>(); const queryDefer1 = utils.defer<IDownloadKeyResult>();
downloadSpy.mockReturnValue(queryDefer1.promise); downloadSpy.mockReturnValue(queryDefer1.promise);
const prom1 = dl.refreshOutdatedDeviceLists(); const prom1 = dl.refreshOutdatedDeviceLists();
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {}); expect(downloadSpy).toHaveBeenCalledWith(["@test1:sw1v.org"], {});
queryDefer1.resolve(utils.deepCopy(signedDeviceList)); queryDefer1.resolve(utils.deepCopy(signedDeviceList));
return prom1.then(() => { return prom1.then(() => {
const storedKeys = dl.getRawStoredDevicesForUser('@test1:sw1v.org'); const storedKeys = dl.getRawStoredDevicesForUser("@test1:sw1v.org");
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']); expect(Object.keys(storedKeys)).toEqual(["HGKAWHRVJQ"]);
dl.stop(); dl.stop();
}); });
}); });
it("should have an outdated devicelist on an invalidation while an " + it("should have an outdated devicelist on an invalidation while an " + "update is in progress", function () {
"update is in progress", function() {
const dl = createTestDeviceList(); const dl = createTestDeviceList();
dl.startTrackingDeviceList('@test1:sw1v.org'); dl.startTrackingDeviceList("@test1:sw1v.org");
const queryDefer1 = utils.defer<IDownloadKeyResult>(); const queryDefer1 = utils.defer<IDownloadKeyResult>();
downloadSpy.mockReturnValue(queryDefer1.promise); downloadSpy.mockReturnValue(queryDefer1.promise);
const prom1 = dl.refreshOutdatedDeviceLists(); const prom1 = dl.refreshOutdatedDeviceLists();
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {}); expect(downloadSpy).toHaveBeenCalledWith(["@test1:sw1v.org"], {});
downloadSpy.mockReset(); downloadSpy.mockReset();
// outdated notif arrives while the request is in flight. // outdated notif arrives while the request is in flight.
const queryDefer2 = utils.defer(); const queryDefer2 = utils.defer();
downloadSpy.mockReturnValue(queryDefer2.promise); downloadSpy.mockReturnValue(queryDefer2.promise);
dl.invalidateUserDeviceList('@test1:sw1v.org'); dl.invalidateUserDeviceList("@test1:sw1v.org");
dl.refreshOutdatedDeviceLists(); dl.refreshOutdatedDeviceLists();
dl.saveIfDirty().then(() => { dl.saveIfDirty()
// the first request completes .then(() => {
queryDefer1.resolve({ // the first request completes
failures: {}, queryDefer1.resolve({
device_keys: { failures: {},
'@test1:sw1v.org': {}, 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); const dl = createTestDeviceList(1);
dl.startTrackingDeviceList('@test1:sw1v.org'); dl.startTrackingDeviceList("@test1:sw1v.org");
dl.startTrackingDeviceList('@test2:sw1v.org'); dl.startTrackingDeviceList("@test2:sw1v.org");
const queryDefer1 = utils.defer<IDownloadKeyResult>(); const queryDefer1 = utils.defer<IDownloadKeyResult>();
downloadSpy.mockReturnValueOnce(queryDefer1.promise); downloadSpy.mockReturnValueOnce(queryDefer1.promise);
@ -205,16 +197,16 @@ describe('DeviceList', function() {
const prom1 = dl.refreshOutdatedDeviceLists(); const prom1 = dl.refreshOutdatedDeviceLists();
expect(downloadSpy).toBeCalledTimes(2); expect(downloadSpy).toBeCalledTimes(2);
expect(downloadSpy).toHaveBeenNthCalledWith(1, ['@test1:sw1v.org'], {}); expect(downloadSpy).toHaveBeenNthCalledWith(1, ["@test1:sw1v.org"], {});
expect(downloadSpy).toHaveBeenNthCalledWith(2, ['@test2:sw1v.org'], {}); expect(downloadSpy).toHaveBeenNthCalledWith(2, ["@test2:sw1v.org"], {});
queryDefer1.resolve(utils.deepCopy(signedDeviceList)); queryDefer1.resolve(utils.deepCopy(signedDeviceList));
queryDefer2.resolve(utils.deepCopy(signedDeviceList2)); queryDefer2.resolve(utils.deepCopy(signedDeviceList2));
return prom1.then(() => { return prom1.then(() => {
const storedKeys1 = dl.getRawStoredDevicesForUser('@test1:sw1v.org'); const storedKeys1 = dl.getRawStoredDevicesForUser("@test1:sw1v.org");
expect(Object.keys(storedKeys1)).toEqual(['HGKAWHRVJQ']); expect(Object.keys(storedKeys1)).toEqual(["HGKAWHRVJQ"]);
const storedKeys2 = dl.getRawStoredDevicesForUser('@test2:sw1v.org'); const storedKeys2 = dl.getRawStoredDevicesForUser("@test2:sw1v.org");
expect(Object.keys(storedKeys2)).toEqual(['QJVRHWAKGH']); expect(Object.keys(storedKeys2)).toEqual(["QJVRHWAKGH"]);
dl.stop(); dl.stop();
}); });
}); });

File diff suppressed because it is too large Load Diff

View File

@ -15,15 +15,15 @@ See the License for the specific language governing permissions and
limitations under the License. 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 { MemoryCryptoStore } from "../../../../src/crypto/store/memory-crypto-store";
import { logger } from "../../../../src/logger"; import { logger } from "../../../../src/logger";
import { OlmDevice } from "../../../../src/crypto/OlmDevice"; import { OlmDevice } from "../../../../src/crypto/OlmDevice";
import * as olmlib from "../../../../src/crypto/olmlib"; import * as olmlib from "../../../../src/crypto/olmlib";
import { DeviceInfo } from "../../../../src/crypto/deviceinfo"; import { DeviceInfo } from "../../../../src/crypto/deviceinfo";
import { MatrixClient } from '../../../../src'; import { MatrixClient } from "../../../../src";
function makeOlmDevice() { function makeOlmDevice() {
const cryptoStore = new MemoryCryptoStore(); const cryptoStore = new MemoryCryptoStore();
@ -34,7 +34,7 @@ function makeOlmDevice() {
async function setupSession(initiator: OlmDevice, opponent: OlmDevice) { async function setupSession(initiator: OlmDevice, opponent: OlmDevice) {
await opponent.generateOneTimeKeys(1); await opponent.generateOneTimeKeys(1);
const keys = await opponent.getOneTimeKeys(); 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; return sid;
@ -46,62 +46,57 @@ function alwaysSucceed<T>(promise: Promise<T>): Promise<T | void> {
return promise.catch(() => {}); return promise.catch(() => {});
} }
describe("OlmDevice", function() { describe("OlmDevice", function () {
if (!global.Olm) { if (!global.Olm) {
logger.warn('Not running megolm unit tests: libolm not present'); logger.warn("Not running megolm unit tests: libolm not present");
return; return;
} }
beforeAll(function() { beforeAll(function () {
return global.Olm.init(); return global.Olm.init();
}); });
let aliceOlmDevice: OlmDevice; let aliceOlmDevice: OlmDevice;
let bobOlmDevice: OlmDevice; let bobOlmDevice: OlmDevice;
beforeEach(async function() { beforeEach(async function () {
aliceOlmDevice = makeOlmDevice(); aliceOlmDevice = makeOlmDevice();
bobOlmDevice = makeOlmDevice(); bobOlmDevice = makeOlmDevice();
await aliceOlmDevice.init(); await aliceOlmDevice.init();
await bobOlmDevice.init(); await bobOlmDevice.init();
}); });
describe('olm', function() { describe("olm", function () {
it("can decrypt messages", async function() { it("can decrypt messages", async function () {
const sid = await setupSession(aliceOlmDevice, bobOlmDevice); const sid = await setupSession(aliceOlmDevice, bobOlmDevice);
const ciphertext = await aliceOlmDevice.encryptMessage( const ciphertext = (await aliceOlmDevice.encryptMessage(
bobOlmDevice.deviceCurve25519Key!, bobOlmDevice.deviceCurve25519Key!,
sid, sid,
"The olm or proteus is an aquatic salamander in the family Proteidae", "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( const result = await bobOlmDevice.createInboundSession(
aliceOlmDevice.deviceCurve25519Key!, aliceOlmDevice.deviceCurve25519Key!,
ciphertext.type, ciphertext.type,
ciphertext.body, ciphertext.body,
); );
expect(result.payload).toEqual( expect(result.payload).toEqual("The olm or proteus is an aquatic salamander in the family Proteidae");
"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 sessionId = await setupSession(aliceOlmDevice, bobOlmDevice);
const exported = await bobOlmDevice.export(); const exported = await bobOlmDevice.export();
// At this moment only Alice (the “initiator” in setupSession) has a session // At this moment only Alice (the “initiator” in setupSession) has a session
expect(exported.sessions).toEqual([]); expect(exported.sessions).toEqual([]);
const MESSAGE = ( const MESSAGE = "The olm or proteus is an aquatic salamander" + " in the family Proteidae";
"The olm or proteus is an aquatic salamander" const ciphertext = (await aliceOlmDevice.encryptMessage(
+ " in the family Proteidae"
);
const ciphertext = await aliceOlmDevice.encryptMessage(
bobOlmDevice.deviceCurve25519Key!, bobOlmDevice.deviceCurve25519Key!,
sessionId, sessionId,
MESSAGE, MESSAGE,
) as any; // OlmDevice.encryptMessage has incorrect return type )) as any; // OlmDevice.encryptMessage has incorrect return type
const bobRecreatedOlmDevice = makeOlmDevice(); const bobRecreatedOlmDevice = makeOlmDevice();
bobRecreatedOlmDevice.init({ fromExportedDevice: exported }); bobRecreatedOlmDevice.init({ fromExportedDevice: exported });
@ -117,15 +112,12 @@ describe("OlmDevice", function() {
// this time we expect Bob to have a session to export // this time we expect Bob to have a session to export
expect(exportedAgain.sessions).toHaveLength(1); expect(exportedAgain.sessions).toHaveLength(1);
const MESSAGE_2 = ( const MESSAGE_2 = "In contrast to most amphibians," + " the olm is entirely aquatic";
"In contrast to most amphibians," const ciphertext2 = (await aliceOlmDevice.encryptMessage(
+ " the olm is entirely aquatic"
);
const ciphertext2 = await aliceOlmDevice.encryptMessage(
bobOlmDevice.deviceCurve25519Key!, bobOlmDevice.deviceCurve25519Key!,
sessionId, sessionId,
MESSAGE_2, MESSAGE_2,
) as any; // OlmDevice.encryptMessage has incorrect return type )) as any; // OlmDevice.encryptMessage has incorrect return type
const bobRecreatedAgainOlmDevice = makeOlmDevice(); const bobRecreatedAgainOlmDevice = makeOlmDevice();
bobRecreatedAgainOlmDevice.init({ fromExportedDevice: exportedAgain }); bobRecreatedAgainOlmDevice.init({ fromExportedDevice: exportedAgain });
@ -140,7 +132,7 @@ describe("OlmDevice", function() {
expect(decrypted2).toEqual(MESSAGE_2); 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 // if we call ensureOlmSessionsForDevices multiple times, it should
// only try to create one session at a time, even if the server is // only try to create one session at a time, even if the server is
// slow // slow
@ -156,22 +148,21 @@ describe("OlmDevice", function() {
} as unknown as MockedObject<MatrixClient>; } as unknown as MockedObject<MatrixClient>;
const devicesByUser = { const devicesByUser = {
"@bob:example.com": [ "@bob:example.com": [
DeviceInfo.fromStorage({ DeviceInfo.fromStorage(
keys: { {
"curve25519:ABCDEFG": "akey", keys: {
"curve25519:ABCDEFG": "akey",
},
}, },
}, "ABCDEFG"), "ABCDEFG",
),
], ],
}; };
// start two tasks that try to ensure that there's an olm session // start two tasks that try to ensure that there's an olm session
const promises = Promise.all([ const promises = Promise.all([
alwaysSucceed(olmlib.ensureOlmSessionsForDevices( alwaysSucceed(olmlib.ensureOlmSessionsForDevices(aliceOlmDevice, baseApis, devicesByUser)),
aliceOlmDevice, baseApis, devicesByUser, alwaysSucceed(olmlib.ensureOlmSessionsForDevices(aliceOlmDevice, baseApis, devicesByUser)),
)),
alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
aliceOlmDevice, baseApis, devicesByUser,
)),
]); ]);
await new Promise((resolve) => { await new Promise((resolve) => {
@ -191,7 +182,7 @@ describe("OlmDevice", function() {
expect(count).toBe(2); 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 // This test checks whether `ensureOlmSessionsForDevices` properly
// handles multiple tasks in flight ensuring some set of devices in // handles multiple tasks in flight ensuring some set of devices in
// common without deadlocks. // common without deadlocks.
@ -207,54 +198,47 @@ describe("OlmDevice", function() {
}, },
} as unknown as MockedObject<MatrixClient>; } as unknown as MockedObject<MatrixClient>;
const deviceBobA = DeviceInfo.fromStorage({ const deviceBobA = DeviceInfo.fromStorage(
keys: { {
"curve25519:BOB-A": "akey", keys: {
"curve25519:BOB-A": "akey",
},
}, },
}, "BOB-A"); "BOB-A",
const deviceBobB = DeviceInfo.fromStorage({ );
keys: { const deviceBobB = DeviceInfo.fromStorage(
"curve25519:BOB-B": "bkey", {
keys: {
"curve25519:BOB-B": "bkey",
},
}, },
}, "BOB-B"); "BOB-B",
);
// There's no required ordering of devices per user, so here we // There's no required ordering of devices per user, so here we
// create two different orderings so that each task reserves a // create two different orderings so that each task reserves a
// device the other task needs before continuing. // device the other task needs before continuing.
const devicesByUserAB = { const devicesByUserAB = {
"@bob:example.com": [ "@bob:example.com": [deviceBobA, deviceBobB],
deviceBobA,
deviceBobB,
],
}; };
const devicesByUserBA = { const devicesByUserBA = {
"@bob:example.com": [ "@bob:example.com": [deviceBobB, deviceBobA],
deviceBobB,
deviceBobA,
],
}; };
const task1 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices( const task1 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(aliceOlmDevice, baseApis, devicesByUserAB));
aliceOlmDevice, baseApis, devicesByUserAB,
));
// After a single tick through the first task, it should have // After a single tick through the first task, it should have
// claimed ownership of all devices to avoid deadlocking others. // claimed ownership of all devices to avoid deadlocking others.
expect(Object.keys(aliceOlmDevice.sessionsInProgress).length).toBe(2); expect(Object.keys(aliceOlmDevice.sessionsInProgress).length).toBe(2);
const task2 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices( const task2 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(aliceOlmDevice, baseApis, devicesByUserBA));
aliceOlmDevice, baseApis, devicesByUserBA,
));
// The second task should not have changed the ownership count, as // The second task should not have changed the ownership count, as
// it's waiting on the first task. // it's waiting on the first task.
expect(Object.keys(aliceOlmDevice.sessionsInProgress).length).toBe(2); expect(Object.keys(aliceOlmDevice.sessionsInProgress).length).toBe(2);
// Track the tasks, but don't await them yet. // Track the tasks, but don't await them yet.
const promises = Promise.all([ const promises = Promise.all([task1, task2]);
task1,
task2,
]);
await new Promise((resolve) => { await new Promise((resolve) => {
setTimeout(resolve, 200); setTimeout(resolve, 200);

View File

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import '../../olm-loader'; import "../../olm-loader";
import { logger } from "../../../src/logger"; import { logger } from "../../../src/logger";
import * as olmlib from "../../../src/crypto/olmlib"; import * as olmlib from "../../../src/crypto/olmlib";
import { MatrixClient } from "../../../src/client"; import { MatrixClient } from "../../../src/client";
@ -28,30 +28,31 @@ import { Crypto } from "../../../src/crypto";
import { resetCrossSigningKeys } from "./crypto-utils"; import { resetCrossSigningKeys } from "./crypto-utils";
import { BackupManager } from "../../../src/crypto/backup"; import { BackupManager } from "../../../src/crypto/backup";
import { StubStore } from "../../../src/store/stub"; import { StubStore } from "../../../src/store/stub";
import { IndexedDBCryptoStore, MatrixScheduler } from '../../../src'; import { IndexedDBCryptoStore, MatrixScheduler } from "../../../src";
import { CryptoStore } from "../../../src/crypto/store/base"; import { CryptoStore } from "../../../src/crypto/store/base";
import { MegolmDecryption as MegolmDecryptionClass } from "../../../src/crypto/algorithms/megolm"; import { MegolmDecryption as MegolmDecryptionClass } from "../../../src/crypto/algorithms/megolm";
import { IKeyBackupInfo } from "../../../src/crypto/keybackup"; import { IKeyBackupInfo } from "../../../src/crypto/keybackup";
const Olm = global.Olm; 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({ const ENCRYPTED_EVENT = new MatrixEvent({
type: 'm.room.encrypted', type: "m.room.encrypted",
room_id: '!ROOM:ID', room_id: "!ROOM:ID",
content: { content: {
algorithm: 'm.megolm.v1.aes-sha2', algorithm: "m.megolm.v1.aes-sha2",
sender_key: 'SENDER_CURVE25519', sender_key: "SENDER_CURVE25519",
session_id: SESSION_ID, session_id: SESSION_ID,
ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N' ciphertext:
+ 'CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl' "AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N" +
+ 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs', "CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl" +
"mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs",
}, },
event_id: '$event1', event_id: "$event1",
origin_server_ts: 1507753886000, origin_server_ts: 1507753886000,
}); });
@ -60,19 +61,20 @@ const CURVE25519_KEY_BACKUP_DATA = {
forwarded_count: 0, forwarded_count: 0,
is_verified: false, is_verified: false,
session_data: { session_data: {
ciphertext: '2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw' ciphertext:
+ '6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ' "2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw" +
+ 'Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9' "6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ" +
+ 'SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy' "Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9" +
+ 'Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF' "SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy" +
+ 'ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV' "Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF" +
+ '4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv' "ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV" +
+ 'C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe' "4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv" +
+ 'Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf' "C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe" +
+ 'QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy' "Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf" +
+ 'iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg', "QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy" +
mac: '5lxYBHQU80M', "iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg",
ephemeral: '/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14', mac: "5lxYBHQU80M",
ephemeral: "/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14",
}, },
}; };
@ -81,23 +83,24 @@ const AES256_KEY_BACKUP_DATA = {
forwarded_count: 0, forwarded_count: 0,
is_verified: false, is_verified: false,
session_data: { session_data: {
iv: 'b3Jqqvm5S9QdmXrzssspLQ', iv: "b3Jqqvm5S9QdmXrzssspLQ",
ciphertext: 'GOOASO3E9ThogkG0zMjEduGLM3u9jHZTkS7AvNNbNj3q1znwk4OlaVKXce' ciphertext:
+ '7ynofiiYIiS865VlOqrKEEXv96XzRyUpgn68e3WsicwYl96EtjIEh/iY003PG2Qd' "GOOASO3E9ThogkG0zMjEduGLM3u9jHZTkS7AvNNbNj3q1znwk4OlaVKXce" +
+ 'EluT899Ax7PydpUHxEktbWckMppYomUR5q8x1KI1SsOQIiJaIGThmIMPANRCFiK0' "7ynofiiYIiS865VlOqrKEEXv96XzRyUpgn68e3WsicwYl96EtjIEh/iY003PG2Qd" +
+ 'WQj+q+dnhzx4lt9AFqU5bKov8qKnw2qGYP7/+6RmJ0Kpvs8tG6lrcNDEHtFc2r0r' "EluT899Ax7PydpUHxEktbWckMppYomUR5q8x1KI1SsOQIiJaIGThmIMPANRCFiK0" +
+ 'KKubDypo0Vc8EWSwsAHdKa36ewRavpreOuE8Z9RLfY0QIR1ecXrMqW0CdGFr7H3P' "WQj+q+dnhzx4lt9AFqU5bKov8qKnw2qGYP7/+6RmJ0Kpvs8tG6lrcNDEHtFc2r0r" +
+ 'vcjF8sjwvQAavzxEKT1WMGizSMLeKWo2mgZ5cKnwV5HGUAw596JQvKs9laG2U89K' "KKubDypo0Vc8EWSwsAHdKa36ewRavpreOuE8Z9RLfY0QIR1ecXrMqW0CdGFr7H3P" +
+ 'YrT0sH30vi62HKzcBLcDkWkUSNYPz7UiZ1MM0L380UA+1ZOXSOmtBA9xxzzbc8Xd' "vcjF8sjwvQAavzxEKT1WMGizSMLeKWo2mgZ5cKnwV5HGUAw596JQvKs9laG2U89K" +
+ 'fRimVgklGdxrxjzuNLYhL2BvVH4oPWonD9j0bvRwE6XkimdbGQA8HB7UmXXjE8WA' "YrT0sH30vi62HKzcBLcDkWkUSNYPz7UiZ1MM0L380UA+1ZOXSOmtBA9xxzzbc8Xd" +
+ 'RgaDHkfzoA3g3aeQ', "fRimVgklGdxrxjzuNLYhL2BvVH4oPWonD9j0bvRwE6XkimdbGQA8HB7UmXXjE8WA" +
mac: 'uR988UYgGL99jrvLLPX3V1ows+UYbktTmMxPAo2kxnU', "RgaDHkfzoA3g3aeQ",
mac: "uR988UYgGL99jrvLLPX3V1ows+UYbktTmMxPAo2kxnU",
}, },
}; };
const CURVE25519_BACKUP_INFO = { const CURVE25519_BACKUP_INFO = {
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
version: '1', version: "1",
auth_data: { auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
}, },
@ -105,7 +108,7 @@ const CURVE25519_BACKUP_INFO = {
const AES256_BACKUP_INFO: IKeyBackupInfo = { const AES256_BACKUP_INFO: IKeyBackupInfo = {
algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2", algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2",
version: '1', version: "1",
auth_data: {} as IKeyBackupInfo["auth_data"], auth_data: {} as IKeyBackupInfo["auth_data"],
}; };
@ -120,15 +123,13 @@ function saveCrossSigningKeys(k: Record<string, Uint8Array>) {
} }
function makeTestScheduler(): MatrixScheduler { function makeTestScheduler(): MatrixScheduler {
return ([ return (["getQueueForEvent", "queueEvent", "removeEventFromQueue", "setProcessFunction"] as const).reduce(
"getQueueForEvent", (r, k) => {
"queueEvent", r[k] = jest.fn();
"removeEventFromQueue", return r;
"setProcessFunction", },
] as const).reduce((r, k) => { {} as MatrixScheduler,
r[k] = jest.fn(); );
return r;
}, {} as MatrixScheduler);
} }
function makeTestClient(cryptoStore: CryptoStore) { function makeTestClient(cryptoStore: CryptoStore) {
@ -153,13 +154,13 @@ function makeTestClient(cryptoStore: CryptoStore) {
return client; return client;
} }
describe("MegolmBackup", function() { describe("MegolmBackup", function () {
if (!global.Olm) { 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; return;
} }
beforeAll(function() { beforeAll(function () {
return Olm.init(); return Olm.init();
}); });
@ -168,8 +169,8 @@ describe("MegolmBackup", function() {
let mockCrypto: Crypto; let mockCrypto: Crypto;
let cryptoStore: CryptoStore; let cryptoStore: CryptoStore;
let megolmDecryption: MegolmDecryptionClass; let megolmDecryption: MegolmDecryptionClass;
beforeEach(async function() { beforeEach(async function () {
mockCrypto = testUtils.mock(Crypto, 'Crypto'); mockCrypto = testUtils.mock(Crypto, "Crypto");
// @ts-ignore making mock // @ts-ignore making mock
mockCrypto.backupManager = testUtils.mock(BackupManager, "BackupManager"); mockCrypto.backupManager = testUtils.mock(BackupManager, "BackupManager");
mockCrypto.backupManager.backupInfo = CURVE25519_BACKUP_INFO; mockCrypto.backupManager.backupInfo = CURVE25519_BACKUP_INFO;
@ -181,18 +182,17 @@ describe("MegolmBackup", function() {
// we stub out the olm encryption bits // we stub out the olm encryption bits
mockOlmLib = {} as unknown as typeof olmlib; mockOlmLib = {} as unknown as typeof olmlib;
mockOlmLib.ensureOlmSessionsForDevices = jest.fn(); mockOlmLib.ensureOlmSessionsForDevices = jest.fn();
mockOlmLib.encryptMessageForDevice = mockOlmLib.encryptMessageForDevice = jest.fn().mockResolvedValue(undefined);
jest.fn().mockResolvedValue(undefined);
}); });
describe("backup", function() { describe("backup", function () {
let mockBaseApis: MatrixClient; let mockBaseApis: MatrixClient;
beforeEach(function() { beforeEach(function () {
mockBaseApis = {} as unknown as MatrixClient; mockBaseApis = {} as unknown as MatrixClient;
megolmDecryption = new MegolmDecryption({ megolmDecryption = new MegolmDecryption({
userId: '@user:id', userId: "@user:id",
crypto: mockCrypto, crypto: mockCrypto,
olmDevice: olmDevice, olmDevice: olmDevice,
baseApis: mockBaseApis, baseApis: mockBaseApis,
@ -206,23 +206,23 @@ describe("MegolmBackup", function() {
// ideally we would use lolex, but we have no oportunity // ideally we would use lolex, but we have no oportunity
// to tick the clock between the first try and the retry. // to tick the clock between the first try and the retry.
const realSetTimeout = global.setTimeout; const realSetTimeout = global.setTimeout;
jest.spyOn(global, 'setTimeout').mockImplementation(function(f, n) { jest.spyOn(global, "setTimeout").mockImplementation(function (f, n) {
return realSetTimeout(f!, n!/100); return realSetTimeout(f!, n! / 100);
}); });
}); });
afterEach(function() { afterEach(function () {
jest.spyOn(global, 'setTimeout').mockRestore(); 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(); const groupSession = new Olm.OutboundGroupSession();
groupSession.create(); groupSession.create();
// construct a fake decrypted key event via the use of a mocked // construct a fake decrypted key event via the use of a mocked
// 'crypto' implementation. // 'crypto' implementation.
const event = new MatrixEvent({ const event = new MatrixEvent({
type: 'm.room.encrypted', type: "m.room.encrypted",
}); });
event.getWireType = () => "m.room.encrypted"; event.getWireType = () => "m.room.encrypted";
event.getWireContent = () => { event.getWireContent = () => {
@ -232,9 +232,9 @@ describe("MegolmBackup", function() {
}; };
const decryptedData = { const decryptedData = {
clearEvent: { clearEvent: {
type: 'm.room_key', type: "m.room_key",
content: { content: {
algorithm: 'm.megolm.v1.aes-sha2', algorithm: "m.megolm.v1.aes-sha2",
room_id: ROOM_ID, room_id: ROOM_ID,
session_id: groupSession.session_id(), session_id: groupSession.session_id(),
session_key: groupSession.session_key(), session_key: groupSession.session_key(),
@ -244,24 +244,27 @@ describe("MegolmBackup", function() {
claimedEd25519Key: "SENDER_ED25519", claimedEd25519Key: "SENDER_ED25519",
}; };
mockCrypto.decryptEvent = function() { mockCrypto.decryptEvent = function () {
return Promise.resolve(decryptedData); return Promise.resolve(decryptedData);
}; };
mockCrypto.cancelRoomKeyRequest = function() {}; mockCrypto.cancelRoomKeyRequest = function () {};
// @ts-ignore readonly field write // @ts-ignore readonly field write
mockCrypto.backupManager = { mockCrypto.backupManager = {
backupGroupSession: jest.fn(), backupGroupSession: jest.fn(),
}; };
return event.attemptDecryption(mockCrypto).then(() => { return event
return megolmDecryption.onRoomKeyEvent(event); .attemptDecryption(mockCrypto)
}).then(() => { .then(() => {
expect(mockCrypto.backupManager.backupGroupSession).toHaveBeenCalled(); 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(); const groupSession = new Olm.OutboundGroupSession();
groupSession.create(); groupSession.create();
const ibGroupSession = new Olm.InboundGroupSession(); const ibGroupSession = new Olm.InboundGroupSession();
@ -270,7 +273,7 @@ describe("MegolmBackup", function() {
const client = makeTestClient(cryptoStore); const client = makeTestClient(cryptoStore);
megolmDecryption = new MegolmDecryption({ megolmDecryption = new MegolmDecryption({
userId: '@user:id', userId: "@user:id",
crypto: mockCrypto, crypto: mockCrypto,
olmDevice: olmDevice, olmDevice: olmDevice,
baseApis: client, baseApis: client,
@ -280,39 +283,36 @@ describe("MegolmBackup", function() {
// @ts-ignore private field access // @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib; megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto() return client
.initCrypto()
.then(() => { .then(() => {
return cryptoStore.doTxn( return cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => {
"readwrite", cryptoStore.addEndToEndInboundGroupSession(
[IndexedDBCryptoStore.STORE_SESSIONS], "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
(txn) => { groupSession.session_id(),
cryptoStore.addEndToEndInboundGroupSession( {
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", forwardingCurve25519KeyChain: undefined!,
groupSession.session_id(), keysClaimed: {
{ ed25519: "SENDER_ED25519",
forwardingCurve25519KeyChain: undefined!,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice.pickleKey),
}, },
txn); room_id: ROOM_ID,
}); session: ibGroupSession.pickle(olmDevice.pickleKey),
},
txn,
);
});
}) })
.then(async () => { .then(async () => {
await client.enableKeyBackup({ await client.enableKeyBackup({
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
version: '1', version: "1",
auth_data: { auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
}, },
}); });
let numCalls = 0; let numCalls = 0;
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
client.http.authedRequest = function( client.http.authedRequest = function (method, path, queryParams, data, opts): any {
method, path, queryParams, data, opts,
): any {
++numCalls; ++numCalls;
expect(numCalls).toBeLessThanOrEqual(1); expect(numCalls).toBeLessThanOrEqual(1);
if (numCalls >= 2) { if (numCalls >= 2) {
@ -322,7 +322,7 @@ describe("MegolmBackup", function() {
} }
expect(method).toBe("PUT"); expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys"); 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).toBeDefined();
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty( expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
groupSession.session_id(), groupSession.session_id(),
@ -341,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(); const groupSession = new Olm.OutboundGroupSession();
groupSession.create(); groupSession.create();
const ibGroupSession = new Olm.InboundGroupSession(); const ibGroupSession = new Olm.InboundGroupSession();
@ -350,7 +350,7 @@ describe("MegolmBackup", function() {
const client = makeTestClient(cryptoStore); const client = makeTestClient(cryptoStore);
megolmDecryption = new MegolmDecryption({ megolmDecryption = new MegolmDecryption({
userId: '@user:id', userId: "@user:id",
crypto: mockCrypto, crypto: mockCrypto,
olmDevice: olmDevice, olmDevice: olmDevice,
baseApis: client, baseApis: client,
@ -360,33 +360,32 @@ describe("MegolmBackup", function() {
// @ts-ignore private field access // @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib; megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto() return client
.initCrypto()
.then(() => { .then(() => {
return client.crypto!.storeSessionBackupPrivateKey(new Uint8Array(32)); return client.crypto!.storeSessionBackupPrivateKey(new Uint8Array(32));
}) })
.then(() => { .then(() => {
return cryptoStore.doTxn( return cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => {
"readwrite", cryptoStore.addEndToEndInboundGroupSession(
[IndexedDBCryptoStore.STORE_SESSIONS], "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
(txn) => { groupSession.session_id(),
cryptoStore.addEndToEndInboundGroupSession( {
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", forwardingCurve25519KeyChain: undefined!,
groupSession.session_id(), keysClaimed: {
{ ed25519: "SENDER_ED25519",
forwardingCurve25519KeyChain: undefined!,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice.pickleKey),
}, },
txn); room_id: ROOM_ID,
}); session: ibGroupSession.pickle(olmDevice.pickleKey),
},
txn,
);
});
}) })
.then(async () => { .then(async () => {
await client.enableKeyBackup({ await client.enableKeyBackup({
algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2", algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2",
version: '1', version: "1",
auth_data: { auth_data: {
iv: "PsCAtR7gMc4xBd9YS3A9Ow", iv: "PsCAtR7gMc4xBd9YS3A9Ow",
mac: "ZSDsTFEZK7QzlauCLMleUcX96GQZZM7UNtk4sripSqQ", mac: "ZSDsTFEZK7QzlauCLMleUcX96GQZZM7UNtk4sripSqQ",
@ -394,9 +393,7 @@ describe("MegolmBackup", function() {
}); });
let numCalls = 0; let numCalls = 0;
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
client.http.authedRequest = function( client.http.authedRequest = function (method, path, queryParams, data, opts): any {
method, path, queryParams, data, opts,
): any {
++numCalls; ++numCalls;
expect(numCalls).toBeLessThanOrEqual(1); expect(numCalls).toBeLessThanOrEqual(1);
if (numCalls >= 2) { if (numCalls >= 2) {
@ -406,7 +403,7 @@ describe("MegolmBackup", function() {
} }
expect(method).toBe("PUT"); expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys"); 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).toBeDefined();
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty( expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
groupSession.session_id(), groupSession.session_id(),
@ -425,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(); const groupSession = new Olm.OutboundGroupSession();
groupSession.create(); groupSession.create();
const ibGroupSession = new Olm.InboundGroupSession(); const ibGroupSession = new Olm.InboundGroupSession();
@ -434,7 +431,7 @@ describe("MegolmBackup", function() {
const client = makeTestClient(cryptoStore); const client = makeTestClient(cryptoStore);
megolmDecryption = new MegolmDecryption({ megolmDecryption = new MegolmDecryption({
userId: '@user:id', userId: "@user:id",
crypto: mockCrypto, crypto: mockCrypto,
olmDevice: olmDevice, olmDevice: olmDevice,
baseApis: client, baseApis: client,
@ -445,16 +442,18 @@ describe("MegolmBackup", function() {
megolmDecryption.olmlib = mockOlmLib; megolmDecryption.olmlib = mockOlmLib;
await client.initCrypto(); await client.initCrypto();
client.uploadDeviceSigningKeys = async function(e) {return {};}; client.uploadDeviceSigningKeys = async function (e) {
client.uploadKeySignatures = async function(e) {return { failures: {} };}; return {};
};
client.uploadKeySignatures = async function (e) {
return { failures: {} };
};
await resetCrossSigningKeys(client); await resetCrossSigningKeys(client);
let numCalls = 0; let numCalls = 0;
await Promise.all([ await Promise.all([
new Promise<void>((resolve, reject) => { new Promise<void>((resolve, reject) => {
let backupInfo: Record<string, any> | BodyInit | undefined; let backupInfo: Record<string, any> | BodyInit | undefined;
client.http.authedRequest = function( client.http.authedRequest = function (method, path, queryParams, data, opts): any {
method, path, queryParams, data, opts,
): any {
++numCalls; ++numCalls;
expect(numCalls).toBeLessThanOrEqual(2); expect(numCalls).toBeLessThanOrEqual(2);
if (numCalls === 1) { if (numCalls === 1) {
@ -463,7 +462,9 @@ describe("MegolmBackup", function() {
try { try {
// make sure auth_data is signed by the master key // make sure auth_data is signed by the master key
olmlib.pkVerify( 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) { } catch (e) {
reject(e); reject(e);
@ -494,7 +495,7 @@ describe("MegolmBackup", function() {
client.stopClient(); client.stopClient();
}); });
it('retries when a backup fails', async function() { it("retries when a backup fails", async function () {
const groupSession = new Olm.OutboundGroupSession(); const groupSession = new Olm.OutboundGroupSession();
groupSession.create(); groupSession.create();
const ibGroupSession = new Olm.InboundGroupSession(); const ibGroupSession = new Olm.InboundGroupSession();
@ -517,7 +518,7 @@ describe("MegolmBackup", function() {
client.uploadKeysRequest = jest.fn(); client.uploadKeysRequest = jest.fn();
megolmDecryption = new MegolmDecryption({ megolmDecryption = new MegolmDecryption({
userId: '@user:id', userId: "@user:id",
crypto: mockCrypto, crypto: mockCrypto,
olmDevice: olmDevice, olmDevice: olmDevice,
baseApis: client, baseApis: client,
@ -528,27 +529,25 @@ describe("MegolmBackup", function() {
megolmDecryption.olmlib = mockOlmLib; megolmDecryption.olmlib = mockOlmLib;
await client.initCrypto(); await client.initCrypto();
await cryptoStore.doTxn( await cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => {
"readwrite", cryptoStore.addEndToEndInboundGroupSession(
[IndexedDBCryptoStore.STORE_SESSIONS], "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
(txn) => { groupSession.session_id(),
cryptoStore.addEndToEndInboundGroupSession( {
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", forwardingCurve25519KeyChain: undefined!,
groupSession.session_id(), keysClaimed: {
{ ed25519: "SENDER_ED25519",
forwardingCurve25519KeyChain: undefined!,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice.pickleKey),
}, },
txn); room_id: ROOM_ID,
}); session: ibGroupSession.pickle(olmDevice.pickleKey),
},
txn,
);
});
await client.enableKeyBackup({ await client.enableKeyBackup({
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
version: '1', version: "1",
auth_data: { auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
}, },
@ -556,9 +555,7 @@ describe("MegolmBackup", function() {
let numCalls = 0; let numCalls = 0;
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
client.http.authedRequest = function( client.http.authedRequest = function (method, path, queryParams, data, opts): any {
method, path, queryParams, data, opts,
): any {
++numCalls; ++numCalls;
expect(numCalls).toBeLessThanOrEqual(2); expect(numCalls).toBeLessThanOrEqual(2);
if (numCalls >= 3) { if (numCalls >= 3) {
@ -568,7 +565,7 @@ describe("MegolmBackup", function() {
} }
expect(method).toBe("PUT"); expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys"); 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).toBeDefined();
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty( expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
groupSession.session_id(), groupSession.session_id(),
@ -577,9 +574,7 @@ describe("MegolmBackup", function() {
resolve(); resolve();
return Promise.resolve({}); return Promise.resolve({});
} else { } else {
return Promise.reject( return Promise.reject(new Error("this is an expected failure"));
new Error("this is an expected failure"),
);
} }
}; };
return client.crypto!.backupManager.backupGroupSession( return client.crypto!.backupManager.backupGroupSession(
@ -592,14 +587,14 @@ describe("MegolmBackup", function() {
}); });
}); });
describe("restore", function() { describe("restore", function () {
let client: MatrixClient; let client: MatrixClient;
beforeEach(function() { beforeEach(function () {
client = makeTestClient(cryptoStore); client = makeTestClient(cryptoStore);
megolmDecryption = new MegolmDecryption({ megolmDecryption = new MegolmDecryption({
userId: '@user:id', userId: "@user:id",
crypto: mockCrypto, crypto: mockCrypto,
olmDevice: olmDevice, olmDevice: olmDevice,
baseApis: client, baseApis: client,
@ -612,46 +607,52 @@ describe("MegolmBackup", function() {
return client.initCrypto(); return client.initCrypto();
}); });
afterEach(function() { afterEach(function () {
client.stopClient(); client.stopClient();
}); });
it('can restore from backup (Curve25519 version)', function() { it("can restore from backup (Curve25519 version)", function () {
client.http.authedRequest = function() { client.http.authedRequest = function () {
return Promise.resolve<any>(CURVE25519_KEY_BACKUP_DATA); return Promise.resolve<any>(CURVE25519_KEY_BACKUP_DATA);
}; };
return client.restoreKeyBackupWithRecoveryKey( return client
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", .restoreKeyBackupWithRecoveryKey(
ROOM_ID, "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
SESSION_ID, ROOM_ID,
CURVE25519_BACKUP_INFO, SESSION_ID,
).then(() => { CURVE25519_BACKUP_INFO,
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT); )
}).then((res) => { .then(() => {
expect(res.clearEvent.content).toEqual('testytest'); return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
expect(res.untrusted).toBeTruthy(); // keys from Curve25519 backup are untrusted })
}); .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() { it("can restore from backup (AES-256 version)", function () {
client.http.authedRequest = function() { client.http.authedRequest = function () {
return Promise.resolve<any>(AES256_KEY_BACKUP_DATA); return Promise.resolve<any>(AES256_KEY_BACKUP_DATA);
}; };
return client.restoreKeyBackupWithRecoveryKey( return client
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", .restoreKeyBackupWithRecoveryKey(
ROOM_ID, "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
SESSION_ID, ROOM_ID,
AES256_BACKUP_INFO, SESSION_ID,
).then(() => { AES256_BACKUP_INFO,
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT); )
}).then((res) => { .then(() => {
expect(res.clearEvent.content).toEqual('testytest'); return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
expect(res.untrusted).toBeFalsy(); // keys from AES backup are trusted })
}); .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() { it("can restore backup by room (Curve25519 version)", function () {
client.http.authedRequest = function() { client.http.authedRequest = function () {
return Promise.resolve<any>({ return Promise.resolve<any>({
rooms: { rooms: {
[ROOM_ID]: { [ROOM_ID]: {
@ -662,27 +663,32 @@ describe("MegolmBackup", function() {
}, },
}); });
}; };
return client.restoreKeyBackupWithRecoveryKey( return client
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", .restoreKeyBackupWithRecoveryKey(
null!, null!, CURVE25519_BACKUP_INFO, "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
).then(() => { null!,
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT); null!,
}).then((res) => { CURVE25519_BACKUP_INFO,
expect(res.clearEvent.content).toEqual('testytest'); )
}); .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]); const key = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]);
await client.crypto!.storeSessionBackupPrivateKey(key); await client.crypto!.storeSessionBackupPrivateKey(key);
const result = await client.crypto!.getSessionBackupPrivateKey(); const result = await client.crypto!.getSessionBackupPrivateKey();
expect(new Uint8Array(result!)).toEqual(key); expect(new Uint8Array(result!)).toEqual(key);
}); });
it('caches session backup keys as it encounters them', async function() { it("caches session backup keys as it encounters them", async function () {
const cachedNull = await client.crypto!.getSessionBackupPrivateKey(); const cachedNull = await client.crypto!.getSessionBackupPrivateKey();
expect(cachedNull).toBeNull(); expect(cachedNull).toBeNull();
client.http.authedRequest = function() { client.http.authedRequest = function () {
return Promise.resolve<any>(CURVE25519_KEY_BACKUP_DATA); return Promise.resolve<any>(CURVE25519_KEY_BACKUP_DATA);
}; };
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
@ -698,20 +704,22 @@ describe("MegolmBackup", function() {
expect(cachedKey).not.toBeNull(); 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, { const BAD_BACKUP_INFO = Object.assign({}, CURVE25519_BACKUP_INFO, {
algorithm: "this.algorithm.does.not.exist", algorithm: "this.algorithm.does.not.exist",
}); });
client.http.authedRequest = function() { client.http.authedRequest = function () {
return Promise.resolve<any>(CURVE25519_KEY_BACKUP_DATA); return Promise.resolve<any>(CURVE25519_KEY_BACKUP_DATA);
}; };
await expect(client.restoreKeyBackupWithRecoveryKey( await expect(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", client.restoreKeyBackupWithRecoveryKey(
ROOM_ID, "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
SESSION_ID, ROOM_ID,
BAD_BACKUP_INFO, SESSION_ID,
)).rejects.toThrow(); BAD_BACKUP_INFO,
),
).rejects.toThrow();
}); });
}); });

View File

@ -15,18 +15,18 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import '../../olm-loader'; import "../../olm-loader";
import anotherjson from 'another-json'; import anotherjson from "another-json";
import { PkSigning } from '@matrix-org/olm'; import { PkSigning } from "@matrix-org/olm";
import HttpBackend from "matrix-mock-request"; import HttpBackend from "matrix-mock-request";
import * as olmlib from "../../../src/crypto/olmlib"; import * as olmlib from "../../../src/crypto/olmlib";
import { MatrixError } from '../../../src/http-api'; import { MatrixError } from "../../../src/http-api";
import { logger } from '../../../src/logger'; import { logger } from "../../../src/logger";
import { ICrossSigningKey, ICreateClientOpts, ISignedKey, MatrixClient } from '../../../src/client'; import { ICrossSigningKey, ICreateClientOpts, ISignedKey, MatrixClient } from "../../../src/client";
import { CryptoEvent, IBootstrapCrossSigningOpts } from '../../../src/crypto'; import { CryptoEvent, IBootstrapCrossSigningOpts } from "../../../src/crypto";
import { IDevice } from '../../../src/crypto/deviceinfo'; import { IDevice } from "../../../src/crypto/deviceinfo";
import { TestClient } from '../../TestClient'; import { TestClient } from "../../TestClient";
import { resetCrossSigningKeys } from "./crypto-utils"; import { resetCrossSigningKeys } from "./crypto-utils";
const PUSH_RULES_RESPONSE: Response = { const PUSH_RULES_RESPONSE: Response = {
@ -35,7 +35,7 @@ const PUSH_RULES_RESPONSE: Response = {
data: {}, data: {},
}; };
const filterResponse = function(userId: string): Response { const filterResponse = function (userId: string): Response {
const filterPath = "/user/" + encodeURIComponent(userId) + "/filter"; const filterPath = "/user/" + encodeURIComponent(userId) + "/filter";
return { return {
method: "POST", method: "POST",
@ -45,21 +45,19 @@ const filterResponse = function(userId: string): Response {
}; };
interface Response { interface Response {
method: 'GET' | 'PUT' | 'POST' | 'DELETE'; method: "GET" | "PUT" | "POST" | "DELETE";
path: string; path: string;
data: object; data: object;
} }
function setHttpResponses(httpBackend: HttpBackend, responses: Response[]) { function setHttpResponses(httpBackend: HttpBackend, responses: Response[]) {
responses.forEach(response => { responses.forEach((response) => {
httpBackend httpBackend.when(response.method, response.path).respond(200, response.data);
.when(response.method, response.path)
.respond(200, response.data);
}); });
} }
async function makeTestClient( async function makeTestClient(
userInfo: { userId: string, deviceId: string}, userInfo: { userId: string; deviceId: string },
options: Partial<ICreateClientOpts> = {}, options: Partial<ICreateClientOpts> = {},
keys: Record<string, Uint8Array> = {}, keys: Record<string, Uint8Array> = {},
) { ) {
@ -72,11 +70,11 @@ async function makeTestClient(
} }
options.cryptoCallbacks = Object.assign( options.cryptoCallbacks = Object.assign(
{}, { getCrossSigningKey, saveCrossSigningKeys }, options.cryptoCallbacks || {}, {},
); { getCrossSigningKey, saveCrossSigningKeys },
const testClient = new TestClient( options.cryptoCallbacks || {},
userInfo.userId, userInfo.deviceId, undefined, undefined, options,
); );
const testClient = new TestClient(userInfo.userId, userInfo.deviceId, undefined, undefined, options);
const client = testClient.client; const client = testClient.client;
await client.initCrypto(); await client.initCrypto();
@ -84,24 +82,25 @@ async function makeTestClient(
return { client, httpBackend: testClient.httpBackend }; return { client, httpBackend: testClient.httpBackend };
} }
describe("Cross Signing", function() { describe("Cross Signing", function () {
if (!global.Olm) { 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; return;
} }
beforeAll(function() { beforeAll(function () {
return global.Olm.init(); return global.Olm.init();
}); });
it("should sign the master key with the device key", async function() { it("should sign the master key with the device key", async function () {
const { client: alice } = await makeTestClient( const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = jest.fn().mockImplementation(async (auth, keys) => { alice.uploadDeviceSigningKeys = jest.fn().mockImplementation(async (auth, keys) => {
await olmlib.verifySignature( await olmlib.verifySignature(
alice.crypto!.olmDevice, keys.master_key, "@alice:example.com", alice.crypto!.olmDevice,
"Osborne2", alice.crypto!.olmDevice.deviceEd25519Key!, keys.master_key,
"@alice:example.com",
"Osborne2",
alice.crypto!.olmDevice.deviceEd25519Key!,
); );
}); });
alice.uploadKeySignatures = async () => ({ failures: {} }); alice.uploadKeySignatures = async () => ({ failures: {} });
@ -109,24 +108,22 @@ describe("Cross Signing", function() {
alice.getAccountDataFromServer = async <T>() => ({} as T); alice.getAccountDataFromServer = async <T>() => ({} as T);
// set Alice's cross-signing key // set Alice's cross-signing key
await alice.bootstrapCrossSigning({ await alice.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async func => { await func({}); }, authUploadDeviceSigningKeys: async (func) => {
await func({});
},
}); });
expect(alice.uploadDeviceSigningKeys).toHaveBeenCalled(); expect(alice.uploadDeviceSigningKeys).toHaveBeenCalled();
alice.stopClient(); alice.stopClient();
}); });
it("should abort bootstrap if device signing auth fails", async function() { it("should abort bootstrap if device signing auth fails", async function () {
const { client: alice } = await makeTestClient( const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async (auth, keys) => { alice.uploadDeviceSigningKeys = async (auth, keys) => {
const errorResponse = { const errorResponse = {
session: "sessionId", session: "sessionId",
flows: [ flows: [
{ {
stages: [ stages: ["m.login.password"],
"m.login.password",
],
}, },
], ],
params: {}, params: {},
@ -148,8 +145,8 @@ describe("Cross Signing", function() {
}; };
alice.uploadKeySignatures = async () => ({ failures: {} }); alice.uploadKeySignatures = async () => ({ failures: {} });
alice.setAccountData = async () => ({}); alice.setAccountData = async () => ({});
alice.getAccountDataFromServer = async <T extends {[k: string]: any}>(): Promise<T | null> => ({} as T); alice.getAccountDataFromServer = async <T extends { [k: string]: any }>(): Promise<T | null> => ({} as T);
const authUploadDeviceSigningKeys: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"] = async func => { const authUploadDeviceSigningKeys: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"] = async (func) => {
await func({}); await func({});
}; };
@ -169,10 +166,8 @@ describe("Cross Signing", function() {
alice.stopClient(); alice.stopClient();
}); });
it("should upload a signature when a user is verified", async function() { it("should upload a signature when a user is verified", async function () {
const { client: alice } = await makeTestClient( const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async () => ({}); alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} }); alice.uploadKeySignatures = async () => ({ failures: {} });
// set Alice's cross-signing key // set Alice's cross-signing key
@ -204,18 +199,14 @@ describe("Cross Signing", function() {
alice.stopClient(); 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([ const masterKey = new Uint8Array([
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82, 0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82, 0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef, 0xae, 0xb1,
0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef, 0x05, 0xc1, 0xe7, 0x62, 0x78, 0xa6, 0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
0xae, 0xb1, 0x05, 0xc1, 0xe7, 0x62, 0x78, 0xa6,
0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
]); ]);
const selfSigningKey = new Uint8Array([ const selfSigningKey = new Uint8Array([
0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66, 0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66, 0x6b, 0xf8, 0x93, 0xf5, 0xb0, 0x4d, 0x17, 0xc0, 0x17, 0xb5,
0x6b, 0xf8, 0x93, 0xf5, 0xb0, 0x4d, 0x17, 0xc0, 0xa5, 0xf6, 0x59, 0x11, 0x8b, 0x49, 0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f,
0x17, 0xb5, 0xa5, 0xf6, 0x59, 0x11, 0x8b, 0x49,
0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f,
]); ]);
const { client: alice, httpBackend } = await makeTestClient( const { client: alice, httpBackend } = await makeTestClient(
@ -223,8 +214,8 @@ describe("Cross Signing", function() {
{ {
cryptoCallbacks: { cryptoCallbacks: {
// will be called to sign our own device // will be called to sign our own device
getCrossSigningKey: async type => { getCrossSigningKey: async (type) => {
if (type === 'master') { if (type === "master") {
return masterKey; return masterKey;
} else { } else {
return selfSigningKey; return selfSigningKey;
@ -248,11 +239,10 @@ describe("Cross Signing", function() {
try { try {
await olmlib.verifySignature( await olmlib.verifySignature(
alice.crypto!.olmDevice, alice.crypto!.olmDevice,
content["@alice:example.com"][ content["@alice:example.com"]["nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"],
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
],
"@alice:example.com", "@alice:example.com",
"Osborne2", alice.crypto!.olmDevice.deviceEd25519Key!, "Osborne2",
alice.crypto!.olmDevice.deviceEd25519Key!,
); );
olmlib.pkVerify( olmlib.pkVerify(
content["@alice:example.com"]["Osborne2"], content["@alice:example.com"]["Osborne2"],
@ -267,8 +257,7 @@ describe("Cross Signing", function() {
}); });
// @ts-ignore private property // @ts-ignore private property
const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"] const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"].Osborne2;
.Osborne2;
const aliceDevice = { const aliceDevice = {
user_id: "@alice:example.com", user_id: "@alice:example.com",
device_id: "Osborne2", device_id: "Osborne2",
@ -276,12 +265,7 @@ describe("Cross Signing", function() {
algorithms: deviceInfo.algorithms, algorithms: deviceInfo.algorithms,
}; };
await alice.crypto!.signObject(aliceDevice); await alice.crypto!.signObject(aliceDevice);
olmlib.pkSign( olmlib.pkSign(aliceDevice as ISignedKey, selfSigningKey as unknown as PkSigning, "@alice:example.com", "");
aliceDevice as ISignedKey,
selfSigningKey as unknown as PkSigning,
"@alice:example.com",
'',
);
// feed sync result that includes master key, ssk, device key // feed sync result that includes master key, ssk, device key
const responses: Response[] = [ const responses: Response[] = [
@ -303,10 +287,7 @@ describe("Cross Signing", function() {
data: { data: {
next_batch: "abcdefg", next_batch: "abcdefg",
device_lists: { device_lists: {
changed: [ changed: ["@alice:example.com", "@bob:example.com"],
"@alice:example.com",
"@bob:example.com",
],
}, },
}, },
}, },
@ -314,35 +295,35 @@ describe("Cross Signing", function() {
method: "POST", method: "POST",
path: "/keys/query", path: "/keys/query",
data: { data: {
"failures": {}, failures: {},
"device_keys": { device_keys: {
"@alice:example.com": { "@alice:example.com": {
"Osborne2": aliceDevice, Osborne2: aliceDevice,
}, },
}, },
"master_keys": { master_keys: {
"@alice:example.com": { "@alice:example.com": {
user_id: "@alice:example.com", user_id: "@alice:example.com",
usage: ["master"], usage: ["master"],
keys: { keys: {
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk":
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk", "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk",
}, },
}, },
}, },
"self_signing_keys": { self_signing_keys: {
"@alice:example.com": { "@alice:example.com": {
user_id: "@alice:example.com", user_id: "@alice:example.com",
usage: ["self-signing"], usage: ["self-signing"],
keys: { keys: {
"ed25519:EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ": "ed25519:EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ":
"EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ", "EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ",
}, },
signatures: { signatures: {
"@alice:example.com": { "@alice:example.com": {
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk":
"Wqx/HXR851KIi8/u/UX+fbAMtq9Uj8sr8FsOcqrLfVYa6lAmbXs" "Wqx/HXR851KIi8/u/UX+fbAMtq9Uj8sr8FsOcqrLfVYa6lAmbXs" +
+ "Vhfy4AlZ3dnEtjgZx0U0QDrghEn2eYBeOCA", "Vhfy4AlZ3dnEtjgZx0U0QDrghEn2eYBeOCA",
}, },
}, },
}, },
@ -382,10 +363,8 @@ describe("Cross Signing", function() {
alice.stopClient(); alice.stopClient();
}); });
it("should use trust chain to determine device verification", async function() { it("should use trust chain to determine device verification", async function () {
const { client: alice } = await makeTestClient( const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async () => ({}); alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} }); alice.uploadKeySignatures = async () => ({ failures: {} });
// set Alice's cross-signing key // set Alice's cross-signing key
@ -472,7 +451,7 @@ describe("Cross Signing", function() {
alice.stopClient(); alice.stopClient();
}); });
it.skip("should trust signatures received from other devices", async function() { it.skip("should trust signatures received from other devices", async function () {
const aliceKeys: Record<string, Uint8Array> = {}; const aliceKeys: Record<string, Uint8Array> = {};
const { client: alice, httpBackend } = await makeTestClient( const { client: alice, httpBackend } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" }, { userId: "@alice:example.com", deviceId: "Osborne2" },
@ -488,10 +467,8 @@ describe("Cross Signing", function() {
await resetCrossSigningKeys(alice); await resetCrossSigningKeys(alice);
const selfSigningKey = new Uint8Array([ const selfSigningKey = new Uint8Array([
0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66, 0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66, 0x6b, 0xf8, 0x93, 0xf5, 0xb0, 0x4d, 0x17, 0xc0, 0x17, 0xb5,
0x6b, 0xf8, 0x93, 0xf5, 0xb0, 0x4d, 0x17, 0xc0, 0xa5, 0xf6, 0x59, 0x11, 0x8b, 0x49, 0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f,
0x17, 0xb5, 0xa5, 0xf6, 0x59, 0x11, 0x8b, 0x49,
0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f,
]); ]);
const keyChangePromise = new Promise<void>((resolve, reject) => { const keyChangePromise = new Promise<void>((resolve, reject) => {
@ -535,22 +512,16 @@ describe("Cross Signing", function() {
verified: 0, verified: 0,
known: false, known: false,
}; };
olmlib.pkSign( olmlib.pkSign(bobDevice, selfSigningKey as unknown as PkSigning, "@bob:example.com", "");
bobDevice,
selfSigningKey as unknown as PkSigning,
"@bob:example.com",
'',
);
const bobMaster: ICrossSigningKey = { const bobMaster: ICrossSigningKey = {
user_id: "@bob:example.com", user_id: "@bob:example.com",
usage: ["master"], usage: ["master"],
keys: { keys: {
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": "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 // Alice downloads Bob's keys
// - device key // - device key
@ -576,9 +547,7 @@ describe("Cross Signing", function() {
data: { data: {
next_batch: "abcdefg", next_batch: "abcdefg",
device_lists: { device_lists: {
changed: [ changed: ["@bob:example.com"],
"@bob:example.com",
],
}, },
}, },
}, },
@ -586,31 +555,31 @@ describe("Cross Signing", function() {
method: "POST", method: "POST",
path: "/keys/query", path: "/keys/query",
data: { data: {
"failures": {}, failures: {},
"device_keys": { device_keys: {
"@alice:example.com": { "@alice:example.com": {
"Osborne2": aliceDevice, Osborne2: aliceDevice,
}, },
"@bob:example.com": { "@bob:example.com": {
"Dynabook": bobDevice, Dynabook: bobDevice,
}, },
}, },
"master_keys": { master_keys: {
"@bob:example.com": bobMaster, "@bob:example.com": bobMaster,
}, },
"self_signing_keys": { self_signing_keys: {
"@bob:example.com": { "@bob:example.com": {
user_id: "@bob:example.com", user_id: "@bob:example.com",
usage: ["self-signing"], usage: ["self-signing"],
keys: { keys: {
"ed25519:EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ": "ed25519:EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ":
"EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ", "EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ",
}, },
signatures: { signatures: {
"@bob:example.com": { "@bob:example.com": {
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk":
"2KLiufImvEbfJuAFvsaZD+PsL8ELWl7N1u9yr/9hZvwRghBfQMB" "2KLiufImvEbfJuAFvsaZD+PsL8ELWl7N1u9yr/9hZvwRghBfQMB" +
+ "LAI86b1kDV9+Cq1lt85ykReeCEzmTEPY2BQ", "LAI86b1kDV9+Cq1lt85ykReeCEzmTEPY2BQ",
}, },
}, },
}, },
@ -646,10 +615,8 @@ describe("Cross Signing", function() {
alice.stopClient(); alice.stopClient();
}); });
it("should dis-trust an unsigned device", async function() { it("should dis-trust an unsigned device", async function () {
const { client: alice } = await makeTestClient( const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async () => ({}); alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} }); alice.uploadKeySignatures = async () => ({ failures: {} });
// set Alice's cross-signing key // set Alice's cross-signing key
@ -716,10 +683,8 @@ describe("Cross Signing", function() {
alice.stopClient(); alice.stopClient();
}); });
it("should dis-trust a user when their ssk changes", async function() { it("should dis-trust a user when their ssk changes", async function () {
const { client: alice } = await makeTestClient( const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async () => ({}); alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} }); alice.uploadKeySignatures = async () => ({ failures: {} });
await resetCrossSigningKeys(alice); await resetCrossSigningKeys(alice);
@ -860,7 +825,7 @@ describe("Cross Signing", function() {
alice.stopClient(); alice.stopClient();
}); });
it("should offer to upgrade device verifications to cross-signing", async function() { it("should offer to upgrade device verifications to cross-signing", async function () {
let upgradeResolveFunc: Function; let upgradeResolveFunc: Function;
const { client: alice } = await makeTestClient( const { client: alice } = await makeTestClient(
@ -874,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.uploadDeviceSigningKeys = async () => ({});
bob.uploadKeySignatures = async () => ({ failures: {} }); bob.uploadKeySignatures = async () => ({ failures: {} });
@ -895,10 +857,7 @@ describe("Cross Signing", function() {
known: true, known: true,
}, },
}); });
alice.crypto!.deviceList.storeCrossSigningForUser( alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", bob.crypto!.crossSigningInfo.toStorage());
"@bob:example.com",
bob.crypto!.crossSigningInfo.toStorage(),
);
alice.uploadDeviceSigningKeys = async () => ({}); alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} }); alice.uploadKeySignatures = async () => ({ failures: {} });
@ -917,8 +876,9 @@ describe("Cross Signing", function() {
expect(bobTrust.isTofu()).toBeTruthy(); expect(bobTrust.isTofu()).toBeTruthy();
// "forget" that Bob is trusted // "forget" that Bob is trusted
delete alice.crypto!.deviceList.crossSigningInfo["@bob:example.com"] delete alice.crypto!.deviceList.crossSigningInfo["@bob:example.com"].keys.master.signatures![
.keys.master.signatures!["@alice:example.com"]; "@alice:example.com"
];
const bobTrust2 = alice.checkUserTrust("@bob:example.com"); const bobTrust2 = alice.checkUserTrust("@bob:example.com");
expect(bobTrust2.isCrossSigningVerified()).toBeFalsy(); expect(bobTrust2.isCrossSigningVerified()).toBeFalsy();
@ -940,91 +900,83 @@ describe("Cross Signing", function() {
bob.stopClient(); bob.stopClient();
}); });
it( it("should observe that our own device is cross-signed, even if this device doesn't trust the key", async function () {
"should observe that our own device is cross-signed, even if this device doesn't trust the key", const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
async function() { alice.uploadDeviceSigningKeys = async () => ({});
const { client: alice } = await makeTestClient( alice.uploadKeySignatures = async () => ({ failures: {} });
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} });
// Generate Alice's SSK etc // Generate Alice's SSK etc
const aliceMasterSigning = new global.Olm.PkSigning(); const aliceMasterSigning = new global.Olm.PkSigning();
const aliceMasterPrivkey = aliceMasterSigning.generate_seed(); const aliceMasterPrivkey = aliceMasterSigning.generate_seed();
const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey); const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey);
const aliceSigning = new global.Olm.PkSigning(); const aliceSigning = new global.Olm.PkSigning();
const alicePrivkey = aliceSigning.generate_seed(); const alicePrivkey = aliceSigning.generate_seed();
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey); const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
const aliceSSK: ICrossSigningKey = { const aliceSSK: ICrossSigningKey = {
user_id: "@alice:example.com", user_id: "@alice:example.com",
usage: ["self_signing"], usage: ["self_signing"],
keys: { keys: {
["ed25519:" + alicePubkey]: alicePubkey, ["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,
},
}, },
}; self_signing: aliceSSK,
const sskSig = aliceMasterSigning.sign(anotherjson.stringify(aliceSSK)); },
aliceSSK.signatures = { 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": { "@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 // We don't trust the cross-signing keys yet...
alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", { expect(alice.checkDeviceTrust("@alice:example.com", aliceDeviceId).isCrossSigningVerified()).toBeFalsy();
keys: { // ... but we do acknowledge that the device is signed by them
master: { expect(alice.checkIfOwnDeviceCrossSigned(aliceDeviceId)).toBeTruthy();
user_id: "@alice:example.com", alice.stopClient();
usage: ["master"], });
keys: {
["ed25519:" + aliceMasterPubkey]: aliceMasterPubkey,
},
},
self_signing: aliceSSK,
},
firstUse: true,
crossSigningVerifiedBefore: false,
});
// Alice has a second device that's cross-signed it("should observe that our own device isn't cross-signed", async function () {
const aliceDeviceId = 'Dynabook'; const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
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" },
);
alice.uploadDeviceSigningKeys = async () => ({}); alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} }); alice.uploadKeySignatures = async () => ({ failures: {} });
@ -1084,9 +1036,7 @@ describe("Cross Signing", function() {
}); });
it("checkIfOwnDeviceCrossSigned should sanely handle unknown devices", async () => { it("checkIfOwnDeviceCrossSigned should sanely handle unknown devices", async () => {
const { client: alice } = await makeTestClient( const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async () => ({}); alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} }); alice.uploadKeySignatures = async () => ({ failures: {} });
@ -1138,7 +1088,7 @@ describe("Cross Signing", function() {
}); });
}); });
describe("userHasCrossSigningKeys", function() { describe("userHasCrossSigningKeys", function () {
if (!global.Olm) { if (!global.Olm) {
return; return;
} }
@ -1160,38 +1110,38 @@ describe("userHasCrossSigningKeys", function() {
}); });
it("should download devices and return true if one is a cross-signing key", async () => { it("should download devices and return true if one is a cross-signing key", async () => {
httpBackend httpBackend.when("POST", "/keys/query").respond(200, {
.when("POST", "/keys/query") master_keys: {
.respond(200, { "@alice:example.com": {
"master_keys": { user_id: "@alice:example.com",
"@alice:example.com": { usage: ["master"],
user_id: "@alice:example.com", keys: {
usage: ["master"], "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk":
keys: {
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk":
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk", "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk",
},
}, },
}, },
}); },
});
let result: boolean; let result: boolean;
await Promise.all([ await Promise.all([
httpBackend.flush("/keys/query"), httpBackend.flush("/keys/query"),
aliceClient.userHasCrossSigningKeys().then((res) => {result = res;}), aliceClient.userHasCrossSigningKeys().then((res) => {
result = res;
}),
]); ]);
expect(result!).toBeTruthy(); expect(result!).toBeTruthy();
}); });
it("should download devices and return false if there is no cross-signing key", async () => { it("should download devices and return false if there is no cross-signing key", async () => {
httpBackend httpBackend.when("POST", "/keys/query").respond(200, {});
.when("POST", "/keys/query")
.respond(200, {});
let result: boolean; let result: boolean;
await Promise.all([ await Promise.all([
httpBackend.flush("/keys/query"), httpBackend.flush("/keys/query"),
aliceClient.userHasCrossSigningKeys().then((res) => {result = res;}), aliceClient.userHasCrossSigningKeys().then((res) => {
result = res;
}),
]); ]);
expect(result!).toBeFalsy(); expect(result!).toBeFalsy();
}); });

View File

@ -1,6 +1,6 @@
import { IRecoveryKey } from '../../../src/crypto/api'; import { IRecoveryKey } from "../../../src/crypto/api";
import { CrossSigningLevel } from '../../../src/crypto/CrossSigning'; import { CrossSigningLevel } from "../../../src/crypto/CrossSigning";
import { IndexedDBCryptoStore } from '../../../src/crypto/store/indexeddb-crypto-store'; import { IndexedDBCryptoStore } from "../../../src/crypto/store/indexeddb-crypto-store";
import { MatrixClient } from "../../../src"; import { MatrixClient } from "../../../src";
import { CryptoEvent } from "../../../src/crypto"; import { CryptoEvent } from "../../../src/crypto";
@ -8,7 +8,7 @@ import { CryptoEvent } from "../../../src/crypto";
// but that is doing too much extra stuff for it to be an easy transition. // but that is doing too much extra stuff for it to be an easy transition.
export async function resetCrossSigningKeys( export async function resetCrossSigningKeys(
client: MatrixClient, client: MatrixClient,
{ level }: { level?: CrossSigningLevel} = {}, { level }: { level?: CrossSigningLevel } = {},
): Promise<void> { ): Promise<void> {
const crypto = client.crypto!; const crypto = client.crypto!;
@ -17,13 +17,9 @@ export async function resetCrossSigningKeys(
await crypto.crossSigningInfo.resetKeys(level); await crypto.crossSigningInfo.resetKeys(level);
await crypto.signObject(crypto.crossSigningInfo.keys.master); await crypto.signObject(crypto.crossSigningInfo.keys.master);
// write a copy locally so we know these are trusted keys // write a copy locally so we know these are trusted keys
await crypto.cryptoStore.doTxn( await crypto.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], crypto.cryptoStore.storeCrossSigningKeys(txn, crypto.crossSigningInfo.keys);
(txn) => { });
crypto.cryptoStore.storeCrossSigningKeys(
txn, crypto.crossSigningInfo.keys);
},
);
} catch (e) { } catch (e) {
// If anything failed here, revert the keys so we know to try again from the start // If anything failed here, revert the keys so we know to try again from the start
// next time. // next time.

View File

@ -14,33 +14,30 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import '../../olm-loader'; import "../../olm-loader";
import { TestClient } from '../../TestClient'; import { TestClient } from "../../TestClient";
import { logger } from '../../../src/logger'; import { logger } from "../../../src/logger";
import { DEHYDRATION_ALGORITHM } from '../../../src/crypto/dehydration'; import { DEHYDRATION_ALGORITHM } from "../../../src/crypto/dehydration";
const Olm = global.Olm; const Olm = global.Olm;
describe("Dehydration", () => { describe("Dehydration", () => {
if (!global.Olm) { if (!global.Olm) {
logger.warn('Not running dehydration unit tests: libolm not present'); logger.warn("Not running dehydration unit tests: libolm not present");
return; return;
} }
beforeAll(function() { beforeAll(function () {
return global.Olm.init(); return global.Olm.init();
}); });
it("should rehydrate a dehydrated device", async () => { it("should rehydrate a dehydrated device", async () => {
const key = new Uint8Array([1, 2, 3]); const key = new Uint8Array([1, 2, 3]);
const alice = new TestClient( const alice = new TestClient("@alice:example.com", "Osborne2", undefined, undefined, {
"@alice:example.com", "Osborne2", undefined, undefined, cryptoCallbacks: {
{ getDehydrationKey: async (t) => key,
cryptoCallbacks: {
getDehydrationKey: async t => key,
},
}, },
); });
const dehydratedDevice = new Olm.Account(); const dehydratedDevice = new Olm.Account();
dehydratedDevice.create(); dehydratedDevice.create();
@ -56,25 +53,20 @@ describe("Dehydration", () => {
success: true, success: true,
}); });
expect((await Promise.all([ expect((await Promise.all([alice.client.rehydrateDevice(), alice.httpBackend.flushAllExpected()]))[0]).toEqual(
alice.client.rehydrateDevice(), "ABCDEFG",
alice.httpBackend.flushAllExpected(), );
]))[0])
.toEqual("ABCDEFG");
expect(alice.client.getDeviceId()).toEqual("ABCDEFG"); expect(alice.client.getDeviceId()).toEqual("ABCDEFG");
}); });
it("should dehydrate a device", async () => { it("should dehydrate a device", async () => {
const key = new Uint8Array([1, 2, 3]); const key = new Uint8Array([1, 2, 3]);
const alice = new TestClient( const alice = new TestClient("@alice:example.com", "Osborne2", undefined, undefined, {
"@alice:example.com", "Osborne2", undefined, undefined, cryptoCallbacks: {
{ getDehydrationKey: async (t) => key,
cryptoCallbacks: {
getDehydrationKey: async t => key,
},
}, },
); });
await alice.client.initCrypto(); await alice.client.initCrypto();
@ -84,7 +76,8 @@ describe("Dehydration", () => {
let pickledAccount = ""; let pickledAccount = "";
alice.httpBackend.when("PUT", "/dehydrated_device") alice.httpBackend
.when("PUT", "/dehydrated_device")
.check((req) => { .check((req) => {
expect(req.data.device_data).toMatchObject({ expect(req.data.device_data).toMatchObject({
algorithm: DEHYDRATION_ALGORITHM, algorithm: DEHYDRATION_ALGORITHM,
@ -95,7 +88,8 @@ describe("Dehydration", () => {
.respond(200, { .respond(200, {
device_id: "ABCDEFG", device_id: "ABCDEFG",
}); });
alice.httpBackend.when("POST", "/keys/upload/ABCDEFG") alice.httpBackend
.when("POST", "/keys/upload/ABCDEFG")
.check((req) => { .check((req) => {
expect(req.data).toMatchObject({ expect(req.data).toMatchObject({
"device_keys": expect.objectContaining({ "device_keys": expect.objectContaining({
@ -119,11 +113,12 @@ describe("Dehydration", () => {
.respond(200, {}); .respond(200, {});
try { try {
const deviceId = const deviceId = (
(await Promise.all([ await Promise.all([
alice.client.createDehydratedDevice(new Uint8Array(key), {}), alice.client.createDehydratedDevice(new Uint8Array(key), {}),
alice.httpBackend.flushAllExpected(), alice.httpBackend.flushAllExpected(),
]))[0]; ])
)[0];
expect(deviceId).toEqual("ABCDEFG"); expect(deviceId).toEqual("ABCDEFG");
expect(deviceId).not.toEqual(""); expect(deviceId).not.toEqual("");

View File

@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { CryptoStore } from '../../../src/crypto/store/base'; import { CryptoStore } from "../../../src/crypto/store/base";
import { IndexedDBCryptoStore } from '../../../src/crypto/store/indexeddb-crypto-store'; import { IndexedDBCryptoStore } from "../../../src/crypto/store/indexeddb-crypto-store";
import { LocalStorageCryptoStore } from '../../../src/crypto/store/localStorage-crypto-store'; import { LocalStorageCryptoStore } from "../../../src/crypto/store/localStorage-crypto-store";
import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store'; import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store";
import { RoomKeyRequestState } from '../../../src/crypto/OutgoingRoomKeyRequestManager'; import { RoomKeyRequestState } from "../../../src/crypto/OutgoingRoomKeyRequestManager";
import 'fake-indexeddb/auto'; import "fake-indexeddb/auto";
import 'jest-localstorage-mock'; import "jest-localstorage-mock";
const requests = [ const requests = [
{ {
@ -46,54 +46,46 @@ const requests = [
requestId: "C", requestId: "C",
requestBody: { session_id: "C", room_id: "C", sender_key: "B", algorithm: "m.megolm.v1.aes-sha2" }, requestBody: { session_id: "C", room_id: "C", sender_key: "B", algorithm: "m.megolm.v1.aes-sha2" },
state: RoomKeyRequestState.Unsent, state: RoomKeyRequestState.Unsent,
recipients: [ recipients: [{ userId: "@becca:example.com", deviceId: "foobarbaz" }],
{ userId: "@becca:example.com", deviceId: "foobarbaz" },
],
}, },
]; ];
describe.each([ describe.each([
["IndexedDBCryptoStore", ["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(global.indexedDB, "tests")],
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
["LocalStorageCryptoStore", () => new LocalStorageCryptoStore(localStorage)], ["LocalStorageCryptoStore", () => new LocalStorageCryptoStore(localStorage)],
["MemoryCryptoStore", () => new MemoryCryptoStore()], ["MemoryCryptoStore", () => new MemoryCryptoStore()],
])("Outgoing room key requests [%s]", function(name, dbFactory) { ])("Outgoing room key requests [%s]", function (name, dbFactory) {
let store: CryptoStore; let store: CryptoStore;
beforeAll(async () => { beforeAll(async () => {
store = dbFactory(); store = dbFactory();
await store.startup(); await store.startup();
await Promise.all(requests.map((request) => await Promise.all(requests.map((request) => store.getOrAddOutgoingRoomKeyRequest(request)));
store.getOrAddOutgoingRoomKeyRequest(request),
));
}); });
it("getAllOutgoingRoomKeyRequestsByState retrieves all entries in a given state", it("getAllOutgoingRoomKeyRequestsByState retrieves all entries in a given state", async () => {
async () => { const r = await store.getAllOutgoingRoomKeyRequestsByState(RoomKeyRequestState.Sent);
const r = await expect(r).toHaveLength(2);
store.getAllOutgoingRoomKeyRequestsByState(RoomKeyRequestState.Sent); requests
expect(r).toHaveLength(2); .filter((e) => e.state === RoomKeyRequestState.Sent)
requests.filter((e) => e.state === RoomKeyRequestState.Sent).forEach((e) => { .forEach((e) => {
expect(r).toContainEqual(e); expect(r).toContainEqual(e);
}); });
}); });
it("getOutgoingRoomKeyRequestsByTarget retrieves all entries with a given target", it("getOutgoingRoomKeyRequestsByTarget retrieves all entries with a given target", async () => {
async () => { const r = await store.getOutgoingRoomKeyRequestsByTarget("@becca:example.com", "foobarbaz", [
const r = await store.getOutgoingRoomKeyRequestsByTarget( RoomKeyRequestState.Sent,
"@becca:example.com", "foobarbaz", [RoomKeyRequestState.Sent], ]);
); expect(r).toHaveLength(1);
expect(r).toHaveLength(1); expect(r[0]).toEqual(requests[0]);
expect(r[0]).toEqual(requests[0]); });
});
test("getOutgoingRoomKeyRequestByState retrieves any entry in a given state", test("getOutgoingRoomKeyRequestByState retrieves any entry in a given state", async () => {
async () => { const r = await store.getOutgoingRoomKeyRequestByState([RoomKeyRequestState.Sent]);
const r = expect(r).not.toBeNull();
await store.getOutgoingRoomKeyRequestByState([RoomKeyRequestState.Sent]); expect(r).not.toBeUndefined();
expect(r).not.toBeNull(); expect(r!.state).toEqual(RoomKeyRequestState.Sent);
expect(r).not.toBeUndefined(); expect(requests).toContainEqual(r);
expect(r!.state).toEqual(RoomKeyRequestState.Sent); });
expect(requests).toContainEqual(r);
});
}); });

View File

@ -14,65 +14,70 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import '../../olm-loader'; import "../../olm-loader";
import * as olmlib from "../../../src/crypto/olmlib"; import * as olmlib from "../../../src/crypto/olmlib";
import { IObject } from "../../../src/crypto/olmlib"; import { IObject } from "../../../src/crypto/olmlib";
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/crypto/SecretStorage"; import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/crypto/SecretStorage";
import { MatrixEvent } from "../../../src/models/event"; import { MatrixEvent } from "../../../src/models/event";
import { TestClient } from '../../TestClient'; import { TestClient } from "../../TestClient";
import { makeTestClients } from './verification/util'; import { makeTestClients } from "./verification/util";
import { encryptAES } from "../../../src/crypto/aes"; import { encryptAES } from "../../../src/crypto/aes";
import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils"; import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils";
import { logger } from '../../../src/logger'; import { logger } from "../../../src/logger";
import { ClientEvent, ICreateClientOpts, ICrossSigningKey, MatrixClient } from '../../../src/client'; import { ClientEvent, ICreateClientOpts, ICrossSigningKey, MatrixClient } from "../../../src/client";
import { ISecretStorageKeyInfo } from '../../../src/crypto/api'; import { ISecretStorageKeyInfo } from "../../../src/crypto/api";
import { DeviceInfo } from '../../../src/crypto/deviceinfo'; import { DeviceInfo } from "../../../src/crypto/deviceinfo";
import { ISignatures } from "../../../src/@types/signed"; import { ISignatures } from "../../../src/@types/signed";
import { ICurve25519AuthData } from "../../../src/crypto/keybackup"; import { ICurve25519AuthData } from "../../../src/crypto/keybackup";
async function makeTestClient(userInfo: { userId: string, deviceId: string}, options: Partial<ICreateClientOpts> = {}) { async function makeTestClient(
const client = (new TestClient( userInfo: { userId: string; deviceId: string },
userInfo.userId, userInfo.deviceId, undefined, undefined, options, options: Partial<ICreateClientOpts> = {},
)).client; ) {
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 // Make it seem as if we've synced and thus the store can be trusted to
// contain valid account data. // contain valid account data.
client.isInitialSyncComplete = function() { client.isInitialSyncComplete = function () {
return true; return true;
}; };
await client.initCrypto(); await client.initCrypto();
// No need to download keys for these tests // No need to download keys for these tests
jest.spyOn(client.crypto!, 'downloadKeys').mockResolvedValue({}); jest.spyOn(client.crypto!, "downloadKeys").mockResolvedValue({});
return client; return client;
} }
// Wrapper around pkSign to return a signed object. pkSign returns the // Wrapper around pkSign to return a signed object. pkSign returns the
// signature, rather than the signed object. // signature, rather than the signed object.
function sign<T extends IObject | ICurve25519AuthData>(obj: T, key: Uint8Array, userId: string): T & { function sign<T extends IObject | ICurve25519AuthData>(
obj: T,
key: Uint8Array,
userId: string,
): T & {
signatures: ISignatures; signatures: ISignatures;
unsigned?: object; unsigned?: object;
} { } {
olmlib.pkSign(obj, key, userId, ''); olmlib.pkSign(obj, key, userId, "");
return obj as T & { return obj as T & {
signatures: ISignatures; signatures: ISignatures;
unsigned?: object; unsigned?: object;
}; };
} }
describe("Secrets", function() { describe("Secrets", function () {
if (!global.Olm) { 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; return;
} }
beforeAll(function() { beforeAll(function () {
return global.Olm.init(); 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); const key = new Uint8Array(16);
for (let i = 0; i < 16; i++) key[i] = i; for (let i = 0; i < 16; i++) key[i] = i;
@ -82,22 +87,22 @@ describe("Secrets", function() {
const signingkeyInfo = { const signingkeyInfo = {
user_id: "@alice:example.com", user_id: "@alice:example.com",
usage: ['master'], usage: ["master"],
keys: { 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"]); expect(Object.keys(e.keys)).toEqual(["abc"]);
return ['abc', key]; return ["abc", key];
}); });
const alice = await makeTestClient( const alice = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" }, { userId: "@alice:example.com", deviceId: "Osborne2" },
{ {
cryptoCallbacks: { cryptoCallbacks: {
getCrossSigningKey: async t => signingKey, getCrossSigningKey: async (t) => signingKey,
getSecretStorageKey: getKey, getSecretStorageKey: getKey,
}, },
}, },
@ -108,21 +113,20 @@ describe("Secrets", function() {
const secretStorage = alice.crypto!.secretStorage; const secretStorage = alice.crypto!.secretStorage;
jest.spyOn(alice, 'setAccountData').mockImplementation( jest.spyOn(alice, "setAccountData").mockImplementation(async function (eventType, contents) {
async function(eventType, contents) { alice.store.storeAccountDataEvents([
alice.store.storeAccountDataEvents([ new MatrixEvent({
new MatrixEvent({ type: eventType,
type: eventType, content: contents,
content: contents, }),
}), ]);
]); return {};
return {}; });
});
const keyAccountData = { const keyAccountData = {
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES, algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
}; };
await alice.crypto!.crossSigningInfo.signObject(keyAccountData, 'master'); await alice.crypto!.crossSigningInfo.signObject(keyAccountData, "master");
alice.store.storeAccountDataEvents([ alice.store.storeAccountDataEvents([
new MatrixEvent({ new MatrixEvent({
@ -142,38 +146,32 @@ describe("Secrets", function() {
alice.stopClient(); alice.stopClient();
}); });
it("should throw if given a key that doesn't exist", async function() { it("should throw if given a key that doesn't exist", async function () {
const alice = await makeTestClient( const alice = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
try { try {
await alice.storeSecret("foo", "bar", ["this secret does not exist"]); await alice.storeSecret("foo", "bar", ["this secret does not exist"]);
// should be able to use expect(...).toThrow() but mocha still fails // 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 // the test even when it throws for reasons I have no inclination to debug
expect(true).toBeFalsy(); expect(true).toBeFalsy();
} catch (e) { } catch (e) {}
}
alice.stopClient(); alice.stopClient();
}); });
it("should refuse to encrypt with zero keys", async function() { it("should refuse to encrypt with zero keys", async function () {
const alice = await makeTestClient( const alice = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
try { try {
await alice.storeSecret("foo", "bar", []); await alice.storeSecret("foo", "bar", []);
expect(true).toBeFalsy(); expect(true).toBeFalsy();
} catch (e) { } catch (e) {}
}
alice.stopClient(); 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); const key = new Uint8Array(16);
for (let i = 0; i < 16; i++) key[i] = i; 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]); expect(Object.keys(e.keys)).toEqual([newKeyId]);
return [newKeyId, key]; return [newKeyId, key];
}); });
@ -183,13 +181,13 @@ describe("Secrets", function() {
{ userId: "@alice:example.com", deviceId: "Osborne2" }, { userId: "@alice:example.com", deviceId: "Osborne2" },
{ {
cryptoCallbacks: { cryptoCallbacks: {
getCrossSigningKey: t => Promise.resolve(keys[t]), getCrossSigningKey: (t) => Promise.resolve(keys[t]),
saveCrossSigningKeys: k => keys = k, saveCrossSigningKeys: (k) => (keys = k),
getSecretStorageKey: getKey, getSecretStorageKey: getKey,
}, },
}, },
); );
alice.setAccountData = async function(eventType, contents) { alice.setAccountData = async function (eventType, contents) {
alice.store.storeAccountDataEvents([ alice.store.storeAccountDataEvents([
new MatrixEvent({ new MatrixEvent({
type: eventType, type: eventType,
@ -200,33 +198,31 @@ describe("Secrets", function() {
}; };
resetCrossSigningKeys(alice); resetCrossSigningKeys(alice);
const { keyId: newKeyId } = await alice.addSecretStorageKey( const { keyId: newKeyId } = await alice.addSecretStorageKey(SECRET_STORAGE_ALGORITHM_V1_AES, {
SECRET_STORAGE_ALGORITHM_V1_AES, { pubkey: undefined, key: undefined }, pubkey: undefined,
); key: undefined,
});
// we don't await on this because it waits for the event to come down the sync // 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 // which won't happen in the test setup
alice.setDefaultSecretStorageKeyId(newKeyId); alice.setDefaultSecretStorageKeyId(newKeyId);
await alice.storeSecret("foo", "bar"); await alice.storeSecret("foo", "bar");
const accountData = alice.getAccountData('foo'); const accountData = alice.getAccountData("foo");
expect(accountData!.getContent().encrypted).toBeTruthy(); expect(accountData!.getContent().encrypted).toBeTruthy();
alice.stopClient(); alice.stopClient();
}); });
it("should refuse to encrypt if no keys given and no default key", async function() { it("should refuse to encrypt if no keys given and no default key", async function () {
const alice = await makeTestClient( const alice = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
try { try {
await alice.storeSecret("foo", "bar"); await alice.storeSecret("foo", "bar");
expect(true).toBeFalsy(); expect(true).toBeFalsy();
} catch (e) { } catch (e) {}
}
alice.stopClient(); 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( const [[osborne2, vax], clearTestClientTimeouts] = await makeTestClients(
[ [
{ userId: "@alice:example.com", deviceId: "Osborne2" }, { userId: "@alice:example.com", deviceId: "Osborne2" },
@ -247,7 +243,7 @@ describe("Secrets", function() {
const secretStorage = osborne2.client.crypto!.secretStorage; const secretStorage = osborne2.client.crypto!.secretStorage;
osborne2.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", { osborne2.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", {
"VAX": { VAX: {
known: false, known: false,
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: { keys: {
@ -258,7 +254,7 @@ describe("Secrets", function() {
}, },
}); });
vax.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", { vax.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", {
"Osborne2": { Osborne2: {
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
verified: 0, verified: 0,
known: false, known: false,
@ -289,30 +285,20 @@ describe("Secrets", function() {
clearTestClientTimeouts(); clearTestClientTimeouts();
}); });
describe("bootstrap", function() { describe("bootstrap", function () {
// keys used in some of the tests // keys used in some of the tests
const XSK = new Uint8Array( const XSK = new Uint8Array(olmlib.decodeBase64("3lo2YdJugHjfE+Or7KJ47NuKbhE7AAGLgQ/dc19913Q="));
olmlib.decodeBase64("3lo2YdJugHjfE+Or7KJ47NuKbhE7AAGLgQ/dc19913Q="),
);
const XSPubKey = "DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0"; const XSPubKey = "DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0";
const USK = new Uint8Array( const USK = new Uint8Array(olmlib.decodeBase64("lKWi3hJGUie5xxHgySoz8PHFnZv6wvNaud/p2shN9VU="));
olmlib.decodeBase64("lKWi3hJGUie5xxHgySoz8PHFnZv6wvNaud/p2shN9VU="),
);
const USPubKey = "CUpoiTtHiyXpUmd+3ohb7JVxAlUaOG1NYs9Jlx8soQU"; const USPubKey = "CUpoiTtHiyXpUmd+3ohb7JVxAlUaOG1NYs9Jlx8soQU";
const SSK = new Uint8Array( const SSK = new Uint8Array(olmlib.decodeBase64("1R6JVlXX99UcfUZzKuCDGQgJTw8ur1/ofgPD8pp+96M="));
olmlib.decodeBase64("1R6JVlXX99UcfUZzKuCDGQgJTw8ur1/ofgPD8pp+96M="),
);
const SSPubKey = "0DfNsRDzEvkCLA0gD3m7VAGJ5VClhjEsewI35xq873Q"; const SSPubKey = "0DfNsRDzEvkCLA0gD3m7VAGJ5VClhjEsewI35xq873Q";
const SSSSKey = new Uint8Array( const SSSSKey = new Uint8Array(olmlib.decodeBase64("XrmITOOdBhw6yY5Bh7trb/bgp1FRdIGyCUxxMP873R0="));
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); const key = new Uint8Array(16);
for (let i = 0; i < 16; i++) key[i] = i; 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]; return [Object.keys(e.keys)[0], key];
}); });
@ -329,20 +315,20 @@ describe("Secrets", function() {
); );
bob.uploadDeviceSigningKeys = async () => ({}); bob.uploadDeviceSigningKeys = async () => ({});
bob.uploadKeySignatures = jest.fn().mockResolvedValue(undefined); bob.uploadKeySignatures = jest.fn().mockResolvedValue(undefined);
bob.setAccountData = async function(eventType, contents) { bob.setAccountData = async function (eventType, contents) {
const event = new MatrixEvent({ const event = new MatrixEvent({
type: eventType, type: eventType,
content: contents, content: contents,
}); });
this.store.storeAccountDataEvents([ this.store.storeAccountDataEvents([event]);
event,
]);
this.emit(ClientEvent.AccountData, event); this.emit(ClientEvent.AccountData, event);
return {}; return {};
}; };
await bob.bootstrapCrossSigning({ await bob.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async func => { await func({}); }, authUploadDeviceSigningKeys: async (func) => {
await func({});
},
}); });
await bob.bootstrapSecretStorage({ await bob.bootstrapSecretStorage({
createSecretStorageKey, createSecretStorageKey,
@ -352,13 +338,12 @@ describe("Secrets", function() {
const secretStorage = bob.crypto!.secretStorage; const secretStorage = bob.crypto!.secretStorage;
expect(crossSigning.getId()).toBeTruthy(); expect(crossSigning.getId()).toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage)) expect(await crossSigning.isStoredInSecretStorage(secretStorage)).toBeTruthy();
.toBeTruthy();
expect(await secretStorage.hasKey()).toBeTruthy(); expect(await secretStorage.hasKey()).toBeTruthy();
bob.stopClient(); 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 decryption = new global.Olm.PkDecryption();
const storagePublicKey = decryption.generate_key(); const storagePublicKey = decryption.generate_key();
const storagePrivateKey = decryption.get_private_key(); const storagePrivateKey = decryption.get_private_key();
@ -370,7 +355,7 @@ describe("Secrets", function() {
}, },
{ {
cryptoCallbacks: { cryptoCallbacks: {
getSecretStorageKey: async request => { getSecretStorageKey: async (request) => {
const defaultKeyId = await bob.getDefaultSecretStorageKeyId(); const defaultKeyId = await bob.getDefaultSecretStorageKeyId();
expect(Object.keys(request.keys)).toEqual([defaultKeyId]); expect(Object.keys(request.keys)).toEqual([defaultKeyId]);
return [defaultKeyId!, storagePrivateKey]; return [defaultKeyId!, storagePrivateKey];
@ -381,14 +366,12 @@ describe("Secrets", function() {
bob.uploadDeviceSigningKeys = async () => ({}); bob.uploadDeviceSigningKeys = async () => ({});
bob.uploadKeySignatures = async () => ({ failures: {} }); bob.uploadKeySignatures = async () => ({ failures: {} });
bob.setAccountData = async function(eventType, contents) { bob.setAccountData = async function (eventType, contents) {
const event = new MatrixEvent({ const event = new MatrixEvent({
type: eventType, type: eventType,
content: contents, content: contents,
}); });
this.store.storeAccountDataEvents([ this.store.storeAccountDataEvents([event]);
event,
]);
this.emit(ClientEvent.AccountData, event); this.emit(ClientEvent.AccountData, event);
return {}; return {};
}; };
@ -399,7 +382,7 @@ describe("Secrets", function() {
// Set up cross-signing keys from scratch with specific storage key // Set up cross-signing keys from scratch with specific storage key
await bob.bootstrapCrossSigning({ await bob.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async func => { authUploadDeviceSigningKeys: async (func) => {
await func({}); await func({});
}, },
}); });
@ -412,25 +395,21 @@ describe("Secrets", function() {
}); });
// Clear local cross-signing keys and read from secret storage // Clear local cross-signing keys and read from secret storage
bob.crypto!.deviceList.storeCrossSigningForUser( bob.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", crossSigning.toStorage());
"@bob:example.com",
crossSigning.toStorage(),
);
crossSigning.keys = {}; crossSigning.keys = {};
await bob.bootstrapCrossSigning({ await bob.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async func => { authUploadDeviceSigningKeys: async (func) => {
await func({}); await func({});
}, },
}); });
expect(crossSigning.getId()).toBeTruthy(); expect(crossSigning.getId()).toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage)) expect(await crossSigning.isStoredInSecretStorage(secretStorage)).toBeTruthy();
.toBeTruthy();
expect(await secretStorage.hasKey()).toBeTruthy(); expect(await secretStorage.hasKey()).toBeTruthy();
bob.stopClient(); 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> = { let crossSigningKeys: Record<string, Uint8Array> = {
master: XSK, master: XSK,
user_signing: USK, user_signing: USK,
@ -443,8 +422,8 @@ describe("Secrets", function() {
{ userId: "@alice:example.com", deviceId: "Osborne2" }, { userId: "@alice:example.com", deviceId: "Osborne2" },
{ {
cryptoCallbacks: { cryptoCallbacks: {
getCrossSigningKey: async t => crossSigningKeys[t], getCrossSigningKey: async (t) => crossSigningKeys[t],
saveCrossSigningKeys: k => crossSigningKeys = k, saveCrossSigningKeys: (k) => (crossSigningKeys = k),
getSecretStorageKey: async ({ keys }, name) => { getSecretStorageKey: async ({ keys }, name) => {
for (const keyId of Object.keys(keys)) { for (const keyId of Object.keys(keys)) {
if (secretStorageKeys[keyId]) { if (secretStorageKeys[keyId]) {
@ -512,32 +491,44 @@ describe("Secrets", function() {
[`ed25519:${XSPubKey}`]: XSPubKey, [`ed25519:${XSPubKey}`]: XSPubKey,
}, },
}, },
self_signing: sign<ICrossSigningKey>({ self_signing: sign<ICrossSigningKey>(
user_id: "@alice:example.com", {
usage: ["self_signing"], user_id: "@alice:example.com",
keys: { usage: ["self_signing"],
[`ed25519:${SSPubKey}`]: SSPubKey, keys: {
[`ed25519:${SSPubKey}`]: SSPubKey,
},
}, },
}, XSK, "@alice:example.com"), XSK,
user_signing: sign<ICrossSigningKey>({ "@alice:example.com",
user_id: "@alice:example.com", ),
usage: ["user_signing"], user_signing: sign<ICrossSigningKey>(
keys: { {
[`ed25519:${USPubKey}`]: USPubKey, user_id: "@alice:example.com",
usage: ["user_signing"],
keys: {
[`ed25519:${USPubKey}`]: USPubKey,
},
}, },
}, XSK, "@alice:example.com"), XSK,
"@alice:example.com",
),
}, },
}); });
alice.getKeyBackupVersion = async () => { alice.getKeyBackupVersion = async () => {
return { return {
version: "1", version: "1",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2", algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
auth_data: sign({ auth_data: sign(
public_key: "pxEXhg+4vdMf/kFwP4bVawFWdb0EmytL3eFJx++zQ0A", {
}, XSK, "@alice:example.com"), 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({ const event = new MatrixEvent({
type: name, type: name,
content: data, content: data,
@ -549,11 +540,9 @@ describe("Secrets", function() {
await alice.bootstrapSecretStorage({}); await alice.bootstrapSecretStorage({});
expect(alice.getAccountData("m.secret_storage.default_key")!.getContent()) expect(alice.getAccountData("m.secret_storage.default_key")!.getContent()).toEqual({ key: "key_id" });
.toEqual({ key: "key_id" });
const keyInfo = alice.getAccountData("m.secret_storage.key.key_id")!.getContent<ISecretStorageKeyInfo>(); const keyInfo = alice.getAccountData("m.secret_storage.key.key_id")!.getContent<ISecretStorageKeyInfo>();
expect(keyInfo.algorithm) expect(keyInfo.algorithm).toEqual("m.secret_storage.v1.aes-hmac-sha2");
.toEqual("m.secret_storage.v1.aes-hmac-sha2");
expect(keyInfo.passphrase).toEqual({ expect(keyInfo.passphrase).toEqual({
algorithm: "m.pbkdf2", algorithm: "m.pbkdf2",
iterations: 500000, iterations: 500000,
@ -561,11 +550,10 @@ describe("Secrets", function() {
}); });
expect(keyInfo).toHaveProperty("iv"); expect(keyInfo).toHaveProperty("iv");
expect(keyInfo).toHaveProperty("mac"); expect(keyInfo).toHaveProperty("mac");
expect(alice.checkSecretStorageKey(secretStorageKeys.key_id, keyInfo)) expect(alice.checkSecretStorageKey(secretStorageKeys.key_id, keyInfo)).toBeTruthy();
.toBeTruthy();
alice.stopClient(); 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> = { let crossSigningKeys: Record<string, Uint8Array> = {
master: XSK, master: XSK,
user_signing: USK, user_signing: USK,
@ -578,8 +566,8 @@ describe("Secrets", function() {
{ userId: "@alice:example.com", deviceId: "Osborne2" }, { userId: "@alice:example.com", deviceId: "Osborne2" },
{ {
cryptoCallbacks: { cryptoCallbacks: {
getCrossSigningKey: async t => crossSigningKeys[t], getCrossSigningKey: async (t) => crossSigningKeys[t],
saveCrossSigningKeys: k => crossSigningKeys = k, saveCrossSigningKeys: (k) => (crossSigningKeys = k),
getSecretStorageKey: async ({ keys }, name) => { getSecretStorageKey: async ({ keys }, name) => {
for (const keyId of Object.keys(keys)) { for (const keyId of Object.keys(keys)) {
if (secretStorageKeys[keyId]) { if (secretStorageKeys[keyId]) {
@ -639,7 +627,8 @@ describe("Secrets", function() {
encrypted: { encrypted: {
key_id: await encryptAES( key_id: await encryptAES(
"123,45,6,7,89,1,234,56,78,90,12,34,5,67,8,90", "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",
), ),
}, },
}, },
@ -656,32 +645,44 @@ describe("Secrets", function() {
[`ed25519:${XSPubKey}`]: XSPubKey, [`ed25519:${XSPubKey}`]: XSPubKey,
}, },
}, },
self_signing: sign<ICrossSigningKey>({ self_signing: sign<ICrossSigningKey>(
user_id: "@alice:example.com", {
usage: ["self_signing"], user_id: "@alice:example.com",
keys: { usage: ["self_signing"],
[`ed25519:${SSPubKey}`]: SSPubKey, keys: {
[`ed25519:${SSPubKey}`]: SSPubKey,
},
}, },
}, XSK, "@alice:example.com"), XSK,
user_signing: sign<ICrossSigningKey>({ "@alice:example.com",
user_id: "@alice:example.com", ),
usage: ["user_signing"], user_signing: sign<ICrossSigningKey>(
keys: { {
[`ed25519:${USPubKey}`]: USPubKey, user_id: "@alice:example.com",
usage: ["user_signing"],
keys: {
[`ed25519:${USPubKey}`]: USPubKey,
},
}, },
}, XSK, "@alice:example.com"), XSK,
"@alice:example.com",
),
}, },
}); });
alice.getKeyBackupVersion = async () => { alice.getKeyBackupVersion = async () => {
return { return {
version: "1", version: "1",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2", algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
auth_data: sign({ auth_data: sign(
public_key: "pxEXhg+4vdMf/kFwP4bVawFWdb0EmytL3eFJx++zQ0A", {
}, XSK, "@alice:example.com"), 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({ const event = new MatrixEvent({
type: name, type: name,
content: data, content: data,
@ -695,8 +696,7 @@ describe("Secrets", function() {
const backupKey = alice.getAccountData("m.megolm_backup.v1")!.getContent(); const backupKey = alice.getAccountData("m.megolm_backup.v1")!.getContent();
expect(backupKey.encrypted).toHaveProperty("key_id"); expect(backupKey.encrypted).toHaveProperty("key_id");
expect(await alice.getSecret("m.megolm_backup.v1")) expect(await alice.getSecret("m.megolm_backup.v1")).toEqual("ey0GB1kB6jhOWgwiBUMIWg==");
.toEqual("ey0GB1kB6jhOWgwiBUMIWg==");
alice.stopClient(); alice.stopClient();
}); });
}); });

View File

@ -17,15 +17,17 @@ import { MatrixClient } from "../../../../src/client";
import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel"; import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel";
import { MatrixEvent } from "../../../../src/models/event"; import { MatrixEvent } from "../../../../src/models/event";
describe("InRoomChannel tests", function() { describe("InRoomChannel tests", function () {
const ALICE = "@alice:hs.tld"; const ALICE = "@alice:hs.tld";
const BOB = "@bob:hs.tld"; const BOB = "@bob:hs.tld";
const MALORY = "@malory:hs.tld"; const MALORY = "@malory:hs.tld";
const client = { const client = {
getUserId() { return ALICE; }, getUserId() {
return ALICE;
},
} as unknown as MatrixClient; } 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({ const invalidEvent = new MatrixEvent({
type: "m.key.verification.request", type: "m.key.verification.request",
}); });
@ -34,34 +36,29 @@ describe("InRoomChannel tests", function() {
type: "m.room.message", type: "m.room.message",
content: { msgtype: "m.key.verification.request" }, content: { msgtype: "m.key.verification.request" },
}); });
expect(InRoomChannel.getEventType(validEvent)). expect(InRoomChannel.getEventType(validEvent)).toStrictEqual("m.key.verification.request");
toStrictEqual("m.key.verification.request");
const validFooEvent = new MatrixEvent({ type: "m.foo" }); const validFooEvent = new MatrixEvent({ type: "m.foo" });
expect(InRoomChannel.getEventType(validFooEvent)). expect(InRoomChannel.getEventType(validFooEvent)).toStrictEqual("m.foo");
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({ const messageEvent = new MatrixEvent({
type: "m.room.message", type: "m.room.message",
content: { msgtype: "m.text" }, content: { msgtype: "m.text" },
}); });
// XXX: The event type doesn't matter too much, just as long as it's not a verification event // XXX: The event type doesn't matter too much, just as long as it's not a verification event
expect(InRoomChannel.getEventType(messageEvent)). expect(InRoomChannel.getEventType(messageEvent)).toStrictEqual("m.room.message");
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({ const event = new MatrixEvent({
type: "m.room.member", type: "m.room.member",
content: { }, content: {},
}); });
expect(InRoomChannel.getEventType(event)). expect(InRoomChannel.getEventType(event)).toStrictEqual("m.room.member");
toStrictEqual("m.room.member");
}); });
it("getOtherPartyUserId should not return anything for a request not " + it("getOtherPartyUserId should not return anything for a request not " + "directed at me", function () {
"directed at me", function() {
const event = new MatrixEvent({ const event = new MatrixEvent({
sender: BOB, sender: BOB,
type: "m.room.message", type: "m.room.message",
@ -70,29 +67,25 @@ describe("InRoomChannel tests", function() {
expect(InRoomChannel.getOtherPartyUserId(event, client)).toStrictEqual(undefined); expect(InRoomChannel.getOtherPartyUserId(event, client)).toStrictEqual(undefined);
}); });
it("getOtherPartyUserId should not return anything an event that is not of a valid " + it("getOtherPartyUserId should not return anything an event that is not of a valid " + "request type", function () {
"request type", function() {
// invalid because this should be a room message with msgtype // invalid because this should be a room message with msgtype
const invalidRequest = new MatrixEvent({ const invalidRequest = new MatrixEvent({
sender: BOB, sender: BOB,
type: "m.key.verification.request", type: "m.key.verification.request",
content: { to: ALICE }, content: { to: ALICE },
}); });
expect(InRoomChannel.getOtherPartyUserId(invalidRequest, client)) expect(InRoomChannel.getOtherPartyUserId(invalidRequest, client)).toStrictEqual(undefined);
.toStrictEqual(undefined);
const startEvent = new MatrixEvent({ const startEvent = new MatrixEvent({
sender: BOB, sender: BOB,
type: "m.key.verification.start", type: "m.key.verification.start",
content: { to: ALICE }, content: { to: ALICE },
}); });
expect(InRoomChannel.getOtherPartyUserId(startEvent, client)) expect(InRoomChannel.getOtherPartyUserId(startEvent, client)).toStrictEqual(undefined);
.toStrictEqual(undefined);
const fooEvent = new MatrixEvent({ const fooEvent = new MatrixEvent({
sender: BOB, sender: BOB,
type: "m.foo", type: "m.foo",
content: { to: ALICE }, content: { to: ALICE },
}); });
expect(InRoomChannel.getOtherPartyUserId(fooEvent, client)) expect(InRoomChannel.getOtherPartyUserId(fooEvent, client)).toStrictEqual(undefined);
.toStrictEqual(undefined);
}); });
}); });

View File

@ -19,13 +19,13 @@ import { logger } from "../../../../src/logger";
const Olm = global.Olm; const Olm = global.Olm;
describe("QR code verification", function() { describe("QR code verification", function () {
if (!global.Olm) { if (!global.Olm) {
logger.warn('Not running device verification tests: libolm not present'); logger.warn("Not running device verification tests: libolm not present");
return; return;
} }
beforeAll(function() { beforeAll(function () {
return Olm.init(); return Olm.init();
}); });

View File

@ -18,23 +18,23 @@ import "../../../olm-loader";
import { CryptoEvent, verificationMethods } from "../../../../src/crypto"; import { CryptoEvent, verificationMethods } from "../../../../src/crypto";
import { logger } from "../../../../src/logger"; import { logger } from "../../../../src/logger";
import { SAS } from "../../../../src/crypto/verification/SAS"; import { SAS } from "../../../../src/crypto/verification/SAS";
import { makeTestClients } from './util'; import { makeTestClients } from "./util";
const Olm = global.Olm; const Olm = global.Olm;
jest.useFakeTimers(); jest.useFakeTimers();
describe("verification request integration tests with crypto layer", function() { describe("verification request integration tests with crypto layer", function () {
if (!global.Olm) { 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; return;
} }
beforeAll(function() { beforeAll(function () {
return Olm.init(); 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( const [[alice, bob], clearTestClientTimeouts] = await makeTestClients(
[ [
{ userId: "@alice:example.com", deviceId: "Osborne2" }, { userId: "@alice:example.com", deviceId: "Osborne2" },
@ -44,7 +44,7 @@ describe("verification request integration tests with crypto layer", function()
verificationMethods: [verificationMethods.SAS], verificationMethods: [verificationMethods.SAS],
}, },
); );
alice.client.crypto!.deviceList.getRawStoredDevicesForUser = function() { alice.client.crypto!.deviceList.getRawStoredDevicesForUser = function () {
return { return {
Dynabook: { Dynabook: {
algorithms: [], algorithms: [],
@ -66,7 +66,7 @@ describe("verification request integration tests with crypto layer", function()
bobVerifier.endTimer(); bobVerifier.endTimer();
}); });
const aliceRequest = await alice.client.requestVerification("@bob:example.com"); 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; const aliceVerifier = aliceRequest.verifier;
expect(aliceVerifier).toBeInstanceOf(SAS); expect(aliceVerifier).toBeInstanceOf(SAS);

View File

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import "../../../olm-loader"; import "../../../olm-loader";
import { makeTestClients } from './util'; import { makeTestClients } from "./util";
import { MatrixEvent } from "../../../../src/models/event"; import { MatrixEvent } from "../../../../src/models/event";
import { ISasEvent, SAS, SasEvent } from "../../../../src/crypto/verification/SAS"; import { ISasEvent, SAS, SasEvent } from "../../../../src/crypto/verification/SAS";
import { DeviceInfo, IDevice } from "../../../../src/crypto/deviceinfo"; import { DeviceInfo, IDevice } from "../../../../src/crypto/deviceinfo";
@ -34,40 +34,42 @@ const Olm = global.Olm;
let ALICE_DEVICES: Record<string, IDevice>; let ALICE_DEVICES: Record<string, IDevice>;
let BOB_DEVICES: Record<string, IDevice>; let BOB_DEVICES: Record<string, IDevice>;
describe("SAS verification", function() { describe("SAS verification", function () {
if (!global.Olm) { 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; return;
} }
beforeAll(function() { beforeAll(function () {
return Olm.init(); 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 //channel, baseApis, userId, deviceId, startEvent, request
const request = { const request = {
onVerifierCancelled: function() {}, onVerifierCancelled: function () {},
} as VerificationRequest; } as VerificationRequest;
const channel = { const channel = {
send: function() { send: function () {
return Promise.resolve(); return Promise.resolve();
}, },
} as unknown as IVerificationChannel; } as unknown as IVerificationChannel;
const mockClient = {} as unknown as MatrixClient; 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); const sas = new SAS(channel, mockClient, "@alice:example.com", "ABCDEFG", event, request);
sas.handleEvent(new MatrixEvent({ sas.handleEvent(
sender: "@alice:example.com", new MatrixEvent({
type: "es.inquisition", sender: "@alice:example.com",
content: {}, type: "es.inquisition",
})); content: {},
}),
);
const spy = jest.fn(); const spy = jest.fn();
await sas.verify().catch(spy); await sas.verify().catch(spy);
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
// Cancel the SAS for cleanup (we started a verification, so abort) // Cancel the SAS for cleanup (we started a verification, so abort)
sas.cancel(new Error('error')); sas.cancel(new Error("error"));
}); });
describe("verification", () => { describe("verification", () => {
@ -117,16 +119,12 @@ describe("SAS verification", function() {
}, },
}; };
alice.client.crypto!.deviceList.storeDevicesForUser( alice.client.crypto!.deviceList.storeDevicesForUser("@bob:example.com", BOB_DEVICES);
"@bob:example.com", BOB_DEVICES,
);
alice.client.downloadKeys = () => { alice.client.downloadKeys = () => {
return Promise.resolve({}); return Promise.resolve({});
}; };
bob.client.crypto!.deviceList.storeDevicesForUser( bob.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", ALICE_DEVICES);
"@alice:example.com", ALICE_DEVICES,
);
bob.client.downloadKeys = () => { bob.client.downloadKeys = () => {
return Promise.resolve({}); return Promise.resolve({});
}; };
@ -135,7 +133,7 @@ describe("SAS verification", function() {
bobSasEvent = null; bobSasEvent = null;
bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => { bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => {
bob.client.on(CryptoEvent.VerificationRequest, request => { bob.client.on(CryptoEvent.VerificationRequest, (request) => {
(<SAS>request.verifier!).on(SasEvent.ShowSas, (e) => { (<SAS>request.verifier!).on(SasEvent.ShowSas, (e) => {
if (!e.sas.emoji || !e.sas.decimal) { if (!e.sas.emoji || !e.sas.decimal) {
e.cancel(); e.cancel();
@ -157,7 +155,9 @@ describe("SAS verification", function() {
}); });
aliceVerifier = alice.client.beginKeyVerification( aliceVerifier = alice.client.beginKeyVerification(
verificationMethods.SAS, bob.client.getUserId()!, bob.deviceId!, verificationMethods.SAS,
bob.client.getUserId()!,
bob.deviceId!,
) as SAS; ) as SAS;
aliceVerifier.on(SasEvent.ShowSas, (e) => { aliceVerifier.on(SasEvent.ShowSas, (e) => {
if (!e.sas.emoji || !e.sas.decimal) { if (!e.sas.emoji || !e.sas.decimal) {
@ -177,10 +177,7 @@ describe("SAS verification", function() {
}); });
}); });
afterEach(async () => { afterEach(async () => {
await Promise.all([ await Promise.all([alice.stop(), bob.stop()]);
alice.stop(),
bob.stop(),
]);
clearTestClientTimeouts(); clearTestClientTimeouts();
}); });
@ -189,23 +186,21 @@ describe("SAS verification", function() {
let macMethod; let macMethod;
let keyAgreement; let keyAgreement;
const origSendToDevice = bob.client.sendToDevice.bind(bob.client); 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") { if (type === "m.key.verification.accept") {
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!] macMethod = map[alice.client.getUserId()!][alice.client.deviceId!].message_authentication_code;
.message_authentication_code; keyAgreement = map[alice.client.getUserId()!][alice.client.deviceId!].key_agreement_protocol;
keyAgreement = map[alice.client.getUserId()!][alice.client.deviceId!]
.key_agreement_protocol;
} }
return origSendToDevice(type, map); return origSendToDevice(type, map);
}; };
alice.httpBackend.when('POST', '/keys/query').respond(200, { alice.httpBackend.when("POST", "/keys/query").respond(200, {
failures: {}, failures: {},
device_keys: { device_keys: {
"@bob:example.com": BOB_DEVICES, "@bob:example.com": BOB_DEVICES,
}, },
}); });
bob.httpBackend.when('POST', '/keys/query').respond(200, { bob.httpBackend.when("POST", "/keys/query").respond(200, {
failures: {}, failures: {},
device_keys: { device_keys: {
"@alice:example.com": ALICE_DEVICES, "@alice:example.com": ALICE_DEVICES,
@ -224,11 +219,9 @@ describe("SAS verification", function() {
expect(keyAgreement).toBe("curve25519-hkdf-sha256"); expect(keyAgreement).toBe("curve25519-hkdf-sha256");
// make sure Alice and Bob verified each other // make sure Alice and Bob verified each other
const bobDevice const bobDevice = await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
expect(bobDevice?.isVerified()).toBeTruthy(); expect(bobDevice?.isVerified()).toBeTruthy();
const aliceDevice const aliceDevice = await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
expect(aliceDevice?.isVerified()).toBeTruthy(); expect(aliceDevice?.isVerified()).toBeTruthy();
}); });
@ -244,27 +237,27 @@ describe("SAS verification", function() {
// has, since it is the same object. If this does not // has, since it is the same object. If this does not
// happen, the verification will fail due to a hash // happen, the verification will fail due to a hash
// commitment mismatch. // commitment mismatch.
map[bob.client.getUserId()!][bob.client.deviceId!] map[bob.client.getUserId()!][bob.client.deviceId!].message_authentication_codes = [
.message_authentication_codes = ['hkdf-hmac-sha256']; "hkdf-hmac-sha256",
];
} }
return aliceOrigSendToDevice(type, map); return aliceOrigSendToDevice(type, map);
}; };
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client); const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
bob.client.sendToDevice = (type, map) => { bob.client.sendToDevice = (type, map) => {
if (type === "m.key.verification.accept") { if (type === "m.key.verification.accept") {
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!] macMethod = map[alice.client.getUserId()!][alice.client.deviceId!].message_authentication_code;
.message_authentication_code;
} }
return bobOrigSendToDevice(type, map); return bobOrigSendToDevice(type, map);
}; };
alice.httpBackend.when('POST', '/keys/query').respond(200, { alice.httpBackend.when("POST", "/keys/query").respond(200, {
failures: {}, failures: {},
device_keys: { device_keys: {
"@bob:example.com": BOB_DEVICES, "@bob:example.com": BOB_DEVICES,
}, },
}); });
bob.httpBackend.when('POST', '/keys/query').respond(200, { bob.httpBackend.when("POST", "/keys/query").respond(200, {
failures: {}, failures: {},
device_keys: { device_keys: {
"@alice:example.com": ALICE_DEVICES, "@alice:example.com": ALICE_DEVICES,
@ -280,11 +273,9 @@ describe("SAS verification", function() {
expect(macMethod).toBe("hkdf-hmac-sha256"); expect(macMethod).toBe("hkdf-hmac-sha256");
const bobDevice const bobDevice = await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
expect(bobDevice!.isVerified()).toBeTruthy(); expect(bobDevice!.isVerified()).toBeTruthy();
const aliceDevice const aliceDevice = await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
expect(aliceDevice!.isVerified()).toBeTruthy(); expect(aliceDevice!.isVerified()).toBeTruthy();
}); });
@ -300,27 +291,25 @@ describe("SAS verification", function() {
// has, since it is the same object. If this does not // has, since it is the same object. If this does not
// happen, the verification will fail due to a hash // happen, the verification will fail due to a hash
// commitment mismatch. // commitment mismatch.
map[bob.client.getUserId()!][bob.client.deviceId!] map[bob.client.getUserId()!][bob.client.deviceId!].message_authentication_codes = ["hmac-sha256"];
.message_authentication_codes = ['hmac-sha256'];
} }
return aliceOrigSendToDevice(type, map); return aliceOrigSendToDevice(type, map);
}; };
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client); const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
bob.client.sendToDevice = (type, map) => { bob.client.sendToDevice = (type, map) => {
if (type === "m.key.verification.accept") { if (type === "m.key.verification.accept") {
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!] macMethod = map[alice.client.getUserId()!][alice.client.deviceId!].message_authentication_code;
.message_authentication_code;
} }
return bobOrigSendToDevice(type, map); return bobOrigSendToDevice(type, map);
}; };
alice.httpBackend.when('POST', '/keys/query').respond(200, { alice.httpBackend.when("POST", "/keys/query").respond(200, {
failures: {}, failures: {},
device_keys: { device_keys: {
"@bob:example.com": BOB_DEVICES, "@bob:example.com": BOB_DEVICES,
}, },
}); });
bob.httpBackend.when('POST', '/keys/query').respond(200, { bob.httpBackend.when("POST", "/keys/query").respond(200, {
failures: {}, failures: {},
device_keys: { device_keys: {
"@alice:example.com": ALICE_DEVICES, "@alice:example.com": ALICE_DEVICES,
@ -336,41 +325,33 @@ describe("SAS verification", function() {
expect(macMethod).toBe("hmac-sha256"); expect(macMethod).toBe("hmac-sha256");
const bobDevice const bobDevice = await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
expect(bobDevice?.isVerified()).toBeTruthy(); expect(bobDevice?.isVerified()).toBeTruthy();
const aliceDevice const aliceDevice = await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
expect(aliceDevice?.isVerified()).toBeTruthy(); expect(aliceDevice?.isVerified()).toBeTruthy();
}); });
it("should verify a cross-signing key", async () => { it("should verify a cross-signing key", async () => {
alice.httpBackend.when('POST', '/keys/device_signing/upload').respond( alice.httpBackend.when("POST", "/keys/device_signing/upload").respond(200, {});
200, {}, alice.httpBackend.when("POST", "/keys/signatures/upload").respond(200, {});
);
alice.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {});
alice.httpBackend.flush(undefined, 2); alice.httpBackend.flush(undefined, 2);
await resetCrossSigningKeys(alice.client); await resetCrossSigningKeys(alice.client);
bob.httpBackend.when('POST', '/keys/device_signing/upload').respond(200, {}); bob.httpBackend.when("POST", "/keys/device_signing/upload").respond(200, {});
bob.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {}); bob.httpBackend.when("POST", "/keys/signatures/upload").respond(200, {});
bob.httpBackend.flush(undefined, 2); bob.httpBackend.flush(undefined, 2);
await resetCrossSigningKeys(bob.client); await resetCrossSigningKeys(bob.client);
bob.client.crypto!.deviceList.storeCrossSigningForUser( bob.client.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
"@alice:example.com", { keys: alice.client.crypto!.crossSigningInfo.keys,
keys: alice.client.crypto!.crossSigningInfo.keys, crossSigningVerifiedBefore: false,
crossSigningVerifiedBefore: false, firstUse: true,
firstUse: true, });
},
);
const verifyProm = Promise.all([ const verifyProm = Promise.all([
aliceVerifier.verify(), aliceVerifier.verify(),
bobPromise.then((verifier) => { bobPromise.then((verifier) => {
bob.httpBackend.when( bob.httpBackend.when("POST", "/keys/signatures/upload").respond(200, {});
'POST', '/keys/signatures/upload',
).respond(200, {});
bob.httpBackend.flush(undefined, 1, 2000); bob.httpBackend.flush(undefined, 1, 2000);
return verifier.verify(); return verifier.verify();
}), }),
@ -378,9 +359,7 @@ describe("SAS verification", function() {
await verifyProm; await verifyProm;
const bobDeviceTrust = alice.client.checkDeviceTrust( const bobDeviceTrust = alice.client.checkDeviceTrust("@bob:example.com", "Dynabook");
"@bob:example.com", "Dynabook",
);
expect(bobDeviceTrust.isLocallyVerified()).toBeTruthy(); expect(bobDeviceTrust.isLocallyVerified()).toBeTruthy();
expect(bobDeviceTrust.isCrossSigningVerified()).toBeFalsy(); expect(bobDeviceTrust.isCrossSigningVerified()).toBeFalsy();
@ -388,15 +367,13 @@ describe("SAS verification", function() {
expect(aliceTrust.isCrossSigningVerified()).toBeTruthy(); expect(aliceTrust.isCrossSigningVerified()).toBeTruthy();
expect(aliceTrust.isTofu()).toBeTruthy(); expect(aliceTrust.isTofu()).toBeTruthy();
const aliceDeviceTrust = bob.client.checkDeviceTrust( const aliceDeviceTrust = bob.client.checkDeviceTrust("@alice:example.com", "Osborne2");
"@alice:example.com", "Osborne2",
);
expect(aliceDeviceTrust.isLocallyVerified()).toBeTruthy(); expect(aliceDeviceTrust.isLocallyVerified()).toBeTruthy();
expect(aliceDeviceTrust.isCrossSigningVerified()).toBeFalsy(); 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( const [[alice, bob], clearTestClientTimeouts] = await makeTestClients(
[ [
{ userId: "@alice:example.com", deviceId: "Osborne2" }, { userId: "@alice:example.com", deviceId: "Osborne2" },
@ -412,7 +389,7 @@ describe("SAS verification", function() {
bob.client.downloadKeys = jest.fn().mockResolvedValue({}); bob.client.downloadKeys = jest.fn().mockResolvedValue({});
const bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => { const bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => {
bob.client.on(CryptoEvent.VerificationRequest, request => { bob.client.on(CryptoEvent.VerificationRequest, (request) => {
(<SAS>request.verifier!).on(SasEvent.ShowSas, (e) => { (<SAS>request.verifier!).on(SasEvent.ShowSas, (e) => {
e.mismatch(); e.mismatch();
}); });
@ -421,7 +398,9 @@ describe("SAS verification", function() {
}); });
const aliceVerifier = alice.client.beginKeyVerification( 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(); const aliceSpy = jest.fn();
@ -432,17 +411,15 @@ describe("SAS verification", function() {
]); ]);
expect(aliceSpy).toHaveBeenCalled(); expect(aliceSpy).toHaveBeenCalled();
expect(bobSpy).toHaveBeenCalled(); expect(bobSpy).toHaveBeenCalled();
expect(alice.client.setDeviceVerified) expect(alice.client.setDeviceVerified).not.toHaveBeenCalled();
.not.toHaveBeenCalled(); expect(bob.client.setDeviceVerified).not.toHaveBeenCalled();
expect(bob.client.setDeviceVerified)
.not.toHaveBeenCalled();
alice.stop(); alice.stop();
bob.stop(); bob.stop();
clearTestClientTimeouts(); clearTestClientTimeouts();
}); });
describe("verification in DM", function() { describe("verification in DM", function () {
let alice: TestClient; let alice: TestClient;
let bob: TestClient; let bob: TestClient;
let aliceSasEvent: ISasEvent | null; let aliceSasEvent: ISasEvent | null;
@ -451,7 +428,7 @@ describe("SAS verification", function() {
let bobPromise: Promise<void>; let bobPromise: Promise<void>;
let clearTestClientTimeouts: Function; let clearTestClientTimeouts: Function;
beforeEach(async function() { beforeEach(async function () {
[[alice, bob], clearTestClientTimeouts] = await makeTestClients( [[alice, bob], clearTestClientTimeouts] = await makeTestClients(
[ [
{ userId: "@alice:example.com", deviceId: "Osborne2" }, { userId: "@alice:example.com", deviceId: "Osborne2" },
@ -526,7 +503,7 @@ describe("SAS verification", function() {
}); });
const aliceRequest = await alice.client.requestVerificationDM(bob.client.getUserId()!, "!room_id"); const aliceRequest = await alice.client.requestVerificationDM(bob.client.getUserId()!, "!room_id");
await aliceRequest.waitFor(r => r.started); await aliceRequest.waitFor((r) => r.started);
aliceVerifier = aliceRequest.verifier! as SAS; aliceVerifier = aliceRequest.verifier! as SAS;
aliceVerifier.on(SasEvent.ShowSas, (e) => { aliceVerifier.on(SasEvent.ShowSas, (e) => {
if (!e.sas.emoji || !e.sas.decimal) { if (!e.sas.emoji || !e.sas.decimal) {
@ -545,40 +522,32 @@ describe("SAS verification", function() {
} }
}); });
}); });
afterEach(async function() { afterEach(async function () {
await Promise.all([ await Promise.all([alice.stop(), bob.stop()]);
alice.stop(),
bob.stop(),
]);
clearTestClientTimeouts(); clearTestClientTimeouts();
}); });
it("should verify a key", async function() { it("should verify a key", async function () {
await Promise.all([ await Promise.all([aliceVerifier.verify(), bobPromise]);
aliceVerifier.verify(),
bobPromise,
]);
// make sure Alice and Bob verified each other // make sure Alice and Bob verified each other
expect(alice.client.crypto!.setDeviceVerification) expect(alice.client.crypto!.setDeviceVerification).toHaveBeenCalledWith(
.toHaveBeenCalledWith( bob.client.getUserId(),
bob.client.getUserId(), bob.client.deviceId,
bob.client.deviceId, true,
true, null,
null, null,
null, { "ed25519:Dynabook": "bob+base64+ed25519+key" },
{ "ed25519:Dynabook": "bob+base64+ed25519+key" }, );
); expect(bob.client.crypto!.setDeviceVerification).toHaveBeenCalledWith(
expect(bob.client.crypto!.setDeviceVerification) alice.client.getUserId(),
.toHaveBeenCalledWith( alice.client.deviceId,
alice.client.getUserId(), true,
alice.client.deviceId, null,
true, null,
null, { "ed25519:Osborne2": "alice+base64+ed25519+key" },
null, );
{ "ed25519:Osborne2": "alice+base64+ed25519+key" },
);
}); });
}); });
}); });

View File

@ -14,28 +14,26 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import '../../../olm-loader'; import "../../../olm-loader";
import { MatrixClient, MatrixEvent } from '../../../../src/matrix'; import { MatrixClient, MatrixEvent } from "../../../../src/matrix";
import { encodeBase64 } from "../../../../src/crypto/olmlib"; import { encodeBase64 } from "../../../../src/crypto/olmlib";
import "../../../../src/crypto"; // import this to cycle-break import "../../../../src/crypto"; // import this to cycle-break
import { CrossSigningInfo } from '../../../../src/crypto/CrossSigning'; import { CrossSigningInfo } from "../../../../src/crypto/CrossSigning";
import { VerificationRequest } from '../../../../src/crypto/verification/request/VerificationRequest'; import { VerificationRequest } from "../../../../src/crypto/verification/request/VerificationRequest";
import { IVerificationChannel } from '../../../../src/crypto/verification/request/Channel'; import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
import { VerificationBase } from '../../../../src/crypto/verification/Base'; import { VerificationBase } from "../../../../src/crypto/verification/Base";
jest.useFakeTimers(); jest.useFakeTimers();
// Private key for tests only // Private key for tests only
const testKey = new Uint8Array([ const testKey = new Uint8Array([
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82, 0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82, 0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef, 0xae, 0xb1, 0x05,
0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef, 0xc1, 0xe7, 0x62, 0x78, 0xa6, 0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
0xae, 0xb1, 0x05, 0xc1, 0xe7, 0x62, 0x78, 0xa6,
0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
]); ]);
const testKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"; const testKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
describe("self-verifications", () => { describe("self-verifications", () => {
beforeAll(function() { beforeAll(function () {
return global.Olm.init(); return global.Olm.init();
}); });
@ -47,26 +45,22 @@ describe("self-verifications", () => {
storeCrossSigningKeyCache: jest.fn(), storeCrossSigningKeyCache: jest.fn(),
}; };
const crossSigningInfo = new CrossSigningInfo( const crossSigningInfo = new CrossSigningInfo(userId, {}, cacheCallbacks);
userId,
{},
cacheCallbacks,
);
crossSigningInfo.keys = { crossSigningInfo.keys = {
master: { master: {
keys: { X: testKeyPub }, keys: { X: testKeyPub },
usage: [], usage: [],
user_id: 'user-id', user_id: "user-id",
}, },
self_signing: { self_signing: {
keys: { X: testKeyPub }, keys: { X: testKeyPub },
usage: [], usage: [],
user_id: 'user-id', user_id: "user-id",
}, },
user_signing: { user_signing: {
keys: { X: testKeyPub }, keys: { X: testKeyPub },
usage: [], usage: [],
user_id: 'user-id', user_id: "user-id",
}, },
}; };
@ -114,13 +108,10 @@ describe("self-verifications", () => {
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls.length).toBe(3); expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls.length).toBe(3);
expect(secretStorage.request.mock.calls.length).toBe(4); expect(secretStorage.request.mock.calls.length).toBe(4);
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[0][1]) expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[0][1]).toEqual(testKey);
.toEqual(testKey); expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[1][1]).toEqual(testKey);
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[1][1])
.toEqual(testKey);
expect(storeSessionBackupPrivateKey.mock.calls[0][0]) expect(storeSessionBackupPrivateKey.mock.calls[0][0]).toEqual(testKey);
.toEqual(testKey);
expect(restoreKeyBackupWithCache).toHaveBeenCalled(); expect(restoreKeyBackupWithCache).toHaveBeenCalled();

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import '../../../olm-loader'; import "../../../olm-loader";
import { CRYPTO_ENABLED, MatrixClient } from "../../../../src/client"; import { CRYPTO_ENABLED, MatrixClient } from "../../../../src/client";
import { TestClient } from "../../../TestClient"; import { TestClient } from "../../../TestClient";
@ -35,7 +35,7 @@ describe("crypto.setDeviceVerification", () => {
}); });
beforeEach(async () => { beforeEach(async () => {
client = (new TestClient(userId, deviceId1)).client; client = new TestClient(userId, deviceId1).client;
await client.initCrypto(); await client.initCrypto();
}); });
@ -46,11 +46,7 @@ describe("crypto.setDeviceVerification", () => {
describe("when setting an own device as verified", () => { describe("when setting an own device as verified", () => {
beforeEach(async () => { beforeEach(async () => {
jest.spyOn(client.crypto!, "cancelAndResendAllOutgoingKeyRequests"); jest.spyOn(client.crypto!, "cancelAndResendAllOutgoingKeyRequests");
await client.crypto!.setDeviceVerification( await client.crypto!.setDeviceVerification(userId, deviceId1, true);
userId,
deviceId1,
true,
);
}); });
it("cancelAndResendAllOutgoingKeyRequests should be called", () => { it("cancelAndResendAllOutgoingKeyRequests should be called", () => {

View File

@ -15,12 +15,12 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { TestClient } from '../../../TestClient'; import { TestClient } from "../../../TestClient";
import { IContent, MatrixEvent } from "../../../../src/models/event"; import { IContent, MatrixEvent } from "../../../../src/models/event";
import { IRoomTimelineData } from "../../../../src/models/event-timeline-set"; import { IRoomTimelineData } from "../../../../src/models/event-timeline-set";
import { Room, RoomEvent } from "../../../../src/models/room"; import { Room, RoomEvent } from "../../../../src/models/room";
import { logger } from '../../../../src/logger'; import { logger } from "../../../../src/logger";
import { MatrixClient, ClientEvent, ICreateClientOpts } from '../../../../src/client'; import { MatrixClient, ClientEvent, ICreateClientOpts } from "../../../../src/client";
interface UserInfo { interface UserInfo {
userId: string; userId: string;
@ -34,31 +34,31 @@ export async function makeTestClients(
const clients: TestClient[] = []; const clients: TestClient[] = [];
const timeouts: ReturnType<typeof setTimeout>[] = []; const timeouts: ReturnType<typeof setTimeout>[] = [];
const clientMap: Record<string, Record<string, MatrixClient>> = {}; const clientMap: Record<string, Record<string, MatrixClient>> = {};
const makeSendToDevice = (matrixClient: MatrixClient): MatrixClient['sendToDevice'] => async (type, map) => { const makeSendToDevice =
// logger.log(this.getUserId(), "sends", type, map); (matrixClient: MatrixClient): MatrixClient["sendToDevice"] =>
for (const [userId, devMap] of Object.entries(map)) { async (type, map) => {
if (userId in clientMap) { // logger.log(this.getUserId(), "sends", type, map);
for (const [deviceId, msg] of Object.entries(devMap)) { for (const [userId, devMap] of Object.entries(map)) {
if (deviceId in clientMap[userId]) { if (userId in clientMap) {
const event = new MatrixEvent({ for (const [deviceId, msg] of Object.entries(devMap)) {
sender: matrixClient.getUserId()!, if (deviceId in clientMap[userId]) {
type: type, const event = new MatrixEvent({
content: msg, sender: matrixClient.getUserId()!,
}); type: type,
const client = clientMap[userId][deviceId]; content: msg,
const decryptionPromise = event.isEncrypted() ? });
event.attemptDecryption(client.crypto!) : const client = clientMap[userId][deviceId];
Promise.resolve(); const decryptionPromise = event.isEncrypted()
? event.attemptDecryption(client.crypto!)
: Promise.resolve();
decryptionPromise.then( decryptionPromise.then(() => client.emit(ClientEvent.ToDeviceEvent, event));
() => client.emit(ClientEvent.ToDeviceEvent, event), }
);
} }
} }
} }
} return {};
return {}; };
};
const makeSendEvent = (matrixClient: MatrixClient) => (room: string, type: string, content: IContent) => { const makeSendEvent = (matrixClient: MatrixClient) => (room: string, type: string, content: IContent) => {
// make up a unique ID as the event ID // make up a unique ID as the event ID
const eventId = "$" + matrixClient.makeTxnId(); const eventId = "$" + matrixClient.makeTxnId();
@ -71,15 +71,17 @@ export async function makeTestClients(
origin_server_ts: Date.now(), origin_server_ts: Date.now(),
}; };
const event = new MatrixEvent(rawEvent); const event = new MatrixEvent(rawEvent);
const remoteEcho = new MatrixEvent(Object.assign({}, rawEvent, { const remoteEcho = new MatrixEvent(
unsigned: { Object.assign({}, rawEvent, {
transaction_id: matrixClient.makeTxnId(), unsigned: {
}, transaction_id: matrixClient.makeTxnId(),
})); },
}),
);
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
for (const tc of clients) { 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; const roomTimelineData = {} as unknown as IRoomTimelineData;
if (tc.client === matrixClient) { if (tc.client === matrixClient) {
logger.log("sending remote echo!!"); logger.log("sending remote echo!!");
@ -100,14 +102,13 @@ export async function makeTestClients(
if (!options) options = {}; if (!options) options = {};
if (!options.cryptoCallbacks) options.cryptoCallbacks = {}; if (!options.cryptoCallbacks) options.cryptoCallbacks = {};
if (!options.cryptoCallbacks.saveCrossSigningKeys) { if (!options.cryptoCallbacks.saveCrossSigningKeys) {
options.cryptoCallbacks.saveCrossSigningKeys = k => { keys = k; }; options.cryptoCallbacks.saveCrossSigningKeys = (k) => {
keys = k;
};
// @ts-ignore tsc getting confused by overloads // @ts-ignore tsc getting confused by overloads
options.cryptoCallbacks.getCrossSigningKey = typ => keys[typ]; options.cryptoCallbacks.getCrossSigningKey = (typ) => keys[typ];
} }
const testClient = new TestClient( const testClient = new TestClient(userInfo.userId, userInfo.deviceId, undefined, undefined, options);
userInfo.userId, userInfo.deviceId, undefined, undefined,
options,
);
if (!(userInfo.userId in clientMap)) { if (!(userInfo.userId in clientMap)) {
clientMap[userInfo.userId] = {}; clientMap[userInfo.userId] = {};
} }

View File

@ -13,11 +13,14 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { VerificationRequest, READY_TYPE, START_TYPE, DONE_TYPE } from import {
"../../../../src/crypto/verification/request/VerificationRequest"; VerificationRequest,
READY_TYPE,
START_TYPE,
DONE_TYPE,
} from "../../../../src/crypto/verification/request/VerificationRequest";
import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel"; import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel";
import { ToDeviceChannel } from import { ToDeviceChannel } from "../../../../src/crypto/verification/request/ToDeviceChannel";
"../../../../src/crypto/verification/request/ToDeviceChannel";
import { IContent, MatrixEvent } from "../../../../src/models/event"; import { IContent, MatrixEvent } from "../../../../src/models/event";
import { MatrixClient } from "../../../../src/client"; import { MatrixClient } from "../../../../src/client";
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel"; import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
@ -32,20 +35,26 @@ function makeMockClient(userId: string, deviceId: string): MockClient {
let events: MatrixEvent[] = []; let events: MatrixEvent[] = [];
const deviceEvents: Record<string, Record<string, MatrixEvent[]>> = {}; const deviceEvents: Record<string, Record<string, MatrixEvent[]>> = {};
return { return {
getUserId() { return userId; }, getUserId() {
getDeviceId() { return deviceId; }, return userId;
},
getDeviceId() {
return deviceId;
},
sendEvent(roomId: string, type: string, content: IContent) { sendEvent(roomId: string, type: string, content: IContent) {
counter = counter + 1; counter = counter + 1;
const eventId = `$${userId}-${deviceId}-${counter}`; const eventId = `$${userId}-${deviceId}-${counter}`;
events.push(new MatrixEvent({ events.push(
sender: userId, new MatrixEvent({
event_id: eventId, sender: userId,
room_id: roomId, event_id: eventId,
type, room_id: roomId,
content, type,
origin_server_ts: Date.now(), content,
})); origin_server_ts: Date.now(),
}),
);
return Promise.resolve({ event_id: eventId }); return Promise.resolve({ event_id: eventId });
}, },
@ -84,7 +93,7 @@ function makeMockClient(userId: string, deviceId: string): MockClient {
} }
const MOCK_METHOD = "mock-verify"; const MOCK_METHOD = "mock-verify";
class MockVerifier extends VerificationBase<'', any> { class MockVerifier extends VerificationBase<"", any> {
public _channel; public _channel;
public _startEvent; public _startEvent;
constructor( constructor(
@ -123,11 +132,13 @@ class MockVerifier extends VerificationBase<'', any> {
} }
function makeRemoteEcho(event: MatrixEvent) { function makeRemoteEcho(event: MatrixEvent) {
return new MatrixEvent(Object.assign({}, event.event, { return new MatrixEvent(
unsigned: { Object.assign({}, event.event, {
transaction_id: "abc", unsigned: {
}, transaction_id: "abc",
})); },
}),
);
} }
async function distributeEvent( async function distributeEvent(
@ -135,33 +146,26 @@ async function distributeEvent(
theirRequest: VerificationRequest, theirRequest: VerificationRequest,
event: MatrixEvent, event: MatrixEvent,
): Promise<void> { ): Promise<void> {
await ownRequest.channel.handleEvent( await ownRequest.channel.handleEvent(makeRemoteEcho(event), ownRequest, true);
makeRemoteEcho(event),
ownRequest,
true,
);
await theirRequest.channel.handleEvent(event, theirRequest, true); await theirRequest.channel.handleEvent(event, theirRequest, true);
} }
jest.useFakeTimers(); jest.useFakeTimers();
describe("verification request unit tests", function() { describe("verification request unit tests", function () {
it("transition from UNSENT to DONE through happy path", async function() { it("transition from UNSENT to DONE through happy path", async function () {
const alice = makeMockClient("@alice:matrix.tld", "device1"); const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1"); const bob = makeMockClient("@bob:matrix.tld", "device1");
const verificationMethods = new Map( const verificationMethods = new Map([[MOCK_METHOD, MockVerifier]]) as unknown as Map<
[[MOCK_METHOD, MockVerifier]], string,
) as unknown as Map<string, typeof VerificationBase>; typeof VerificationBase
>;
const aliceRequest = new VerificationRequest( const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob.getUserId()!), new InRoomChannel(alice, "!room", bob.getUserId()!),
verificationMethods, verificationMethods,
alice, alice,
); );
const bobRequest = new VerificationRequest( const bobRequest = new VerificationRequest(new InRoomChannel(bob, "!room"), verificationMethods, bob);
new InRoomChannel(bob, "!room"),
verificationMethods,
bob,
);
expect(aliceRequest.invalid).toBe(true); expect(aliceRequest.invalid).toBe(true);
expect(bobRequest.invalid).toBe(true); expect(bobRequest.invalid).toBe(true);
@ -199,23 +203,23 @@ describe("verification request unit tests", function() {
expect(bobRequest.done).toBe(true); 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 alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1"); const bob = makeMockClient("@bob:matrix.tld", "device1");
const aliceVerificationMethods = new Map( const aliceVerificationMethods = new Map([
[["c", function() {}], ["a", function() {}]], ["c", function () {}],
) as unknown as Map<string, typeof VerificationBase>; ["a", function () {}],
const bobVerificationMethods = new Map( ]) as unknown as Map<string, typeof VerificationBase>;
[["c", function() {}], ["b", function() {}]], const bobVerificationMethods = new Map([
) as unknown as Map<string, typeof VerificationBase>; ["c", function () {}],
["b", function () {}],
]) as unknown as Map<string, typeof VerificationBase>;
const aliceRequest = new VerificationRequest( const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob.getUserId()!), new InRoomChannel(alice, "!room", bob.getUserId()!),
aliceVerificationMethods, alice); aliceVerificationMethods,
const bobRequest = new VerificationRequest( alice,
new InRoomChannel(bob, "!room"),
bobVerificationMethods,
bob,
); );
const bobRequest = new VerificationRequest(new InRoomChannel(bob, "!room"), bobVerificationMethods, bob);
await aliceRequest.sendRequest(); await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents(); const [requestEvent] = alice.popEvents();
await distributeEvent(aliceRequest, bobRequest, requestEvent); await distributeEvent(aliceRequest, bobRequest, requestEvent);
@ -226,7 +230,7 @@ describe("verification request unit tests", function() {
expect(bobRequest.methods).toStrictEqual(["c"]); 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 alice = makeMockClient("@alice:matrix.tld", "device1");
const bob1 = makeMockClient("@bob:matrix.tld", "device1"); const bob1 = makeMockClient("@bob:matrix.tld", "device1");
const bob2 = makeMockClient("@bob:matrix.tld", "device2"); const bob2 = makeMockClient("@bob:matrix.tld", "device2");
@ -237,16 +241,8 @@ describe("verification request unit tests", function() {
); );
await aliceRequest.sendRequest(); await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents(); const [requestEvent] = alice.popEvents();
const bob1Request = new VerificationRequest( const bob1Request = new VerificationRequest(new InRoomChannel(bob1, "!room"), new Map(), bob1);
new InRoomChannel(bob1, "!room"), const bob2Request = new VerificationRequest(new InRoomChannel(bob2, "!room"), new Map(), bob2);
new Map(),
bob1,
);
const bob2Request = new VerificationRequest(
new InRoomChannel(bob2, "!room"),
new Map(),
bob2,
);
await bob1Request.channel.handleEvent(requestEvent, bob1Request, true); await bob1Request.channel.handleEvent(requestEvent, bob1Request, true);
await bob2Request.channel.handleEvent(requestEvent, bob2Request, true); await bob2Request.channel.handleEvent(requestEvent, bob2Request, true);
@ -258,12 +254,13 @@ describe("verification request unit tests", function() {
expect(bob2Request.observeOnly).toBe(true); 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 bob1 = makeMockClient("@bob:matrix.tld", "device1");
const bob2 = makeMockClient("@bob:matrix.tld", "device2"); const bob2 = makeMockClient("@bob:matrix.tld", "device2");
const verificationMethods = new Map( const verificationMethods = new Map([[MOCK_METHOD, MockVerifier]]) as unknown as Map<
[[MOCK_METHOD, MockVerifier]], string,
) as unknown as Map<string, typeof VerificationBase>; typeof VerificationBase
>;
const bob1Request = new VerificationRequest( const bob1Request = new VerificationRequest(
new ToDeviceChannel( new ToDeviceChannel(
bob1, bob1,
@ -300,7 +297,7 @@ describe("verification request unit tests", function() {
expect(bob2Request.done).toBe(true); 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 alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1"); const bob = makeMockClient("@bob:matrix.tld", "device1");
const aliceRequest = new VerificationRequest( const aliceRequest = new VerificationRequest(
@ -318,7 +315,7 @@ describe("verification request unit tests", function() {
expect(aliceRequest._cancellingUserId).toBe(alice.getUserId()); 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 alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1"); const bob = makeMockClient("@bob:matrix.tld", "device1");
const aliceRequest = new VerificationRequest( const aliceRequest = new VerificationRequest(
@ -328,11 +325,7 @@ describe("verification request unit tests", function() {
); );
await aliceRequest.sendRequest(); await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents(); const [requestEvent] = alice.popEvents();
const bobRequest = new VerificationRequest( const bobRequest = new VerificationRequest(new InRoomChannel(bob, "!room"), new Map(), bob);
new InRoomChannel(bob, "!room"),
new Map(),
bob,
);
await bobRequest.channel.handleEvent(requestEvent, bobRequest, true); await bobRequest.channel.handleEvent(requestEvent, bobRequest, true);

View File

@ -23,13 +23,7 @@ limitations under the License.
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import { MockedObject } from "jest-mock"; import { MockedObject } from "jest-mock";
import { import { WidgetApi, WidgetApiToWidgetAction, MatrixCapabilities, ITurnServer, IRoomEvent } from "matrix-widget-api";
WidgetApi,
WidgetApiToWidgetAction,
MatrixCapabilities,
ITurnServer,
IRoomEvent,
} from "matrix-widget-api";
import { createRoomWidgetClient, MsgType } from "../../src/matrix"; import { createRoomWidgetClient, MsgType } from "../../src/matrix";
import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "../../src/client"; import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "../../src/client";
@ -88,7 +82,9 @@ describe("RoomWidgetClient", () => {
expect(widgetApi.requestCapabilityToSendEvent).toHaveBeenCalledWith("org.matrix.rageshake_request"); expect(widgetApi.requestCapabilityToSendEvent).toHaveBeenCalledWith("org.matrix.rageshake_request");
await client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }); await client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 });
expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith( 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.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToReceiveEvent).toHaveBeenCalledWith("org.matrix.rageshake_request"); expect(widgetApi.requestCapabilityToReceiveEvent).toHaveBeenCalledWith("org.matrix.rageshake_request");
const emittedEvent = new Promise<MatrixEvent>(resolve => client.once(ClientEvent.Event, resolve)); const emittedEvent = new Promise<MatrixEvent>((resolve) => client.once(ClientEvent.Event, resolve));
const emittedSync = new Promise<SyncState>(resolve => client.once(ClientEvent.Sync, resolve)); const emittedSync = new Promise<SyncState>((resolve) => client.once(ClientEvent.Sync, resolve));
widgetApi.emit( widgetApi.emit(
`action:${WidgetApiToWidgetAction.SendEvent}`, `action:${WidgetApiToWidgetAction.SendEvent}`,
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }), 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 // It should've also inserted the event into the room object
const room = client.getRoom("!1:example.org"); const room = client.getRoom("!1:example.org");
expect(room).not.toBeNull(); 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"); expect(widgetApi.requestCapabilityToSendState).toHaveBeenCalledWith("org.example.foo", "bar");
await client.sendStateEvent("!1:example.org", "org.example.foo", { hello: "world" }, "bar"); await client.sendStateEvent("!1:example.org", "org.example.foo", { hello: "world" }, "bar");
expect(widgetApi.sendStateEvent).toHaveBeenCalledWith( 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.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar"); expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
const emittedEvent = new Promise<MatrixEvent>(resolve => client.once(ClientEvent.Event, resolve)); const emittedEvent = new Promise<MatrixEvent>((resolve) => client.once(ClientEvent.Event, resolve));
const emittedSync = new Promise<SyncState>(resolve => client.once(ClientEvent.Sync, resolve)); const emittedSync = new Promise<SyncState>((resolve) => client.once(ClientEvent.Sync, resolve));
widgetApi.emit( widgetApi.emit(
`action:${WidgetApiToWidgetAction.SendEvent}`, `action:${WidgetApiToWidgetAction.SendEvent}`,
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }), new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
@ -260,8 +264,8 @@ describe("RoomWidgetClient", () => {
content: { hello: "world" }, content: { hello: "world" },
}; };
const emittedEvent = new Promise<MatrixEvent>(resolve => client.once(ClientEvent.ToDeviceEvent, resolve)); const emittedEvent = new Promise<MatrixEvent>((resolve) => client.once(ClientEvent.ToDeviceEvent, resolve));
const emittedSync = new Promise<SyncState>(resolve => client.once(ClientEvent.Sync, resolve)); const emittedSync = new Promise<SyncState>((resolve) => client.once(ClientEvent.Sync, resolve));
widgetApi.emit( widgetApi.emit(
`action:${WidgetApiToWidgetAction.SendToDevice}`, `action:${WidgetApiToWidgetAction.SendToDevice}`,
new CustomEvent(`action:${WidgetApiToWidgetAction.SendToDevice}`, { detail: { data: event } }), new CustomEvent(`action:${WidgetApiToWidgetAction.SendToDevice}`, { detail: { data: event } }),
@ -308,7 +312,7 @@ describe("RoomWidgetClient", () => {
}; };
let emitServer2: () => void; 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* () { widgetApi.getTurnServers.mockImplementation(async function* () {
yield server1; yield server1;
yield await getServer2; yield await getServer2;
@ -321,7 +325,7 @@ describe("RoomWidgetClient", () => {
expect(client.getTurnServers()).toEqual([clientServer1]); expect(client.getTurnServers()).toEqual([clientServer1]);
// Subsequent servers arrive asynchronously and should emit an event // 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), client.once(ClientEvent.TurnServers, resolve),
); );
emitServer2!(); emitServer2!();

View File

@ -18,7 +18,7 @@ import { MatrixClient, MatrixEvent, MatrixEventEvent, MatrixScheduler, Room } fr
import { eventMapperFor } from "../../src/event-mapper"; import { eventMapperFor } from "../../src/event-mapper";
import { IStore } from "../../src/store"; import { IStore } from "../../src/store";
describe("eventMapperFor", function() { describe("eventMapperFor", function () {
let rooms: Room[] = []; let rooms: Room[] = [];
const userId = "@test:example.org"; const userId = "@test:example.org";
@ -29,10 +29,10 @@ describe("eventMapperFor", function() {
client = new MatrixClient({ client = new MatrixClient({
baseUrl: "https://my.home.server", baseUrl: "https://my.home.server",
accessToken: "my.access.token", accessToken: "my.access.token",
fetchFn: function() {} as any, // NOP fetchFn: function () {} as any, // NOP
store: { store: {
getRoom(roomId: string): Room | null { getRoom(roomId: string): Room | null {
return rooms.find(r => r.roomId === roomId) ?? null; return rooms.find((r) => r.roomId === roomId) ?? null;
}, },
} as IStore, } as IStore,
scheduler: { scheduler: {

View File

@ -25,12 +25,12 @@ import {
MatrixEvent, MatrixEvent,
MatrixEventEvent, MatrixEventEvent,
Room, Room,
} from '../../src'; } from "../../src";
import { Thread } from "../../src/models/thread"; import { Thread } from "../../src/models/thread";
import { ReEmitter } from "../../src/ReEmitter"; import { ReEmitter } from "../../src/ReEmitter";
describe('EventTimelineSet', () => { describe("EventTimelineSet", () => {
const roomId = '!foo:bar'; const roomId = "!foo:bar";
const userA = "@alice:bar"; const userA = "@alice:bar";
let room: Room; let room: Room;
@ -42,7 +42,7 @@ describe('EventTimelineSet', () => {
let replyEvent: MatrixEvent; let replyEvent: MatrixEvent;
const itShouldReturnTheRelatedEvents = () => { const itShouldReturnTheRelatedEvents = () => {
it('should return the related events', () => { it("should return the related events", () => {
eventTimelineSet.relations.aggregateChildEvent(messageEvent); eventTimelineSet.relations.aggregateChildEvent(messageEvent);
const relations = eventTimelineSet.relations.getChildEventsForEvent( const relations = eventTimelineSet.relations.getChildEventsForEvent(
messageEvent.getId()!, messageEvent.getId()!,
@ -55,45 +55,49 @@ describe('EventTimelineSet', () => {
}); });
}; };
const mkThreadResponse = (root: MatrixEvent) => utils.mkEvent({ const mkThreadResponse = (root: MatrixEvent) =>
event: true, utils.mkEvent(
type: EventType.RoomMessage, {
user: userA, event: true,
room: roomId, type: EventType.RoomMessage,
content: { user: userA,
"body": "Thread response :: " + Math.random(), room: roomId,
"m.relates_to": { content: {
"event_id": root.getId(), "body": "Thread response :: " + Math.random(),
"m.in_reply_to": { "m.relates_to": {
"event_id": root.getId(), "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(() => { beforeEach(() => {
client = utils.mock(MatrixClient, 'MatrixClient'); client = utils.mock(MatrixClient, "MatrixClient");
client.reEmitter = utils.mock(ReEmitter, 'ReEmitter'); client.reEmitter = utils.mock(ReEmitter, "ReEmitter");
room = new Room(roomId, client, userA); room = new Room(roomId, client, userA);
eventTimelineSet = new EventTimelineSet(room); eventTimelineSet = new EventTimelineSet(room);
eventTimeline = new EventTimeline(eventTimelineSet); eventTimeline = new EventTimeline(eventTimelineSet);
messageEvent = utils.mkMessage({ messageEvent = utils.mkMessage({
room: roomId, room: roomId,
user: userA, user: userA,
msg: 'Hi!', msg: "Hi!",
event: true, event: true,
}); });
replyEvent = utils.mkReplyMessage({ replyEvent = utils.mkReplyMessage({
room: roomId, room: roomId,
user: userA, user: userA,
msg: 'Hoo!', msg: "Hoo!",
event: true, event: true,
replyToMessage: messageEvent, replyToMessage: messageEvent,
}); });
}); });
describe('addLiveEvent', () => { describe("addLiveEvent", () => {
it("Adds event to the live timeline in the timeline set", () => { it("Adds event to the live timeline in the timeline set", () => {
const liveTimeline = eventTimelineSet.getLiveTimeline(); const liveTimeline = eventTimelineSet.getLiveTimeline();
expect(liveTimeline.getEvents().length).toStrictEqual(0); expect(liveTimeline.getEvents().length).toStrictEqual(0);
@ -111,7 +115,10 @@ describe('EventTimelineSet', () => {
// make a duplicate // make a duplicate
const duplicateMessageEvent = utils.mkMessage({ 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(); duplicateMessageEvent.event.event_id = messageEvent.getId();
@ -133,7 +140,7 @@ describe('EventTimelineSet', () => {
}); });
}); });
describe('addEventToTimeline', () => { describe("addEventToTimeline", () => {
let thread: Thread; let thread: Thread;
beforeEach(() => { beforeEach(() => {
@ -153,19 +160,10 @@ describe('EventTimelineSet', () => {
it("Make sure legacy overload passing options directly as parameters still works", () => { it("Make sure legacy overload passing options directly as parameters still works", () => {
const liveTimeline = eventTimelineSet.getLiveTimeline(); const liveTimeline = eventTimelineSet.getLiveTimeline();
expect(() => { expect(() => {
eventTimelineSet.addEventToTimeline( eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, true);
messageEvent,
liveTimeline,
true,
);
}).not.toThrow(); }).not.toThrow();
expect(() => { expect(() => {
eventTimelineSet.addEventToTimeline( eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, true, false);
messageEvent,
liveTimeline,
true,
false,
);
}).not.toThrow(); }).not.toThrow();
}); });
@ -204,8 +202,8 @@ describe('EventTimelineSet', () => {
expect(liveTimeline.getEvents().length).toStrictEqual(0); expect(liveTimeline.getEvents().length).toStrictEqual(0);
}); });
describe('non-room timeline', () => { describe("non-room timeline", () => {
it('Adds event to timeline', () => { it("Adds event to timeline", () => {
const nonRoomEventTimelineSet = new EventTimelineSet( const nonRoomEventTimelineSet = new EventTimelineSet(
// This is what we're specifically testing against, a timeline // This is what we're specifically testing against, a timeline
// without a `room` defined // without a `room` defined
@ -222,24 +220,16 @@ describe('EventTimelineSet', () => {
}); });
}); });
describe('aggregateRelations', () => { describe("aggregateRelations", () => {
describe('with unencrypted events', () => { describe("with unencrypted events", () => {
beforeEach(() => { beforeEach(() => {
eventTimelineSet.addEventsToTimeline( eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, eventTimeline, "foo");
[
messageEvent,
replyEvent,
],
true,
eventTimeline,
'foo',
);
}); });
itShouldReturnTheRelatedEvents(); itShouldReturnTheRelatedEvents();
}); });
describe('with events to be decrypted', () => { describe("with events to be decrypted", () => {
let messageEventShouldAttemptDecryptionSpy: jest.SpyInstance; let messageEventShouldAttemptDecryptionSpy: jest.SpyInstance;
let messageEventIsDecryptionFailureSpy: jest.SpyInstance; let messageEventIsDecryptionFailureSpy: jest.SpyInstance;
@ -247,26 +237,18 @@ describe('EventTimelineSet', () => {
let replyEventIsDecryptionFailureSpy: jest.SpyInstance; let replyEventIsDecryptionFailureSpy: jest.SpyInstance;
beforeEach(() => { beforeEach(() => {
messageEventShouldAttemptDecryptionSpy = jest.spyOn(messageEvent, 'shouldAttemptDecryption'); messageEventShouldAttemptDecryptionSpy = jest.spyOn(messageEvent, "shouldAttemptDecryption");
messageEventShouldAttemptDecryptionSpy.mockReturnValue(true); 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); replyEventShouldAttemptDecryptionSpy.mockReturnValue(true);
replyEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, 'isDecryptionFailure'); replyEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, "isDecryptionFailure");
eventTimelineSet.addEventsToTimeline( eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, eventTimeline, "foo");
[
messageEvent,
replyEvent,
],
true,
eventTimeline,
'foo',
);
}); });
it('should not return the related events', () => { it("should not return the related events", () => {
eventTimelineSet.relations.aggregateChildEvent(messageEvent); eventTimelineSet.relations.aggregateChildEvent(messageEvent);
const relations = eventTimelineSet.relations.getChildEventsForEvent( const relations = eventTimelineSet.relations.getChildEventsForEvent(
messageEvent.getId()!, messageEvent.getId()!,
@ -276,7 +258,7 @@ describe('EventTimelineSet', () => {
expect(relations).toBeUndefined(); expect(relations).toBeUndefined();
}); });
describe('after decryption', () => { describe("after decryption", () => {
beforeEach(() => { beforeEach(() => {
// simulate decryption failure once // simulate decryption failure once
messageEventIsDecryptionFailureSpy.mockReturnValue(true); messageEventIsDecryptionFailureSpy.mockReturnValue(true);
@ -302,22 +284,26 @@ describe('EventTimelineSet', () => {
}); });
describe("canContain", () => { describe("canContain", () => {
const mkThreadResponse = (root: MatrixEvent) => utils.mkEvent({ const mkThreadResponse = (root: MatrixEvent) =>
event: true, utils.mkEvent(
type: EventType.RoomMessage, {
user: userA, event: true,
room: roomId, type: EventType.RoomMessage,
content: { user: userA,
"body": "Thread response :: " + Math.random(), room: roomId,
"m.relates_to": { content: {
"event_id": root.getId(), "body": "Thread response :: " + Math.random(),
"m.in_reply_to": { "m.relates_to": {
"event_id": root.getId()!, "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; let thread: Thread;

View File

@ -1,4 +1,4 @@
import { mocked } from 'jest-mock'; import { mocked } from "jest-mock";
import * as utils from "../test-utils/test-utils"; import * as utils from "../test-utils/test-utils";
import { Direction, EventTimeline } from "../../src/models/event-timeline"; 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 { RoomMember } from "../../src/models/room-member";
import { EventTimelineSet } from "../../src/models/event-timeline-set"; import { EventTimelineSet } from "../../src/models/event-timeline-set";
describe("EventTimeline", function() { describe("EventTimeline", function () {
const roomId = "!foo:bar"; const roomId = "!foo:bar";
const userA = "@alice:bar"; const userA = "@alice:bar";
const userB = "@bertha:bar"; const userB = "@bertha:bar";
@ -19,7 +19,7 @@ describe("EventTimeline", function() {
const getTimeline = (): EventTimeline => { const getTimeline = (): EventTimeline => {
const room = new Room(roomId, mockClient, userA); const room = new Room(roomId, mockClient, userA);
const timelineSet = new EventTimelineSet(room); const timelineSet = new EventTimelineSet(room);
jest.spyOn(room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet); jest.spyOn(room, "getUnfilteredTimelineSet").mockReturnValue(timelineSet);
const timeline = new EventTimeline(timelineSet); const timeline = new EventTimeline(timelineSet);
// We manually stub the methods we'll be mocking out later instead of mocking the whole module // 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; return timeline;
}; };
beforeEach(function() { beforeEach(function () {
// reset any RoomState mocks // reset any RoomState mocks
jest.resetAllMocks(); jest.resetAllMocks();
timeline = getTimeline(); timeline = getTimeline();
}); });
describe("construction", function() { describe("construction", function () {
it("getRoomId should get room id", function() { it("getRoomId should get room id", function () {
const v = timeline.getRoomId(); const v = timeline.getRoomId();
expect(v).toEqual(roomId); expect(v).toEqual(roomId);
}); });
}); });
describe("initialiseState", function() { describe("initialiseState", function () {
it("should copy state events to start and end state", function() { it("should copy state events to start and end state", function () {
const events = [ const events = [
utils.mkMembership({ utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, room: roomId,
mship: "invite",
user: userB,
skey: userA,
event: true, event: true,
}), }),
utils.mkEvent({ utils.mkEvent({
type: "m.room.name", room: roomId, user: userB, type: "m.room.name",
room: roomId,
user: userB,
event: true, event: true,
content: { name: "New room" }, content: { name: "New room" },
}), }),
@ -61,49 +66,51 @@ describe("EventTimeline", function() {
timeline.initialiseState(events); timeline.initialiseState(events);
// @ts-ignore private prop // @ts-ignore private prop
const timelineStartState = timeline.startState!; const timelineStartState = timeline.startState!;
expect(mocked(timelineStartState).setStateEvents).toHaveBeenCalledWith( expect(mocked(timelineStartState).setStateEvents).toHaveBeenCalledWith(events, {
events, timelineWasEmpty: undefined,
{ timelineWasEmpty: undefined }, });
);
// @ts-ignore private prop // @ts-ignore private prop
const timelineEndState = timeline.endState!; const timelineEndState = timeline.endState!;
expect(mocked(timelineEndState).setStateEvents).toHaveBeenCalledWith( expect(mocked(timelineEndState).setStateEvents).toHaveBeenCalledWith(events, {
events, timelineWasEmpty: undefined,
{ timelineWasEmpty: undefined }, });
);
}); });
it("should raise an exception if called after events are added", function() { it("should raise an exception if called after events are added", function () {
const event = const event = utils.mkMessage({
utils.mkMessage({ room: roomId,
room: roomId, user: userA, msg: "Adam stole the plushies", user: userA,
event: true, msg: "Adam stole the plushies",
}); event: true,
});
const state = [ const state = [
utils.mkMembership({ utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, room: roomId,
mship: "invite",
user: userB,
skey: userA,
event: true, event: true,
}), }),
]; ];
expect(function() { expect(function () {
timeline.initialiseState(state); timeline.initialiseState(state);
}).not.toThrow(); }).not.toThrow();
timeline.addEvent(event, { toStartOfTimeline: false }); timeline.addEvent(event, { toStartOfTimeline: false });
expect(function() { expect(function () {
timeline.initialiseState(state); timeline.initialiseState(state);
}).toThrow(); }).toThrow();
}); });
}); });
describe("paginationTokens", function() { describe("paginationTokens", function () {
it("pagination tokens should start null", function() { it("pagination tokens should start null", function () {
expect(timeline.getPaginationToken(EventTimeline.BACKWARDS)).toBe(null); expect(timeline.getPaginationToken(EventTimeline.BACKWARDS)).toBe(null);
expect(timeline.getPaginationToken(EventTimeline.FORWARDS)).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("back", EventTimeline.BACKWARDS);
timeline.setPaginationToken("fwd", EventTimeline.FORWARDS); timeline.setPaginationToken("fwd", EventTimeline.FORWARDS);
expect(timeline.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("back"); expect(timeline.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("back");
@ -121,13 +128,13 @@ describe("EventTimeline", function() {
}); });
}); });
describe("neighbouringTimelines", function() { describe("neighbouringTimelines", function () {
it("neighbouring timelines should start null", function() { it("neighbouring timelines should start null", function () {
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(null); expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(null);
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS)).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 prev = getTimeline();
const next = getTimeline(); const next = getTimeline();
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS); timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
@ -136,42 +143,44 @@ describe("EventTimeline", function() {
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS)).toBe(next); 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 prev = getTimeline();
const next = getTimeline(); const next = getTimeline();
expect(function() { expect(function () {
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS); timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
}).not.toThrow(); }).not.toThrow();
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)) expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(prev);
.toBe(prev); expect(function () {
expect(function() {
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS); timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
}).toThrow(); }).toThrow();
expect(function() { expect(function () {
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS); timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
}).not.toThrow(); }).not.toThrow();
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS)) expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS)).toBe(next);
.toBe(next); expect(function () {
expect(function() {
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS); timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
}).toThrow(); }).toThrow();
}); });
}); });
describe("addEvent", function() { describe("addEvent", function () {
const events = [ const events = [
utils.mkMessage({ utils.mkMessage({
room: roomId, user: userA, msg: "hungry hungry hungry", room: roomId,
user: userA,
msg: "hungry hungry hungry",
event: true, event: true,
}), }),
utils.mkMessage({ utils.mkMessage({
room: roomId, user: userB, msg: "nom nom nom", room: roomId,
user: userB,
msg: "nom nom nom",
event: true, 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 }); timeline.addEvent(events[0], { toStartOfTimeline: false });
const initialIndex = timeline.getBaseIndex(); const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], { toStartOfTimeline: false }); timeline.addEvent(events[1], { toStartOfTimeline: false });
@ -181,7 +190,7 @@ describe("EventTimeline", function() {
expect(timeline.getEvents()[1]).toEqual(events[1]); 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 }); timeline.addEvent(events[0], { toStartOfTimeline: true });
const initialIndex = timeline.getBaseIndex(); const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], { toStartOfTimeline: true }); timeline.addEvent(events[1], { toStartOfTimeline: true });
@ -191,7 +200,7 @@ describe("EventTimeline", function() {
expect(timeline.getEvents()[1]).toEqual(events[0]); 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); const sentinel = new RoomMember(roomId, userA);
sentinel.name = "Alice"; sentinel.name = "Alice";
sentinel.membership = "join"; sentinel.membership = "join";
@ -200,27 +209,31 @@ describe("EventTimeline", function() {
sentinel.name = "Old Alice"; sentinel.name = "Old Alice";
sentinel.membership = "join"; sentinel.membership = "join";
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
.mockImplementation(function(uid) { if (uid === userA) {
if (uid === userA) { return sentinel;
return sentinel; }
} return null;
return null; });
}); mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember if (uid === userA) {
.mockImplementation(function(uid) { return oldSentinel;
if (uid === userA) { }
return oldSentinel; return null;
} });
return null;
});
const newEv = utils.mkEvent({ 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" }, content: { name: "New Room Name" },
}); });
const oldEv = utils.mkEvent({ 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" }, content: { name: "Old Room Name" },
}); });
@ -230,128 +243,159 @@ describe("EventTimeline", function() {
expect(oldEv.sender).toEqual(oldSentinel); expect(oldEv.sender).toEqual(oldSentinel);
}); });
it("should set event.target for new and old m.room.member events", it("should set event.target for new and old m.room.member events", function () {
function() { const sentinel = new RoomMember(roomId, userA);
const sentinel = new RoomMember(roomId, userA); sentinel.name = "Alice";
sentinel.name = "Alice"; sentinel.membership = "join";
sentinel.membership = "join";
const oldSentinel = new RoomMember(roomId, userA); const oldSentinel = new RoomMember(roomId, userA);
sentinel.name = "Old Alice"; sentinel.name = "Old Alice";
sentinel.membership = "join"; sentinel.membership = "join";
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
.mockImplementation(function(uid) { if (uid === userA) {
if (uid === userA) { return sentinel;
return sentinel; }
} return null;
return null; });
}); mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember if (uid === userA) {
.mockImplementation(function(uid) { return oldSentinel;
if (uid === userA) { }
return oldSentinel; return null;
}
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);
}); });
it("should call setStateEvents on the right RoomState with the right " + const newEv = utils.mkMembership({
"forwardLooking value for new events", function() { room: roomId,
const events = [ mship: "invite",
utils.mkMembership({ user: userB,
room: roomId, mship: "invite", user: userB, skey: userA, event: true, skey: userA,
}), event: true,
utils.mkEvent({ });
type: "m.room.name", room: roomId, user: userB, event: true, const oldEv = utils.mkMembership({
content: { room: roomId,
name: "New room", mship: "ban",
}, user: userB,
}), skey: userA,
]; event: true,
});
timeline.addEvent(events[0], { toStartOfTimeline: false }); timeline.addEvent(newEv, { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: false }); expect(newEv.target).toEqual(sentinel);
timeline.addEvent(oldEv, { toStartOfTimeline: true });
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents). expect(oldEv.target).toEqual(oldSentinel);
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();
}); });
it("should call setStateEvents on the right RoomState with the right " + it(
"forwardLooking value for old events", function() { "should call setStateEvents on the right RoomState with the right " + "forwardLooking value for new events",
const events = [ function () {
utils.mkMembership({ const events = [
room: roomId, mship: "invite", user: userB, skey: userA, event: true, utils.mkMembership({
}), room: roomId,
utils.mkEvent({ mship: "invite",
type: "m.room.name", room: roomId, user: userB, event: true, user: userB,
content: { skey: userA,
name: "New room", 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[0], { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: true }); timeline.addEvent(events[1], { toStartOfTimeline: false });
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents). expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).toHaveBeenCalledWith([events[0]], {
toHaveBeenCalledWith([events[0]], { timelineWasEmpty: undefined }); timelineWasEmpty: undefined,
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents). });
toHaveBeenCalledWith([events[1]], { timelineWasEmpty: undefined }); expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).toHaveBeenCalledWith([events[1]], {
timelineWasEmpty: undefined,
});
expect(events[0].forwardLooking).toBe(false); expect(events[0].forwardLooking).toBe(true);
expect(events[1].forwardLooking).toBe(false); expect(events[1].forwardLooking).toBe(true);
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents). expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).not.toHaveBeenCalled();
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", () => { it("Make sure legacy overload passing options directly as parameters still works", () => {
expect(() => timeline.addEvent(events[0], { toStartOfTimeline: true })).not.toThrow(); expect(() => timeline.addEvent(events[0], { toStartOfTimeline: true })).not.toThrow();
// @ts-ignore stateContext is not a valid param // @ts-ignore stateContext is not a valid param
expect(() => timeline.addEvent(events[0], { stateContext: new RoomState(roomId) })).not.toThrow(); expect(() => timeline.addEvent(events[0], { stateContext: new RoomState(roomId) })).not.toThrow();
expect(() => timeline.addEvent(events[0], expect(() =>
{ toStartOfTimeline: false, roomState: new RoomState(roomId) }, timeline.addEvent(events[0], { toStartOfTimeline: false, roomState: new RoomState(roomId) }),
)).not.toThrow(); ).not.toThrow();
}); });
}); });
describe("removeEvent", function() { describe("removeEvent", function () {
const events = [ const events = [
utils.mkMessage({ utils.mkMessage({
room: roomId, user: userA, msg: "hungry hungry hungry", room: roomId,
user: userA,
msg: "hungry hungry hungry",
event: true, event: true,
}), }),
utils.mkMessage({ utils.mkMessage({
room: roomId, user: userB, msg: "nom nom nom", room: roomId,
user: userB,
msg: "nom nom nom",
event: true, event: true,
}), }),
utils.mkMessage({ utils.mkMessage({
room: roomId, user: userB, msg: "piiie", room: roomId,
user: userB,
msg: "piiie",
event: true, event: true,
}), }),
]; ];
it("should remove events", function() { it("should remove events", function () {
timeline.addEvent(events[0], { toStartOfTimeline: false }); timeline.addEvent(events[0], { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: false }); timeline.addEvent(events[1], { toStartOfTimeline: false });
expect(timeline.getEvents().length).toEqual(2); expect(timeline.getEvents().length).toEqual(2);
@ -365,7 +409,7 @@ describe("EventTimeline", function() {
expect(timeline.getEvents().length).toEqual(0); 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[0], { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: true }); timeline.addEvent(events[1], { toStartOfTimeline: true });
timeline.addEvent(events[2], { toStartOfTimeline: false }); 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 // this is basically https://github.com/vector-im/vector-web/issues/937
// - removing the last event got baseIndex into such a state that // - removing the last event got baseIndex into such a state that
// further addEvent(ev, false) calls made the index increase. // further addEvent(ev, false) calls made the index increase.
it("should not make baseIndex assplode when removing the last event", it("should not make baseIndex assplode when removing the last event", function () {
function() { timeline.addEvent(events[0], { toStartOfTimeline: true });
timeline.addEvent(events[0], { toStartOfTimeline: true }); timeline.removeEvent(events[0].getId()!);
timeline.removeEvent(events[0].getId()!); const initialIndex = timeline.getBaseIndex();
const initialIndex = timeline.getBaseIndex(); timeline.addEvent(events[1], { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: false }); timeline.addEvent(events[2], { toStartOfTimeline: false });
timeline.addEvent(events[2], { toStartOfTimeline: false }); expect(timeline.getBaseIndex()).toEqual(initialIndex);
expect(timeline.getBaseIndex()).toEqual(initialIndex); expect(timeline.getEvents().length).toEqual(2);
expect(timeline.getEvents().length).toEqual(2); });
});
}); });
}); });

View File

@ -1,15 +1,15 @@
import { RelationType } from "../../src"; import { RelationType } from "../../src";
import { FilterComponent } from "../../src/filter-component"; 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("Filter Component", function () {
describe("types", function() { describe("types", function () {
it("should filter out events with other types", function() { it("should filter out events with other types", function () {
const filter = new FilterComponent({ types: ['m.room.message'] }); const filter = new FilterComponent({ types: ["m.room.message"] });
const event = mkEvent({ const event = mkEvent({
type: 'm.room.member', type: "m.room.member",
content: { }, content: {},
room: 'roomId', room: "roomId",
event: true, event: true,
}); });
@ -18,12 +18,12 @@ describe("Filter Component", function() {
expect(checkResult).toBe(false); expect(checkResult).toBe(false);
}); });
it("should validate events with the same type", function() { it("should validate events with the same type", function () {
const filter = new FilterComponent({ types: ['m.room.message'] }); const filter = new FilterComponent({ types: ["m.room.message"] });
const event = mkEvent({ const event = mkEvent({
type: 'm.room.message', type: "m.room.message",
content: { }, content: {},
room: 'roomId', room: "roomId",
event: true, event: true,
}); });
@ -32,17 +32,20 @@ describe("Filter Component", function() {
expect(checkResult).toBe(true); expect(checkResult).toBe(true);
}); });
it("should filter out events by relation participation", function() { it("should filter out events by relation participation", function () {
const currentUserId = '@me:server.org'; const currentUserId = "@me:server.org";
const filter = new FilterComponent({ const filter = new FilterComponent(
related_by_senders: [currentUserId], {
}, currentUserId); related_by_senders: [currentUserId],
},
currentUserId,
);
const threadRootNotParticipated = mkEvent({ const threadRootNotParticipated = mkEvent({
type: 'm.room.message', type: "m.room.message",
content: {}, content: {},
room: 'roomId', room: "roomId",
user: '@someone-else:server.org', user: "@someone-else:server.org",
event: true, event: true,
unsigned: { unsigned: {
"m.relations": { "m.relations": {
@ -57,14 +60,17 @@ describe("Filter Component", function() {
expect(filter.check(threadRootNotParticipated)).toBe(false); expect(filter.check(threadRootNotParticipated)).toBe(false);
}); });
it("should keep events by relation participation", function() { it("should keep events by relation participation", function () {
const currentUserId = '@me:server.org'; const currentUserId = "@me:server.org";
const filter = new FilterComponent({ const filter = new FilterComponent(
related_by_senders: [currentUserId], {
}, currentUserId); related_by_senders: [currentUserId],
},
currentUserId,
);
const threadRootParticipated = mkEvent({ const threadRootParticipated = mkEvent({
type: 'm.room.message', type: "m.room.message",
content: {}, content: {},
unsigned: { unsigned: {
"m.relations": { "m.relations": {
@ -74,23 +80,23 @@ describe("Filter Component", function() {
}, },
}, },
}, },
user: '@someone-else:server.org', user: "@someone-else:server.org",
room: 'roomId', room: "roomId",
event: true, event: true,
}); });
expect(filter.check(threadRootParticipated)).toBe(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({ const filter = new FilterComponent({
related_by_rel_types: ["m.thread"], related_by_rel_types: ["m.thread"],
}); });
const referenceRelationEvent = mkEvent({ const referenceRelationEvent = mkEvent({
type: 'm.room.message', type: "m.room.message",
content: {}, content: {},
room: 'roomId', room: "roomId",
event: true, event: true,
unsigned: { unsigned: {
"m.relations": { "m.relations": {
@ -102,13 +108,13 @@ describe("Filter Component", function() {
expect(filter.check(referenceRelationEvent)).toBe(false); 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({ const filter = new FilterComponent({
related_by_rel_types: ["m.thread"], related_by_rel_types: ["m.thread"],
}); });
const threadRootEvent = mkEvent({ const threadRootEvent = mkEvent({
type: 'm.room.message', type: "m.room.message",
content: {}, content: {},
unsigned: { unsigned: {
"m.relations": { "m.relations": {
@ -118,22 +124,22 @@ describe("Filter Component", function() {
}, },
}, },
}, },
room: 'roomId', room: "roomId",
event: true, event: true,
}); });
const eventWithMultipleRelations = mkEvent({ const eventWithMultipleRelations = mkEvent({
"type": "m.room.message", type: "m.room.message",
"content": {}, content: {},
"unsigned": { unsigned: {
"m.relations": { "m.relations": {
"testtesttest": {}, "testtesttest": {},
"m.annotation": { "m.annotation": {
"chunk": [ chunk: [
{ {
"type": "m.reaction", type: "m.reaction",
"key": "🤫", key: "🤫",
"count": 1, count: 1,
}, },
], ],
}, },
@ -143,20 +149,20 @@ describe("Filter Component", function() {
}, },
}, },
}, },
"room": 'roomId', room: "roomId",
"event": true, event: true,
}); });
const noMatchEvent = mkEvent({ const noMatchEvent = mkEvent({
"type": "m.room.message", type: "m.room.message",
"content": {}, content: {},
"unsigned": { unsigned: {
"m.relations": { "m.relations": {
"testtesttest": {}, testtesttest: {},
}, },
}, },
"room": 'roomId', room: "roomId",
"event": true, event: true,
}); });
expect(filter.check(threadRootEvent)).toBe(true); expect(filter.check(threadRootEvent)).toBe(true);

View File

@ -19,17 +19,17 @@ import { Filter, IFilterDefinition } from "../../src/filter";
import { mkEvent } from "../test-utils/test-utils"; import { mkEvent } from "../test-utils/test-utils";
import { EventType } from "../../src"; import { EventType } from "../../src";
describe("Filter", function() { describe("Filter", function () {
const filterId = "f1lt3ring15g00d4ursoul"; const filterId = "f1lt3ring15g00d4ursoul";
const userId = "@sir_arthur_david:humming.tiger"; const userId = "@sir_arthur_david:humming.tiger";
let filter: Filter; let filter: Filter;
beforeEach(function() { beforeEach(function () {
filter = new Filter(userId); filter = new Filter(userId);
}); });
describe("fromJson", function() { describe("fromJson", function () {
it("create a new Filter from the provided values", function() { it("create a new Filter from the provided values", function () {
const definition = { const definition = {
event_fields: ["type", "content"], event_fields: ["type", "content"],
}; };
@ -40,8 +40,8 @@ describe("Filter", function() {
}); });
}); });
describe("setTimelineLimit", function() { describe("setTimelineLimit", function () {
it("should set room.timeline.limit of the filter definition", function() { it("should set room.timeline.limit of the filter definition", function () {
filter.setTimelineLimit(10); filter.setTimelineLimit(10);
expect(filter.getDefinition()).toEqual({ expect(filter.getDefinition()).toEqual({
room: { room: {
@ -53,18 +53,18 @@ describe("Filter", function() {
}); });
}); });
describe("setDefinition/getDefinition", function() { describe("setDefinition/getDefinition", function () {
it("should set and get the filter body", function() { it("should set and get the filter body", function () {
const definition = { const definition = {
event_format: "client" as IFilterDefinition['event_format'], event_format: "client" as IFilterDefinition["event_format"],
}; };
filter.setDefinition(definition); filter.setDefinition(definition);
expect(filter.getDefinition()).toEqual(definition); expect(filter.getDefinition()).toEqual(definition);
}); });
}); });
describe("setUnreadThreadNotifications", function() { describe("setUnreadThreadNotifications", function () {
it("setUnreadThreadNotifications", function() { it("setUnreadThreadNotifications", function () {
filter.setUnreadThreadNotifications(true); filter.setUnreadThreadNotifications(true);
expect(filter.getDefinition()).toEqual({ expect(filter.getDefinition()).toEqual({
room: { room: {

View File

@ -65,8 +65,9 @@ describe("FetchHttpApi", () => {
describe("idServerRequest", () => { describe("idServerRequest", () => {
it("should throw if no idBaseUrl", () => { it("should throw if no idBaseUrl", () => {
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix }); const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
expect(() => api.idServerRequest(Method.Get, "/test", {}, IdentityPrefix.V2)) expect(() => api.idServerRequest(Method.Get, "/test", {}, IdentityPrefix.V2)).toThrow(
.toThrow("No identity server base URL set"); "No identity server base URL set",
);
}); });
it("should send params as query string for GET requests", () => { it("should send params as query string for GET requests", () => {
@ -105,9 +106,11 @@ describe("FetchHttpApi", () => {
const text = "418 I'm a teapot"; const text = "418 I'm a teapot";
const fetchFn = jest.fn().mockResolvedValue({ ok: true, text: jest.fn().mockResolvedValue(text) }); const fetchFn = jest.fn().mockResolvedValue({ ok: true, text: jest.fn().mockResolvedValue(text) });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true }); const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
await expect(api.requestOtherUrl(Method.Get, "http://url", undefined, { await expect(
json: false, api.requestOtherUrl(Method.Get, "http://url", undefined, {
})).resolves.toBe(text); json: false,
}),
).resolves.toBe(text);
}); });
it("should send token via query params if useAuthorizationHeader=false", () => { it("should send token via query params if useAuthorizationHeader=false", () => {
@ -207,10 +210,12 @@ describe("FetchHttpApi", () => {
return name === "Content-Type" ? "application/json" : null; return name === "Content-Type" ? "application/json" : null;
}, },
}, },
text: jest.fn().mockResolvedValue(JSON.stringify({ text: jest.fn().mockResolvedValue(
errcode: "M_CONSENT_NOT_GIVEN", JSON.stringify({
error: "Ye shall ask for consent", errcode: "M_CONSENT_NOT_GIVEN",
})), error: "Ye shall ask for consent",
}),
),
}); });
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(); const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn }); const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn });

View File

@ -85,8 +85,10 @@ describe("MatrixHttpApi", () => {
useAuthorizationHeader: false, useAuthorizationHeader: false,
}); });
upload = api.uploadContent({} as File); upload = api.uploadContent({} as File);
expect(xhr.open) expect(xhr.open).toHaveBeenCalledWith(
.toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload?access_token=token"); Method.Post,
baseUrl.toLowerCase() + "/_matrix/media/r0/upload?access_token=token",
);
expect(xhr.setRequestHeader).not.toHaveBeenCalledWith("Authorization"); expect(xhr.setRequestHeader).not.toHaveBeenCalledWith("Authorization");
}); });
@ -104,8 +106,10 @@ describe("MatrixHttpApi", () => {
it("should include filename by default", () => { it("should include filename by default", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix }); const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File, { name: "name" }); upload = api.uploadContent({} as File, { name: "name" });
expect(xhr.open) expect(xhr.open).toHaveBeenCalledWith(
.toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload?filename=name"); Method.Post,
baseUrl.toLowerCase() + "/_matrix/media/r0/upload?filename=name",
);
}); });
it("should allow not sending the filename", () => { it("should allow not sending the filename", () => {
@ -216,9 +220,9 @@ describe("MatrixHttpApi", () => {
it("should return active uploads in `getCurrentUploads`", () => { it("should return active uploads in `getCurrentUploads`", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix }); const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File); upload = api.uploadContent({} as File);
expect(api.getCurrentUploads().find(u => u.promise === upload)).toBeTruthy(); expect(api.getCurrentUploads().find((u) => u.promise === upload)).toBeTruthy();
api.cancelUpload(upload); api.cancelUpload(upload);
expect(api.getCurrentUploads().find(u => u.promise === upload)).toBeFalsy(); expect(api.getCurrentUploads().find((u) => u.promise === upload)).toBeFalsy();
}); });
it("should return expected object from `getContentUri`", () => { it("should return expected object from `getContentUri`", () => {

View File

@ -51,10 +51,7 @@ describe("anySignal", () => {
jest.useFakeTimers(); jest.useFakeTimers();
it("should fire when any signal fires", () => { it("should fire when any signal fires", () => {
const { signal } = anySignal([ const { signal } = anySignal([timeoutSignal(3000), timeoutSignal(2000)]);
timeoutSignal(3000),
timeoutSignal(2000),
]);
const onabort = jest.fn(); const onabort = jest.fn();
signal.onabort = onabort; signal.onabort = onabort;
@ -67,10 +64,7 @@ describe("anySignal", () => {
}); });
it("should cleanup when instructed", () => { it("should cleanup when instructed", () => {
const { signal, cleanup } = anySignal([ const { signal, cleanup } = anySignal([timeoutSignal(3000), timeoutSignal(2000)]);
timeoutSignal(3000),
timeoutSignal(2000),
]);
const onabort = jest.fn(); const onabort = jest.fn();
signal.onabort = onabort; signal.onabort = onabort;
@ -93,53 +87,95 @@ describe("anySignal", () => {
describe("parseErrorResponse", () => { describe("parseErrorResponse", () => {
it("should resolve Matrix Errors from XHR", () => { it("should resolve Matrix Errors from XHR", () => {
expect(parseErrorResponse({ expect(
getResponseHeader(name: string): string | null { parseErrorResponse(
return name === "Content-Type" ? "application/json" : null; {
}, getResponseHeader(name: string): string | null {
status: 500, return name === "Content-Type" ? "application/json" : null;
} as XMLHttpRequest, '{"errcode": "TEST"}')).toStrictEqual(new MatrixError({ },
errcode: "TEST", status: 500,
}, 500)); } as XMLHttpRequest,
'{"errcode": "TEST"}',
),
).toStrictEqual(
new MatrixError(
{
errcode: "TEST",
},
500,
),
);
}); });
it("should resolve Matrix Errors from fetch", () => { it("should resolve Matrix Errors from fetch", () => {
expect(parseErrorResponse({ expect(
headers: { parseErrorResponse(
get(name: string): string | null { {
return name === "Content-Type" ? "application/json" : null; headers: {
get(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
},
status: 500,
} as Response,
'{"errcode": "TEST"}',
),
).toStrictEqual(
new MatrixError(
{
errcode: "TEST",
}, },
}, 500,
status: 500, ),
} as Response, '{"errcode": "TEST"}')).toStrictEqual(new MatrixError({ );
errcode: "TEST",
}, 500));
}); });
it("should resolve Matrix Errors from XHR with urls", () => { it("should resolve Matrix Errors from XHR with urls", () => {
expect(parseErrorResponse({ expect(
responseURL: "https://example.com", parseErrorResponse(
getResponseHeader(name: string): string | null { {
return name === "Content-Type" ? "application/json" : null; responseURL: "https://example.com",
}, getResponseHeader(name: string): string | null {
status: 500, return name === "Content-Type" ? "application/json" : null;
} as XMLHttpRequest, '{"errcode": "TEST"}')).toStrictEqual(new MatrixError({ },
errcode: "TEST", status: 500,
}, 500, "https://example.com")); } as XMLHttpRequest,
'{"errcode": "TEST"}',
),
).toStrictEqual(
new MatrixError(
{
errcode: "TEST",
},
500,
"https://example.com",
),
);
}); });
it("should resolve Matrix Errors from fetch with urls", () => { it("should resolve Matrix Errors from fetch with urls", () => {
expect(parseErrorResponse({ expect(
url: "https://example.com", parseErrorResponse(
headers: { {
get(name: string): string | null { url: "https://example.com",
return name === "Content-Type" ? "application/json" : null; headers: {
get(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
},
status: 500,
} as Response,
'{"errcode": "TEST"}',
),
).toStrictEqual(
new MatrixError(
{
errcode: "TEST",
}, },
}, 500,
status: 500, "https://example.com",
} as Response, '{"errcode": "TEST"}')).toStrictEqual(new MatrixError({ ),
errcode: "TEST", );
}, 500, "https://example.com"));
}); });
it("should set a sensible default error message on MatrixError", () => { it("should set a sensible default error message on MatrixError", () => {
@ -152,37 +188,51 @@ describe("parseErrorResponse", () => {
}); });
it("should handle no type gracefully", () => { it("should handle no type gracefully", () => {
expect(parseErrorResponse({ expect(
headers: { parseErrorResponse(
get(name: string): string | null { {
return null; headers: {
}, get(name: string): string | null {
}, return null;
status: 500, },
} as Response, '{"errcode": "TEST"}')).toStrictEqual(new HTTPError("Server returned 500 error", 500)); },
status: 500,
} as Response,
'{"errcode": "TEST"}',
),
).toStrictEqual(new HTTPError("Server returned 500 error", 500));
}); });
it("should handle invalid type gracefully", () => { it("should handle invalid type gracefully", () => {
expect(parseErrorResponse({ expect(
headers: { parseErrorResponse(
get(name: string): string | null { {
return name === "Content-Type" ? " " : null; headers: {
}, get(name: string): string | null {
}, return name === "Content-Type" ? " " : null;
status: 500, },
} as Response, '{"errcode": "TEST"}')) },
.toStrictEqual(new Error("Error parsing Content-Type ' ': TypeError: invalid media type")); status: 500,
} as Response,
'{"errcode": "TEST"}',
),
).toStrictEqual(new Error("Error parsing Content-Type ' ': TypeError: invalid media type"));
}); });
it("should handle plaintext errors", () => { it("should handle plaintext errors", () => {
expect(parseErrorResponse({ expect(
headers: { parseErrorResponse(
get(name: string): string | null { {
return name === "Content-Type" ? "text/plain" : null; headers: {
}, get(name: string): string | null {
}, return name === "Content-Type" ? "text/plain" : null;
status: 418, },
} as Response, "I'm a teapot")).toStrictEqual(new HTTPError("Server returned 418 error: I'm a teapot", 418)); },
status: 418,
} as Response,
"I'm a teapot",
),
).toStrictEqual(new HTTPError("Server returned 418 error: I'm a teapot", 418));
}); });
}); });

View File

@ -44,9 +44,7 @@ describe("InteractiveAuth", () => {
requestEmailToken: jest.fn(), requestEmailToken: jest.fn(),
authData: { authData: {
session: "sessionId", session: "sessionId",
flows: [ flows: [{ stages: [AuthType.Password] }],
{ stages: [AuthType.Password] },
],
params: { params: {
[AuthType.Password]: { param: "aa" }, [AuthType.Password]: { param: "aa" },
}, },
@ -60,7 +58,7 @@ describe("InteractiveAuth", () => {
// first we expect a call here // first we expect a call here
stateUpdated.mockImplementation((stage) => { stateUpdated.mockImplementation((stage) => {
logger.log('aaaa'); logger.log("aaaa");
expect(stage).toEqual(AuthType.Password); expect(stage).toEqual(AuthType.Password);
ia.submitAuthDict({ ia.submitAuthDict({
type: AuthType.Password, type: AuthType.Password,
@ -68,9 +66,9 @@ describe("InteractiveAuth", () => {
}); });
// .. which should trigger a call here // .. which should trigger a call here
const requestRes = { "a": "b" }; const requestRes = { a: "b" };
doRequest.mockImplementation(async (authData) => { doRequest.mockImplementation(async (authData) => {
logger.log('cccc'); logger.log("cccc");
expect(authData).toEqual({ expect(authData).toEqual({
session: "sessionId", session: "sessionId",
type: AuthType.Password, type: AuthType.Password,
@ -95,9 +93,7 @@ describe("InteractiveAuth", () => {
requestEmailToken: jest.fn(), requestEmailToken: jest.fn(),
authData: { authData: {
session: "sessionId", session: "sessionId",
flows: [ flows: [{ stages: [AuthType.Password] }],
{ stages: [AuthType.Password] },
],
errcode: "MockError0", errcode: "MockError0",
params: { params: {
[AuthType.Password]: { param: "aa" }, [AuthType.Password]: { param: "aa" },
@ -112,7 +108,7 @@ describe("InteractiveAuth", () => {
// first we expect a call here // first we expect a call here
stateUpdated.mockImplementation((stage) => { stateUpdated.mockImplementation((stage) => {
logger.log('aaaa'); logger.log("aaaa");
expect(stage).toEqual(AuthType.Password); expect(stage).toEqual(AuthType.Password);
ia.submitAuthDict({ ia.submitAuthDict({
type: AuthType.Password, type: AuthType.Password,
@ -120,9 +116,9 @@ describe("InteractiveAuth", () => {
}); });
// .. which should trigger a call here // .. which should trigger a call here
const requestRes = { "a": "b" }; const requestRes = { a: "b" };
doRequest.mockImplementation(async (authData) => { doRequest.mockImplementation(async (authData) => {
logger.log('cccc'); logger.log("cccc");
expect(authData).toEqual({ expect(authData).toEqual({
session: "sessionId", session: "sessionId",
type: AuthType.Password, type: AuthType.Password,
@ -146,12 +142,10 @@ describe("InteractiveAuth", () => {
stateUpdated, stateUpdated,
requestEmailToken, requestEmailToken,
matrixClient: getFakeClient(), matrixClient: getFakeClient(),
emailSid: 'myEmailSid', emailSid: "myEmailSid",
authData: { authData: {
session: "sessionId", session: "sessionId",
flows: [ flows: [{ stages: [AuthType.Email, AuthType.Password] }],
{ stages: [AuthType.Email, AuthType.Password] },
],
params: { params: {
[AuthType.Email]: { param: "aa" }, [AuthType.Email]: { param: "aa" },
[AuthType.Password]: { param: "bb" }, [AuthType.Password]: { param: "bb" },
@ -166,7 +160,7 @@ describe("InteractiveAuth", () => {
// first we expect a call here // first we expect a call here
stateUpdated.mockImplementation((stage) => { stateUpdated.mockImplementation((stage) => {
logger.log('husky'); logger.log("husky");
expect(stage).toEqual(AuthType.Email); expect(stage).toEqual(AuthType.Email);
ia.submitAuthDict({ ia.submitAuthDict({
type: AuthType.Email, type: AuthType.Email,
@ -174,9 +168,9 @@ describe("InteractiveAuth", () => {
}); });
// .. which should trigger a call here // .. which should trigger a call here
const requestRes = { "a": "b" }; const requestRes = { a: "b" };
doRequest.mockImplementation(async (authData) => { doRequest.mockImplementation(async (authData) => {
logger.log('barfoo'); logger.log("barfoo");
expect(authData).toEqual({ expect(authData).toEqual({
session: "sessionId", session: "sessionId",
type: AuthType.Email, type: AuthType.Email,
@ -211,20 +205,21 @@ describe("InteractiveAuth", () => {
doRequest.mockImplementation((authData) => { doRequest.mockImplementation((authData) => {
logger.log("request1", authData); logger.log("request1", authData);
expect(authData).toEqual(null); // first request should be null expect(authData).toEqual(null); // first request should be null
const err = new MatrixError({ const err = new MatrixError(
session: "sessionId", {
flows: [ session: "sessionId",
{ stages: [AuthType.Password] }, flows: [{ stages: [AuthType.Password] }],
], params: {
params: { [AuthType.Password]: { param: "aa" },
[AuthType.Password]: { param: "aa" }, },
}, },
}, 401); 401,
);
throw err; throw err;
}); });
// .. which should be followed by a call to stateUpdated // .. which should be followed by a call to stateUpdated
const requestRes = { "a": "b" }; const requestRes = { a: "b" };
stateUpdated.mockImplementation((stage) => { stateUpdated.mockImplementation((stage) => {
expect(stage).toEqual(AuthType.Password); expect(stage).toEqual(AuthType.Password);
expect(ia.getSessionId()).toEqual("sessionId"); expect(ia.getSessionId()).toEqual("sessionId");
@ -272,20 +267,21 @@ describe("InteractiveAuth", () => {
doRequest.mockImplementation((authData) => { doRequest.mockImplementation((authData) => {
logger.log("request1", authData); logger.log("request1", authData);
expect(authData).toEqual(null); // first request should be null expect(authData).toEqual(null); // first request should be null
const err = new MatrixError({ const err = new MatrixError(
session: "sessionId", {
flows: [ session: "sessionId",
{ stages: [AuthType.Password] }, flows: [{ stages: [AuthType.Password] }],
], params: {
params: { [AuthType.Password]: { param: "aa" },
[AuthType.Password]: { param: "aa" }, },
}, },
}, 401); 401,
);
throw err; throw err;
}); });
// .. which should be followed by a call to stateUpdated // .. which should be followed by a call to stateUpdated
const requestRes = { "a": "b" }; const requestRes = { a: "b" };
stateUpdated.mockImplementation((stage) => { stateUpdated.mockImplementation((stage) => {
expect(stage).toEqual(AuthType.Password); expect(stage).toEqual(AuthType.Password);
expect(ia.getSessionId()).toEqual("sessionId"); expect(ia.getSessionId()).toEqual("sessionId");
@ -329,19 +325,20 @@ describe("InteractiveAuth", () => {
doRequest.mockImplementation((authData) => { doRequest.mockImplementation((authData) => {
logger.log("request1", authData); logger.log("request1", authData);
expect(authData).toEqual(null); // first request should be null expect(authData).toEqual(null); // first request should be null
const err = new MatrixError({ const err = new MatrixError(
session: "sessionId", {
flows: [], session: "sessionId",
params: { flows: [],
[AuthType.Password]: { param: "aa" }, params: {
[AuthType.Password]: { param: "aa" },
},
}, },
}, 401); 401,
);
throw err; throw err;
}); });
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow( await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(new Error("No appropriate authentication flow found"));
new Error('No appropriate authentication flow found'),
);
}); });
it("should start an auth stage and reject if no auth flow but has session", async () => { it("should start an auth stage and reject if no auth flow but has session", async () => {
@ -354,29 +351,29 @@ describe("InteractiveAuth", () => {
doRequest, doRequest,
stateUpdated, stateUpdated,
requestEmailToken, requestEmailToken,
authData: { authData: {},
},
sessionId: "sessionId", sessionId: "sessionId",
}); });
doRequest.mockImplementation((authData) => { doRequest.mockImplementation((authData) => {
logger.log("request1", authData); logger.log("request1", authData);
expect(authData).toEqual({ "session": "sessionId" }); // has existing sessionId expect(authData).toEqual({ session: "sessionId" }); // has existing sessionId
const err = new MatrixError({ const err = new MatrixError(
session: "sessionId", {
flows: [], session: "sessionId",
params: { flows: [],
[AuthType.Password]: { param: "aa" }, params: {
[AuthType.Password]: { param: "aa" },
},
error: "Mock Error 1",
errcode: "MOCKERR1",
}, },
error: "Mock Error 1", 401,
errcode: "MOCKERR1", );
}, 401);
throw err; throw err;
}); });
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow( await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(new Error("No appropriate authentication flow found"));
new Error('No appropriate authentication flow found'),
);
}); });
it("should handle unexpected error types without data propery set", async () => { it("should handle unexpected error types without data propery set", async () => {
@ -396,14 +393,12 @@ describe("InteractiveAuth", () => {
doRequest.mockImplementation((authData) => { doRequest.mockImplementation((authData) => {
logger.log("request1", authData); logger.log("request1", authData);
expect(authData).toEqual({ "session": "sessionId" }); // has existing sessionId expect(authData).toEqual({ session: "sessionId" }); // has existing sessionId
const err = new HTTPError('myerror', 401); const err = new HTTPError("myerror", 401);
throw err; throw err;
}); });
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow( await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(new Error("myerror"));
new Error("myerror"),
);
}); });
it("should allow dummy auth", async () => { it("should allow dummy auth", async () => {
@ -417,15 +412,13 @@ describe("InteractiveAuth", () => {
stateUpdated, stateUpdated,
requestEmailToken, requestEmailToken,
authData: { authData: {
session: 'sessionId', session: "sessionId",
flows: [ flows: [{ stages: [AuthType.Dummy] }],
{ stages: [AuthType.Dummy] },
],
params: {}, params: {},
}, },
}); });
const requestRes = { "a": "b" }; const requestRes = { a: "b" };
doRequest.mockImplementation((authData) => { doRequest.mockImplementation((authData) => {
logger.log("request1", authData); logger.log("request1", authData);
expect(authData).toEqual({ expect(authData).toEqual({
@ -450,7 +443,9 @@ describe("InteractiveAuth", () => {
const ia = new InteractiveAuth({ const ia = new InteractiveAuth({
matrixClient: getFakeClient(), matrixClient: getFakeClient(),
doRequest, stateUpdated, requestEmailToken, doRequest,
stateUpdated,
requestEmailToken,
}); });
await ia.requestEmailToken(); await ia.requestEmailToken();
@ -477,7 +472,9 @@ describe("InteractiveAuth", () => {
const ia = new InteractiveAuth({ const ia = new InteractiveAuth({
matrixClient: getFakeClient(), matrixClient: getFakeClient(),
doRequest, stateUpdated, requestEmailToken, doRequest,
stateUpdated,
requestEmailToken,
}); });
await ia.requestEmailToken(); await ia.requestEmailToken();
@ -506,7 +503,9 @@ describe("InteractiveAuth", () => {
const ia = new InteractiveAuth({ const ia = new InteractiveAuth({
matrixClient: getFakeClient(), matrixClient: getFakeClient(),
doRequest, stateUpdated, requestEmailToken, doRequest,
stateUpdated,
requestEmailToken,
}); });
await expect(ia.requestEmailToken.bind(ia)).rejects.toThrowError("unspecific network error"); await expect(ia.requestEmailToken.bind(ia)).rejects.toThrowError("unspecific network error");
@ -520,7 +519,9 @@ describe("InteractiveAuth", () => {
const ia = new InteractiveAuth({ const ia = new InteractiveAuth({
matrixClient: getFakeClient(), matrixClient: getFakeClient(),
doRequest, stateUpdated, requestEmailToken, doRequest,
stateUpdated,
requestEmailToken,
}); });
await Promise.all([ia.requestEmailToken(), ia.requestEmailToken(), ia.requestEmailToken()]); await Promise.all([ia.requestEmailToken(), ia.requestEmailToken(), ia.requestEmailToken()]);
@ -536,7 +537,9 @@ describe("InteractiveAuth", () => {
const ia = new InteractiveAuth({ const ia = new InteractiveAuth({
matrixClient: getFakeClient(), matrixClient: getFakeClient(),
doRequest, stateUpdated, requestEmailToken, doRequest,
stateUpdated,
requestEmailToken,
}); });
await ia.requestEmailToken(); await ia.requestEmailToken();

View File

@ -16,15 +16,13 @@ limitations under the License.
import { LocalNotificationSettings } from "../../src/@types/local_notifications"; import { LocalNotificationSettings } from "../../src/@types/local_notifications";
import { LOCAL_NOTIFICATION_SETTINGS_PREFIX, MatrixClient } from "../../src/matrix"; import { LOCAL_NOTIFICATION_SETTINGS_PREFIX, MatrixClient } from "../../src/matrix";
import { TestClient } from '../TestClient'; import { TestClient } from "../TestClient";
let client: MatrixClient; let client: MatrixClient;
describe("Local notification settings", () => { describe("Local notification settings", () => {
beforeEach(() => { beforeEach(() => {
client = (new TestClient( client = new TestClient("@alice:matrix.org", "123", undefined, undefined, undefined).client;
"@alice:matrix.org", "123", undefined, undefined, undefined,
)).client;
client.setAccountData = jest.fn(); client.setAccountData = jest.fn();
}); });

View File

@ -25,13 +25,13 @@ import {
import { TEXT_NODE_TYPE } from "../../src/@types/extensible_events"; import { TEXT_NODE_TYPE } from "../../src/@types/extensible_events";
import { MsgType } from "../../src/@types/event"; import { MsgType } from "../../src/@types/event";
describe("Location", function() { describe("Location", function () {
const defaultContent = { const defaultContent = {
"body": "Location geo:-36.24484561954707,175.46884959563613;u=10 at 2022-03-09T11:01:52.443Z", body: "Location geo:-36.24484561954707,175.46884959563613;u=10 at 2022-03-09T11:01:52.443Z",
"msgtype": "m.location", msgtype: "m.location",
"geo_uri": "geo:-36.24484561954707,175.46884959563613;u=10", geo_uri: "geo:-36.24484561954707,175.46884959563613;u=10",
[M_LOCATION.name]: { "uri": "geo:-36.24484561954707,175.46884959563613;u=10", "description": null }, [M_LOCATION.name]: { uri: "geo:-36.24484561954707,175.46884959563613;u=10", description: null },
[M_ASSET.name]: { "type": "m.self" }, [M_ASSET.name]: { type: "m.self" },
[TEXT_NODE_TYPE.name]: "Location geo:-36.24484561954707,175.46884959563613;u=10 at 2022-03-09T11:01:52.443Z", [TEXT_NODE_TYPE.name]: "Location geo:-36.24484561954707,175.46884959563613;u=10 at 2022-03-09T11:01:52.443Z",
[M_TIMESTAMP.name]: 1646823712443, [M_TIMESTAMP.name]: 1646823712443,
} as any; } as any;
@ -44,12 +44,14 @@ describe("Location", function() {
const legacyEventContent = { const legacyEventContent = {
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
body, msgtype, geo_uri, body,
msgtype,
geo_uri,
} as LocationEventWireContent; } as LocationEventWireContent;
it("should create a valid location with defaults", function() { it("should create a valid location with defaults", function () {
const loc = makeLocationContent(undefined, "geo:foo", 134235435); const loc = makeLocationContent(undefined, "geo:foo", 134235435);
expect(loc.body).toEqual('User Location geo:foo at 1970-01-02T13:17:15.435Z'); expect(loc.body).toEqual("User Location geo:foo at 1970-01-02T13:17:15.435Z");
expect(loc.msgtype).toEqual(MsgType.Location); expect(loc.msgtype).toEqual(MsgType.Location);
expect(loc.geo_uri).toEqual("geo:foo"); expect(loc.geo_uri).toEqual("geo:foo");
expect(M_LOCATION.findIn(loc)).toEqual({ expect(M_LOCATION.findIn(loc)).toEqual({
@ -57,13 +59,12 @@ describe("Location", function() {
description: undefined, description: undefined,
}); });
expect(M_ASSET.findIn(loc)).toEqual({ type: LocationAssetType.Self }); expect(M_ASSET.findIn(loc)).toEqual({ type: LocationAssetType.Self });
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual('User Location geo:foo at 1970-01-02T13:17:15.435Z'); expect(TEXT_NODE_TYPE.findIn(loc)).toEqual("User Location geo:foo at 1970-01-02T13:17:15.435Z");
expect(M_TIMESTAMP.findIn(loc)).toEqual(134235435); expect(M_TIMESTAMP.findIn(loc)).toEqual(134235435);
}); });
it("should create a valid location with explicit properties", function() { it("should create a valid location with explicit properties", function () {
const loc = makeLocationContent( const loc = makeLocationContent(undefined, "geo:bar", 134235436, "desc", LocationAssetType.Pin);
undefined, "geo:bar", 134235436, "desc", LocationAssetType.Pin);
expect(loc.body).toEqual('Location "desc" geo:bar at 1970-01-02T13:17:15.436Z'); expect(loc.body).toEqual('Location "desc" geo:bar at 1970-01-02T13:17:15.436Z');
expect(loc.msgtype).toEqual(MsgType.Location); expect(loc.msgtype).toEqual(MsgType.Location);
@ -77,19 +78,19 @@ describe("Location", function() {
expect(M_TIMESTAMP.findIn(loc)).toEqual(134235436); expect(M_TIMESTAMP.findIn(loc)).toEqual(134235436);
}); });
it('parses backwards compatible event correctly', () => { it("parses backwards compatible event correctly", () => {
const eventContent = parseLocationEvent(backwardsCompatibleEventContent); const eventContent = parseLocationEvent(backwardsCompatibleEventContent);
expect(eventContent).toEqual(backwardsCompatibleEventContent); expect(eventContent).toEqual(backwardsCompatibleEventContent);
}); });
it('parses modern correctly', () => { it("parses modern correctly", () => {
const eventContent = parseLocationEvent(modernEventContent); const eventContent = parseLocationEvent(modernEventContent);
expect(eventContent).toEqual(backwardsCompatibleEventContent); expect(eventContent).toEqual(backwardsCompatibleEventContent);
}); });
it('parses legacy event correctly', () => { it("parses legacy event correctly", () => {
const eventContent = parseLocationEvent(legacyEventContent); const eventContent = parseLocationEvent(legacyEventContent);
const { const {

View File

@ -1,59 +1,59 @@
import { SSOAction } from '../../src/@types/auth'; import { SSOAction } from "../../src/@types/auth";
import { TestClient } from '../TestClient'; import { TestClient } from "../TestClient";
describe('Login request', function() { describe("Login request", function () {
let client: TestClient; let client: TestClient;
beforeEach(function() { beforeEach(function () {
client = new TestClient(); client = new TestClient();
}); });
afterEach(function() { afterEach(function () {
client.stop(); client.stop();
}); });
it('should store "access_token" and "user_id" if in response', async function() { it('should store "access_token" and "user_id" if in response', async function () {
const response = { user_id: 1, access_token: Date.now().toString(16) }; const response = { user_id: 1, access_token: Date.now().toString(16) };
client.httpBackend.when('POST', '/login').respond(200, response); client.httpBackend.when("POST", "/login").respond(200, response);
client.httpBackend.flush('/login', 1, 100); client.httpBackend.flush("/login", 1, 100);
await client.client.login('m.login.any', { user: 'test', password: '12312za' }); await client.client.login("m.login.any", { user: "test", password: "12312za" });
expect(client.client.getAccessToken()).toBe(response.access_token); expect(client.client.getAccessToken()).toBe(response.access_token);
expect(client.client.getUserId()).toBe(response.user_id); expect(client.client.getUserId()).toBe(response.user_id);
}); });
}); });
describe('SSO login URL', function() { describe("SSO login URL", function () {
let client: TestClient; let client: TestClient;
beforeEach(function() { beforeEach(function () {
client = new TestClient(); client = new TestClient();
}); });
afterEach(function() { afterEach(function () {
client.stop(); client.stop();
}); });
describe('SSOAction', function() { describe("SSOAction", function () {
const redirectUri = "https://test.com/foo"; const redirectUri = "https://test.com/foo";
it('No action', function() { it("No action", function () {
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, undefined); const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, undefined);
const url = new URL(urlString); const url = new URL(urlString);
expect(url.searchParams.has('org.matrix.msc3824.action')).toBe(false); expect(url.searchParams.has("org.matrix.msc3824.action")).toBe(false);
}); });
it('register', function() { it("register", function () {
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.REGISTER); const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.REGISTER);
const url = new URL(urlString); const url = new URL(urlString);
expect(url.searchParams.get('org.matrix.msc3824.action')).toEqual('register'); expect(url.searchParams.get("org.matrix.msc3824.action")).toEqual("register");
}); });
it('login', function() { it("login", function () {
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.LOGIN); const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.LOGIN);
const url = new URL(urlString); const url = new URL(urlString);
expect(url.searchParams.get('org.matrix.msc3824.action')).toEqual('login'); expect(url.searchParams.get("org.matrix.msc3824.action")).toEqual("login");
}); });
}); });
}); });

View File

@ -38,7 +38,8 @@ import { makeBeaconInfoContent } from "../../src/content-helpers";
import { M_BEACON_INFO } from "../../src/@types/beacon"; import { M_BEACON_INFO } from "../../src/@types/beacon";
import { import {
ContentHelpers, ContentHelpers,
EventTimeline, ICreateRoomOpts, EventTimeline,
ICreateRoomOpts,
IRequestOpts, IRequestOpts,
MatrixError, MatrixError,
MatrixHttpApi, MatrixHttpApi,
@ -83,7 +84,7 @@ type WrappedRoom = Room & {
_state: Map<string, any>; _state: Map<string, any>;
}; };
describe("MatrixClient", function() { describe("MatrixClient", function () {
const userId = "@alice:bar"; const userId = "@alice:bar";
const identityServerUrl = "https://identity.server"; const identityServerUrl = "https://identity.server";
const identityServerDomain = "identity.server"; const identityServerDomain = "identity.server";
@ -137,13 +138,20 @@ describe("MatrixClient", function() {
}); });
} }
const next = httpLookups.shift(); const next = httpLookups.shift();
const logLine = ( const logLine =
"MatrixClient[UT] RECV " + method + " " + path + " " + "MatrixClient[UT] RECV " +
"EXPECT " + (next ? next.method : next) + " " + (next ? next.path : next) method +
); " " +
path +
" " +
"EXPECT " +
(next ? next.method : next) +
" " +
(next ? next.path : next);
logger.log(logLine); logger.log(logLine);
if (!next) { // no more things to return if (!next) {
// no more things to return
if (pendingLookup) { if (pendingLookup) {
if (pendingLookup.method === method && pendingLookup.path === path) { if (pendingLookup.method === method && pendingLookup.path === path) {
return pendingLookup.promise; return pendingLookup.promise;
@ -159,15 +167,12 @@ describe("MatrixClient", function() {
return pendingLookup.promise; return pendingLookup.promise;
} }
if (next.path === path && next.method === method) { if (next.path === path && next.method === method) {
logger.log( logger.log("MatrixClient[UT] Matched. Returning " + (next.error ? "BAD" : "GOOD") + " response");
"MatrixClient[UT] Matched. Returning " +
(next.error ? "BAD" : "GOOD") + " response",
);
if (next.expectBody) { if (next.expectBody) {
expect(data).toEqual(next.expectBody); expect(data).toEqual(next.expectBody);
} }
if (next.expectQueryParams) { if (next.expectQueryParams) {
Object.keys(next.expectQueryParams).forEach(function(k) { Object.keys(next.expectQueryParams).forEach(function (k) {
expect(qp?.[k]).toEqual(next.expectQueryParams![k]); expect(qp?.[k]).toEqual(next.expectQueryParams![k]);
}); });
} }
@ -201,18 +206,13 @@ describe("MatrixClient", function() {
baseUrl: "https://my.home.server", baseUrl: "https://my.home.server",
idBaseUrl: identityServerUrl, idBaseUrl: identityServerUrl,
accessToken: "my.access.token", accessToken: "my.access.token",
fetchFn: function() {} as any, // NOP fetchFn: function () {} as any, // NOP
store: store, store: store,
scheduler: scheduler, scheduler: scheduler,
userId: userId, userId: userId,
}); });
// FIXME: We shouldn't be yanking http like this. // FIXME: We shouldn't be yanking http like this.
client.http = ([ client.http = (["authedRequest", "getContentUri", "request", "uploadContent"] as const).reduce((r, k) => {
"authedRequest",
"getContentUri",
"request",
"uploadContent",
] as const).reduce((r, k) => {
r[k] = jest.fn(); r[k] = jest.fn();
return r; return r;
}, {} as MatrixHttpApi<any>); }, {} as MatrixHttpApi<any>);
@ -220,22 +220,35 @@ describe("MatrixClient", function() {
mocked(client.http.request).mockImplementation(httpReq); mocked(client.http.request).mockImplementation(httpReq);
} }
beforeEach(function() { beforeEach(function () {
scheduler = ([ scheduler = (["getQueueForEvent", "queueEvent", "removeEventFromQueue", "setProcessFunction"] as const).reduce(
"getQueueForEvent", (r, k) => {
"queueEvent", r[k] = jest.fn();
"removeEventFromQueue", return r;
"setProcessFunction", },
] as const).reduce((r, k) => { {} as MatrixScheduler,
r[k] = jest.fn(); );
return r; store = (
}, {} as MatrixScheduler); [
store = ([ "getRoom",
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback", "getRooms",
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom", "storeUser", "getUser",
"getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter", "getSyncToken",
"startup", "deleteAllData", "scrollback",
] as const).reduce((r, k) => { "save",
"wantsSave",
"setSyncToken",
"storeEvents",
"storeRoom",
"storeUser",
"getFilterIdByName",
"setFilterIdByName",
"getFilter",
"storeFilter",
"startup",
"deleteAllData",
] as const
).reduce((r, k) => {
r[k] = jest.fn(); r[k] = jest.fn();
return r; return r;
}, {} as Store); }, {} as Store);
@ -256,13 +269,13 @@ describe("MatrixClient", function() {
httpLookups.push(SYNC_RESPONSE); httpLookups.push(SYNC_RESPONSE);
}); });
afterEach(function() { afterEach(function () {
// need to re-stub the requests with NOPs because there are no guarantees // need to re-stub the requests with NOPs because there are no guarantees
// clients from previous tests will be GC'd before the next test. This // clients from previous tests will be GC'd before the next test. This
// means they may call /events and then fail an expect() which will fail // means they may call /events and then fail an expect() which will fail
// a DIFFERENT test (pollution between tests!) - we return unresolved // a DIFFERENT test (pollution between tests!) - we return unresolved
// promises to stop the client from continuing to run. // promises to stop the client from continuing to run.
mocked(client.http.authedRequest).mockImplementation(function() { mocked(client.http.authedRequest).mockImplementation(function () {
return new Promise(() => {}); return new Promise(() => {});
}); });
client.stopClient(); client.stopClient();
@ -276,12 +289,14 @@ describe("MatrixClient", function() {
it("overload without threadId works", async () => { it("overload without threadId works", async () => {
const eventId = "$eventId:example.org"; const eventId = "$eventId:example.org";
const txnId = client.makeTxnId(); const txnId = client.makeTxnId();
httpLookups = [{ httpLookups = [
method: "PUT", {
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`, method: "PUT",
data: { event_id: eventId }, path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
expectBody: content, data: { event_id: eventId },
}]; expectBody: content,
},
];
await client.sendEvent(roomId, EventType.RoomMessage, { ...content }, txnId); await client.sendEvent(roomId, EventType.RoomMessage, { ...content }, txnId);
}); });
@ -289,12 +304,14 @@ describe("MatrixClient", function() {
it("overload with null threadId works", async () => { it("overload with null threadId works", async () => {
const eventId = "$eventId:example.org"; const eventId = "$eventId:example.org";
const txnId = client.makeTxnId(); const txnId = client.makeTxnId();
httpLookups = [{ httpLookups = [
method: "PUT", {
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`, method: "PUT",
data: { event_id: eventId }, path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
expectBody: content, data: { event_id: eventId },
}]; expectBody: content,
},
];
await client.sendEvent(roomId, null, EventType.RoomMessage, { ...content }, txnId); await client.sendEvent(roomId, null, EventType.RoomMessage, { ...content }, txnId);
}); });
@ -303,19 +320,21 @@ describe("MatrixClient", function() {
const eventId = "$eventId:example.org"; const eventId = "$eventId:example.org";
const txnId = client.makeTxnId(); const txnId = client.makeTxnId();
const threadId = "$threadId:server"; const threadId = "$threadId:server";
httpLookups = [{ httpLookups = [
method: "PUT", {
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`, method: "PUT",
data: { event_id: eventId }, path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
expectBody: { data: { event_id: eventId },
...content, expectBody: {
"m.relates_to": { ...content,
"event_id": threadId, "m.relates_to": {
"is_falling_back": true, event_id: threadId,
"rel_type": "m.thread", is_falling_back: true,
rel_type: "m.thread",
},
}, },
}, },
}]; ];
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId); await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
}); });
@ -331,22 +350,24 @@ describe("MatrixClient", function() {
const rootEvent = new MatrixEvent({ event_id: threadId }); const rootEvent = new MatrixEvent({ event_id: threadId });
room.createThread(threadId, rootEvent, [rootEvent], false); room.createThread(threadId, rootEvent, [rootEvent], false);
httpLookups = [{ httpLookups = [
method: "PUT", {
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`, method: "PUT",
data: { event_id: eventId }, path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
expectBody: { data: { event_id: eventId },
...content, expectBody: {
"m.relates_to": { ...content,
"m.in_reply_to": { "m.relates_to": {
event_id: threadId, "m.in_reply_to": {
event_id: threadId,
},
"event_id": threadId,
"is_falling_back": true,
"rel_type": "m.thread",
}, },
"event_id": threadId,
"is_falling_back": true,
"rel_type": "m.thread",
}, },
}, },
}]; ];
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId); await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
}); });
@ -371,22 +392,24 @@ describe("MatrixClient", function() {
const rootEvent = new MatrixEvent({ event_id: threadId }); const rootEvent = new MatrixEvent({ event_id: threadId });
room.createThread(threadId, rootEvent, [rootEvent], false); room.createThread(threadId, rootEvent, [rootEvent], false);
httpLookups = [{ httpLookups = [
method: "PUT", {
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`, method: "PUT",
data: { event_id: eventId }, path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
expectBody: { data: { event_id: eventId },
...content, expectBody: {
"m.relates_to": { ...content,
"m.in_reply_to": { "m.relates_to": {
event_id: "$other:event", "m.in_reply_to": {
event_id: "$other:event",
},
"event_id": threadId,
"is_falling_back": false,
"rel_type": "m.thread",
}, },
"event_id": threadId,
"is_falling_back": false,
"rel_type": "m.thread",
}, },
}, },
}]; ];
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId); await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
}); });
@ -572,15 +595,12 @@ describe("MatrixClient", function() {
expect(tree).toBeFalsy(); expect(tree).toBeFalsy();
}); });
it("should not POST /filter if a matching filter already exists", async function() { it("should not POST /filter if a matching filter already exists", async function () {
httpLookups = [ httpLookups = [PUSH_RULES_RESPONSE, SYNC_RESPONSE];
PUSH_RULES_RESPONSE,
SYNC_RESPONSE,
];
const filterId = "ehfewf"; const filterId = "ehfewf";
mocked(store.getFilterIdByName).mockReturnValue(filterId); mocked(store.getFilterIdByName).mockReturnValue(filterId);
const filter = new Filter("0", filterId); const filter = new Filter("0", filterId);
filter.setDefinition({ "room": { "timeline": { "limit": 8 } } }); filter.setDefinition({ room: { timeline: { limit: 8 } } });
mocked(store.getFilter).mockReturnValue(filter); mocked(store.getFilter).mockReturnValue(filter);
const syncPromise = new Promise<void>((resolve, reject) => { const syncPromise = new Promise<void>((resolve, reject) => {
client.on(ClientEvent.Sync, function syncListener(state) { client.on(ClientEvent.Sync, function syncListener(state) {
@ -597,12 +617,12 @@ describe("MatrixClient", function() {
await syncPromise; await syncPromise;
}); });
describe("getSyncState", function() { describe("getSyncState", function () {
it("should return null if the client isn't started", function() { it("should return null if the client isn't started", function () {
expect(client.getSyncState()).toBe(null); expect(client.getSyncState()).toBe(null);
}); });
it("should return the same sync state as emitted sync events", async function() { it("should return the same sync state as emitted sync events", async function () {
const syncingPromise = new Promise<void>((resolve) => { const syncingPromise = new Promise<void>((resolve) => {
client.on(ClientEvent.Sync, function syncListener(state) { client.on(ClientEvent.Sync, function syncListener(state) {
expect(state).toEqual(client.getSyncState()); expect(state).toEqual(client.getSyncState());
@ -617,22 +637,20 @@ describe("MatrixClient", function() {
}); });
}); });
describe("getOrCreateFilter", function() { describe("getOrCreateFilter", function () {
it("should POST createFilter if no id is present in localStorage", function() { it("should POST createFilter if no id is present in localStorage", function () {});
}); it("should use an existing filter if id is present in localStorage", function () {});
it("should use an existing filter if id is present in localStorage", function() { it("should handle localStorage filterId missing from the server", function (done) {
});
it("should handle localStorage filterId missing from the server", function(done) {
function getFilterName(userId: string, suffix?: string) { function getFilterName(userId: string, suffix?: string) {
// scope this on the user ID because people may login on many accounts // scope this on the user ID because people may login on many accounts
// and they all need to be stored! // and they all need to be stored!
return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : ""); return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : "");
} }
const invalidFilterId = 'invalidF1lt3r'; const invalidFilterId = "invalidF1lt3r";
httpLookups = []; httpLookups = [];
httpLookups.push({ httpLookups.push({
method: "GET", method: "GET",
path: FILTER_PATH + '/' + invalidFilterId, path: FILTER_PATH + "/" + invalidFilterId,
error: { error: {
errcode: "M_UNKNOWN", errcode: "M_UNKNOWN",
name: "M_UNKNOWN", name: "M_UNKNOWN",
@ -648,25 +666,27 @@ describe("MatrixClient", function() {
client.store.setFilterIdByName(filterName, invalidFilterId); client.store.setFilterIdByName(filterName, invalidFilterId);
const filter = new Filter(client.credentials.userId); const filter = new Filter(client.credentials.userId);
client.getOrCreateFilter(filterName, filter).then(function(filterId) { client.getOrCreateFilter(filterName, filter).then(function (filterId) {
expect(filterId).toEqual(FILTER_RESPONSE.data?.filter_id); expect(filterId).toEqual(FILTER_RESPONSE.data?.filter_id);
done(); done();
}); });
}); });
}); });
describe("retryImmediately", function() { describe("retryImmediately", function () {
it("should return false if there is no request waiting", async function() { it("should return false if there is no request waiting", async function () {
httpLookups = []; httpLookups = [];
await client.startClient(); await client.startClient();
expect(client.retryImmediately()).toBe(false); expect(client.retryImmediately()).toBe(false);
}); });
it("should work on /filter", function(done) { it("should work on /filter", function (done) {
httpLookups = []; httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE); httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push({ httpLookups.push({
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" }, method: "POST",
path: FILTER_PATH,
error: { errcode: "NOPE_NOPE_NOPE" },
}); });
httpLookups.push(FILTER_RESPONSE); httpLookups.push(FILTER_RESPONSE);
httpLookups.push(SYNC_RESPONSE); httpLookups.push(SYNC_RESPONSE);
@ -687,20 +707,22 @@ describe("MatrixClient", function() {
client.startClient(); client.startClient();
}); });
it("should work on /sync", function(done) { it("should work on /sync", function (done) {
httpLookups.push({ httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" }, method: "GET",
path: "/sync",
error: { errcode: "NOPE_NOPE_NOPE" },
}); });
httpLookups.push({ httpLookups.push({
method: "GET", path: "/sync", data: SYNC_DATA, method: "GET",
path: "/sync",
data: SYNC_DATA,
}); });
client.on(ClientEvent.Sync, function syncListener(state) { client.on(ClientEvent.Sync, function syncListener(state) {
if (state === "ERROR" && httpLookups.length > 0) { if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(1); expect(httpLookups.length).toEqual(1);
expect(client.retryImmediately()).toBe( expect(client.retryImmediately()).toBe(true);
true,
);
jest.advanceTimersByTime(1); jest.advanceTimersByTime(1);
} else if (state === "RECONNECTING" && httpLookups.length > 0) { } else if (state === "RECONNECTING" && httpLookups.length > 0) {
jest.advanceTimersByTime(10000); jest.advanceTimersByTime(10000);
@ -712,10 +734,12 @@ describe("MatrixClient", function() {
client.startClient(); client.startClient();
}); });
it("should work on /pushrules", function(done) { it("should work on /pushrules", function (done) {
httpLookups = []; httpLookups = [];
httpLookups.push({ httpLookups.push({
method: "GET", path: "/pushrules/", error: { errcode: "NOPE_NOPE_NOPE" }, method: "GET",
path: "/pushrules/",
error: { errcode: "NOPE_NOPE_NOPE" },
}); });
httpLookups.push(PUSH_RULES_RESPONSE); httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE); httpLookups.push(FILTER_RESPONSE);
@ -738,13 +762,11 @@ describe("MatrixClient", function() {
}); });
}); });
describe("emitted sync events", function() { describe("emitted sync events", function () {
function syncChecker(expectedStates: [string, string | null][], done: Function) { function syncChecker(expectedStates: [string, string | null][], done: Function) {
return function syncListener(state: SyncState, old: SyncState | null) { return function syncListener(state: SyncState, old: SyncState | null) {
const expected = expectedStates.shift(); const expected = expectedStates.shift();
logger.log( logger.log("'sync' curr=%s old=%s EXPECT=%s", state, old, expected);
"'sync' curr=%s old=%s EXPECT=%s", state, old, expected,
);
if (!expected) { if (!expected) {
done(); done();
return; return;
@ -760,19 +782,21 @@ describe("MatrixClient", function() {
}; };
} }
it("should transition null -> PREPARED after the first /sync", function(done) { it("should transition null -> PREPARED after the first /sync", function (done) {
const expectedStates: [string, string | null][] = []; const expectedStates: [string, string | null][] = [];
expectedStates.push(["PREPARED", null]); expectedStates.push(["PREPARED", null]);
client.on(ClientEvent.Sync, syncChecker(expectedStates, done)); client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
client.startClient(); client.startClient();
}); });
it("should transition null -> ERROR after a failed /filter", function(done) { it("should transition null -> ERROR after a failed /filter", function (done) {
const expectedStates: [string, string | null][] = []; const expectedStates: [string, string | null][] = [];
httpLookups = []; httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE); httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push({ httpLookups.push({
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" }, method: "POST",
path: FILTER_PATH,
error: { errcode: "NOPE_NOPE_NOPE" },
}); });
expectedStates.push(["ERROR", null]); expectedStates.push(["ERROR", null]);
client.on(ClientEvent.Sync, syncChecker(expectedStates, done)); client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
@ -782,24 +806,31 @@ describe("MatrixClient", function() {
// Disabled because now `startClient` makes a legit call to `/versions` // Disabled because now `startClient` makes a legit call to `/versions`
// And those tests are really unhappy about it... Not possible to figure // And those tests are really unhappy about it... Not possible to figure
// out what a good resolution would look like // out what a good resolution would look like
xit("should transition ERROR -> CATCHUP after /sync if prev failed", function(done) { xit("should transition ERROR -> CATCHUP after /sync if prev failed", function (done) {
const expectedStates: [string, string | null][] = []; const expectedStates: [string, string | null][] = [];
acceptKeepalives = false; acceptKeepalives = false;
httpLookups = []; httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE); httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE); httpLookups.push(FILTER_RESPONSE);
httpLookups.push({ httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" }, method: "GET",
path: "/sync",
error: { errcode: "NOPE_NOPE_NOPE" },
}); });
httpLookups.push({ httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH, method: "GET",
path: KEEP_ALIVE_PATH,
error: { errcode: "KEEPALIVE_FAIL" }, error: { errcode: "KEEPALIVE_FAIL" },
}); });
httpLookups.push({ httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH, data: {}, method: "GET",
path: KEEP_ALIVE_PATH,
data: {},
}); });
httpLookups.push({ httpLookups.push({
method: "GET", path: "/sync", data: SYNC_DATA, method: "GET",
path: "/sync",
data: SYNC_DATA,
}); });
expectedStates.push(["RECONNECTING", null]); expectedStates.push(["RECONNECTING", null]);
@ -809,7 +840,7 @@ describe("MatrixClient", function() {
client.startClient(); client.startClient();
}); });
it("should transition PREPARED -> SYNCING after /sync", function(done) { it("should transition PREPARED -> SYNCING after /sync", function (done) {
const expectedStates: [string, string | null][] = []; const expectedStates: [string, string | null][] = [];
expectedStates.push(["PREPARED", null]); expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]); expectedStates.push(["SYNCING", "PREPARED"]);
@ -817,14 +848,17 @@ describe("MatrixClient", function() {
client.startClient(); client.startClient();
}); });
xit("should transition SYNCING -> ERROR after a failed /sync", function(done) { xit("should transition SYNCING -> ERROR after a failed /sync", function (done) {
acceptKeepalives = false; acceptKeepalives = false;
const expectedStates: [string, string | null][] = []; const expectedStates: [string, string | null][] = [];
httpLookups.push({ httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }, method: "GET",
path: "/sync",
error: { errcode: "NONONONONO" },
}); });
httpLookups.push({ httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH, method: "GET",
path: KEEP_ALIVE_PATH,
error: { errcode: "KEEPALIVE_FAIL" }, error: { errcode: "KEEPALIVE_FAIL" },
}); });
@ -836,10 +870,12 @@ describe("MatrixClient", function() {
client.startClient(); client.startClient();
}); });
xit("should transition ERROR -> SYNCING after /sync if prev failed", function(done) { xit("should transition ERROR -> SYNCING after /sync if prev failed", function (done) {
const expectedStates: [string, string | null][] = []; const expectedStates: [string, string | null][] = [];
httpLookups.push({ httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }, method: "GET",
path: "/sync",
error: { errcode: "NONONONONO" },
}); });
httpLookups.push(SYNC_RESPONSE); httpLookups.push(SYNC_RESPONSE);
@ -850,7 +886,7 @@ describe("MatrixClient", function() {
client.startClient(); client.startClient();
}); });
it("should transition SYNCING -> SYNCING on subsequent /sync successes", function(done) { it("should transition SYNCING -> SYNCING on subsequent /sync successes", function (done) {
const expectedStates: [string, string | null][] = []; const expectedStates: [string, string | null][] = [];
httpLookups.push(SYNC_RESPONSE); httpLookups.push(SYNC_RESPONSE);
httpLookups.push(SYNC_RESPONSE); httpLookups.push(SYNC_RESPONSE);
@ -862,18 +898,22 @@ describe("MatrixClient", function() {
client.startClient(); client.startClient();
}); });
xit("should transition ERROR -> ERROR if keepalive keeps failing", function(done) { xit("should transition ERROR -> ERROR if keepalive keeps failing", function (done) {
acceptKeepalives = false; acceptKeepalives = false;
const expectedStates: [string, string | null][] = []; const expectedStates: [string, string | null][] = [];
httpLookups.push({ httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }, method: "GET",
path: "/sync",
error: { errcode: "NONONONONO" },
}); });
httpLookups.push({ httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH, method: "GET",
path: KEEP_ALIVE_PATH,
error: { errcode: "KEEPALIVE_FAIL" }, error: { errcode: "KEEPALIVE_FAIL" },
}); });
httpLookups.push({ httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH, method: "GET",
path: KEEP_ALIVE_PATH,
error: { errcode: "KEEPALIVE_FAIL" }, error: { errcode: "KEEPALIVE_FAIL" },
}); });
@ -887,27 +927,29 @@ describe("MatrixClient", function() {
}); });
}); });
describe("inviteByEmail", function() { describe("inviteByEmail", function () {
const roomId = "!foo:bar"; const roomId = "!foo:bar";
it("should send an invite HTTP POST", function() { it("should send an invite HTTP POST", function () {
httpLookups = [{ httpLookups = [
method: "POST", {
path: "/rooms/!foo%3Abar/invite", method: "POST",
data: {}, path: "/rooms/!foo%3Abar/invite",
expectBody: { data: {},
id_server: identityServerDomain, expectBody: {
medium: "email", id_server: identityServerDomain,
address: "alice@gmail.com", medium: "email",
address: "alice@gmail.com",
},
}, },
}]; ];
client.inviteByEmail(roomId, "alice@gmail.com"); client.inviteByEmail(roomId, "alice@gmail.com");
expect(httpLookups.length).toEqual(0); expect(httpLookups.length).toEqual(0);
}); });
}); });
describe("guest rooms", function() { describe("guest rooms", function () {
it("should only do /sync calls (without filter/pushrules)", async function() { it("should only do /sync calls (without filter/pushrules)", async function () {
httpLookups = []; // no /pushrules or /filter httpLookups = []; // no /pushrules or /filter
httpLookups.push({ httpLookups.push({
method: "GET", method: "GET",
@ -919,20 +961,21 @@ describe("MatrixClient", function() {
expect(httpLookups.length).toBe(0); expect(httpLookups.length).toBe(0);
}); });
xit("should be able to peek into a room using peekInRoom", function(done) { xit("should be able to peek into a room using peekInRoom", function (done) {});
});
}); });
describe("getPresence", function() { describe("getPresence", function () {
it("should send a presence HTTP GET", function() { it("should send a presence HTTP GET", function () {
httpLookups = [{ httpLookups = [
method: "GET", {
path: `/presence/${encodeURIComponent(userId)}/status`, method: "GET",
data: { path: `/presence/${encodeURIComponent(userId)}/status`,
"presence": "unavailable", data: {
"last_active_ago": 420845, presence: "unavailable",
last_active_ago: 420845,
},
}, },
}]; ];
client.getPresence(userId); client.getPresence(userId);
expect(httpLookups.length).toEqual(0); expect(httpLookups.length).toEqual(0);
}); });
@ -970,11 +1013,13 @@ describe("MatrixClient", function() {
it("overload without threadId works", async () => { it("overload without threadId works", async () => {
const eventId = "$eventId:example.org"; const eventId = "$eventId:example.org";
const txnId = client.makeTxnId(); const txnId = client.makeTxnId();
httpLookups = [{ httpLookups = [
method: "PUT", {
path: `/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${txnId}`, method: "PUT",
data: { event_id: eventId }, path: `/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${txnId}`,
}]; data: { event_id: eventId },
},
];
await client.redactEvent(roomId, eventId, txnId); await client.redactEvent(roomId, eventId, txnId);
}); });
@ -982,11 +1027,13 @@ describe("MatrixClient", function() {
it("overload with null threadId works", async () => { it("overload with null threadId works", async () => {
const eventId = "$eventId:example.org"; const eventId = "$eventId:example.org";
const txnId = client.makeTxnId(); const txnId = client.makeTxnId();
httpLookups = [{ httpLookups = [
method: "PUT", {
path: `/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${txnId}`, method: "PUT",
data: { event_id: eventId }, path: `/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${txnId}`,
}]; data: { event_id: eventId },
},
];
await client.redactEvent(roomId, null, eventId, txnId); await client.redactEvent(roomId, null, eventId, txnId);
}); });
@ -994,11 +1041,13 @@ describe("MatrixClient", function() {
it("overload with threadId works", async () => { it("overload with threadId works", async () => {
const eventId = "$eventId:example.org"; const eventId = "$eventId:example.org";
const txnId = client.makeTxnId(); const txnId = client.makeTxnId();
httpLookups = [{ httpLookups = [
method: "PUT", {
path: `/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${txnId}`, method: "PUT",
data: { event_id: eventId }, path: `/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${txnId}`,
}]; data: { event_id: eventId },
},
];
await client.redactEvent(roomId, "$threadId:server", eventId, txnId); await client.redactEvent(roomId, "$threadId:server", eventId, txnId);
}); });
@ -1007,12 +1056,14 @@ describe("MatrixClient", function() {
const eventId = "$eventId:example.org"; const eventId = "$eventId:example.org";
const txnId = client.makeTxnId(); const txnId = client.makeTxnId();
const reason = "This is the redaction reason"; const reason = "This is the redaction reason";
httpLookups = [{ httpLookups = [
method: "PUT", {
path: `/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${txnId}`, method: "PUT",
expectBody: { reason }, // NOT ENCRYPTED path: `/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${txnId}`,
data: { event_id: eventId }, expectBody: { reason }, // NOT ENCRYPTED
}]; data: { event_id: eventId },
},
];
await client.redactEvent(roomId, eventId, txnId, { reason }); await client.redactEvent(roomId, eventId, txnId, { reason });
}); });
@ -1058,7 +1109,8 @@ describe("MatrixClient", function() {
expect(getRoomId).toEqual(roomId); expect(getRoomId).toEqual(roomId);
return mockRoom; return mockRoom;
}; };
client.crypto = { // mock crypto client.crypto = {
// mock crypto
encryptEvent: () => new Promise(() => {}), encryptEvent: () => new Promise(() => {}),
stop: jest.fn(), stop: jest.fn(),
} as unknown as Crypto; } as unknown as Crypto;
@ -1067,7 +1119,7 @@ describe("MatrixClient", function() {
function assertCancelled() { function assertCancelled() {
expect(event.status).toBe(EventStatus.CANCELLED); expect(event.status).toBe(EventStatus.CANCELLED);
expect(client.scheduler?.removeEventFromQueue(event)).toBeFalsy(); expect(client.scheduler?.removeEventFromQueue(event)).toBeFalsy();
expect(httpLookups.filter(h => h.path.includes("/send/")).length).toBe(0); expect(httpLookups.filter((h) => h.path.includes("/send/")).length).toBe(0);
} }
it("should cancel an event which is queued", () => { it("should cancel an event which is queued", () => {
@ -1105,22 +1157,22 @@ describe("MatrixClient", function() {
const room = new Room("!room1:matrix.org", client, userId); const room = new Room("!room1:matrix.org", client, userId);
const rootEvent = new MatrixEvent({ const rootEvent = new MatrixEvent({
"content": {}, content: {},
"origin_server_ts": 1, origin_server_ts: 1,
"room_id": "!room1:matrix.org", room_id: "!room1:matrix.org",
"sender": "@alice:matrix.org", sender: "@alice:matrix.org",
"type": "m.room.message", type: "m.room.message",
"unsigned": { unsigned: {
"m.relations": { "m.relations": {
"m.thread": { "m.thread": {
"latest_event": {}, latest_event: {},
"count": 33, count: 33,
"current_user_participated": false, current_user_participated: false,
}, },
}, },
}, },
"event_id": "$ev1", event_id: "$ev1",
"user_id": "@alice:matrix.org", user_id: "@alice:matrix.org",
}); });
expect(rootEvent.isThreadRoot).toBe(true); expect(rootEvent.isThreadRoot).toBe(true);
@ -1145,12 +1197,7 @@ describe("MatrixClient", function() {
const rpEvent = new MatrixEvent({ event_id: "read_private_event_id" }); const rpEvent = new MatrixEvent({ event_id: "read_private_event_id" });
client.getRoom = () => room; client.getRoom = () => room;
client.setRoomReadMarkers( client.setRoomReadMarkers("room_id", "read_marker_event_id", rrEvent, rpEvent);
"room_id",
"read_marker_event_id",
rrEvent,
rpEvent,
);
expect(client.setRoomReadMarkersHttpRequest).toHaveBeenCalledWith( expect(client.setRoomReadMarkersHttpRequest).toHaveBeenCalledWith(
"room_id", "room_id",
@ -1175,7 +1222,7 @@ describe("MatrixClient", function() {
}); });
describe("beacons", () => { describe("beacons", () => {
const roomId = '!room:server.org'; const roomId = "!room:server.org";
const content = makeBeaconInfoContent(100, true); const content = makeBeaconInfoContent(100, true);
beforeEach(() => { beforeEach(() => {
@ -1188,10 +1235,10 @@ describe("MatrixClient", function() {
// event type combined // event type combined
const expectedEventType = M_BEACON_INFO.name; const expectedEventType = M_BEACON_INFO.name;
const [method, path, queryParams, requestContent] = mocked(client.http.authedRequest).mock.calls[0]; const [method, path, queryParams, requestContent] = mocked(client.http.authedRequest).mock.calls[0];
expect(method).toBe('PUT'); expect(method).toBe("PUT");
expect(path).toEqual( expect(path).toEqual(
`/rooms/${encodeURIComponent(roomId)}/state/` + `/rooms/${encodeURIComponent(roomId)}/state/` +
`${encodeURIComponent(expectedEventType)}/${encodeURIComponent(userId)}`, `${encodeURIComponent(expectedEventType)}/${encodeURIComponent(userId)}`,
); );
expect(queryParams).toBeFalsy(); expect(queryParams).toBeFalsy();
expect(requestContent).toEqual(content); expect(requestContent).toEqual(content);
@ -1204,31 +1251,31 @@ describe("MatrixClient", function() {
const [, path, , requestContent] = mocked(client.http.authedRequest).mock.calls[0]; const [, path, , requestContent] = mocked(client.http.authedRequest).mock.calls[0];
expect(path).toEqual( expect(path).toEqual(
`/rooms/${encodeURIComponent(roomId)}/state/` + `/rooms/${encodeURIComponent(roomId)}/state/` +
`${encodeURIComponent(M_BEACON_INFO.name)}/${encodeURIComponent(userId)}`, `${encodeURIComponent(M_BEACON_INFO.name)}/${encodeURIComponent(userId)}`,
); );
expect(requestContent).toEqual(content); expect(requestContent).toEqual(content);
}); });
describe('processBeaconEvents()', () => { describe("processBeaconEvents()", () => {
it('does nothing when events is falsy', () => { it("does nothing when events is falsy", () => {
const room = new Room(roomId, client, userId); const room = new Room(roomId, client, userId);
const roomStateProcessSpy = jest.spyOn(room.currentState, 'processBeaconEvents'); const roomStateProcessSpy = jest.spyOn(room.currentState, "processBeaconEvents");
client.processBeaconEvents(room, undefined); client.processBeaconEvents(room, undefined);
expect(roomStateProcessSpy).not.toHaveBeenCalled(); expect(roomStateProcessSpy).not.toHaveBeenCalled();
}); });
it('does nothing when events is of length 0', () => { it("does nothing when events is of length 0", () => {
const room = new Room(roomId, client, userId); const room = new Room(roomId, client, userId);
const roomStateProcessSpy = jest.spyOn(room.currentState, 'processBeaconEvents'); const roomStateProcessSpy = jest.spyOn(room.currentState, "processBeaconEvents");
client.processBeaconEvents(room, []); client.processBeaconEvents(room, []);
expect(roomStateProcessSpy).not.toHaveBeenCalled(); expect(roomStateProcessSpy).not.toHaveBeenCalled();
}); });
it('calls room states processBeaconEvents with events', () => { it("calls room states processBeaconEvents with events", () => {
const room = new Room(roomId, client, userId); const room = new Room(roomId, client, userId);
const roomStateProcessSpy = jest.spyOn(room.currentState, 'processBeaconEvents'); const roomStateProcessSpy = jest.spyOn(room.currentState, "processBeaconEvents");
const messageEvent = testUtils.mkMessage({ room: roomId, user: userId, event: true }); const messageEvent = testUtils.mkMessage({ room: roomId, user: userId, event: true });
const beaconEvent = makeBeaconEvent(userId); const beaconEvent = makeBeaconEvent(userId);
@ -1242,14 +1289,13 @@ describe("MatrixClient", function() {
describe("setRoomTopic", () => { describe("setRoomTopic", () => {
const roomId = "!foofoofoofoofoofoo:matrix.org"; const roomId = "!foofoofoofoofoofoo:matrix.org";
const createSendStateEventMock = (topic: string, htmlTopic?: string) => { const createSendStateEventMock = (topic: string, htmlTopic?: string) => {
return jest.fn() return jest.fn().mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => { expect(roomId).toEqual(roomId);
expect(roomId).toEqual(roomId); expect(eventType).toEqual(EventType.RoomTopic);
expect(eventType).toEqual(EventType.RoomTopic); expect(content).toMatchObject(ContentHelpers.makeTopicContent(topic, htmlTopic));
expect(content).toMatchObject(ContentHelpers.makeTopicContent(topic, htmlTopic)); expect(stateKey).toBeUndefined();
expect(stateKey).toBeUndefined(); return Promise.resolve();
return Promise.resolve(); });
});
}; };
it("is called with plain text topic and sends state event", async () => { it("is called with plain text topic and sends state event", async () => {
@ -1275,13 +1321,13 @@ describe("MatrixClient", function() {
}); });
describe("setPassword", () => { describe("setPassword", () => {
const auth = { session: 'abcdef', type: 'foo' }; const auth = { session: "abcdef", type: "foo" };
const newPassword = 'newpassword'; const newPassword = "newpassword";
const passwordTest = (expectedRequestContent: any) => { const passwordTest = (expectedRequestContent: any) => {
const [method, path, queryParams, requestContent] = mocked(client.http.authedRequest).mock.calls[0]; const [method, path, queryParams, requestContent] = mocked(client.http.authedRequest).mock.calls[0];
expect(method).toBe('POST'); expect(method).toBe("POST");
expect(path).toEqual('/account/password'); expect(path).toEqual("/account/password");
expect(queryParams).toBeFalsy(); expect(queryParams).toBeFalsy();
expect(requestContent).toEqual(expectedRequestContent); expect(requestContent).toEqual(expectedRequestContent);
}; };
@ -1334,7 +1380,7 @@ describe("MatrixClient", function() {
// Current version of the endpoint we support is v3 // Current version of the endpoint we support is v3
const [method, path, queryParams, data, opts] = mocked(client.http.authedRequest).mock.calls[0]; const [method, path, queryParams, data, opts] = mocked(client.http.authedRequest).mock.calls[0];
expect(data).toBeFalsy(); expect(data).toBeFalsy();
expect(method).toBe('GET'); expect(method).toBe("GET");
expect(path).toEqual(`/rooms/${encodeURIComponent(roomId)}/aliases`); expect(path).toEqual(`/rooms/${encodeURIComponent(roomId)}/aliases`);
expect(opts).toMatchObject({ prefix: "/_matrix/client/v3" }); expect(opts).toMatchObject({ prefix: "/_matrix/client/v3" });
expect(queryParams).toBeFalsy(); expect(queryParams).toBeFalsy();
@ -1397,11 +1443,17 @@ describe("MatrixClient", function() {
client.on(ClientEvent.TurnServers, onTurnServers); client.on(ClientEvent.TurnServers, onTurnServers);
expect(await client.checkTurnServers()).toBe(true); expect(await client.checkTurnServers()).toBe(true);
client.off(ClientEvent.TurnServers, onTurnServers); client.off(ClientEvent.TurnServers, onTurnServers);
expect(events).toEqual([[[{ expect(events).toEqual([
urls: turnServer.uris, [
username: turnServer.username, [
credential: turnServer.password, {
}]]]); urls: turnServer.uris,
username: turnServer.username,
credential: turnServer.password,
},
],
],
]);
}); });
it("emits an event when an error occurs", async () => { it("emits an event when an error occurs", async () => {
@ -1448,11 +1500,11 @@ describe("MatrixClient", function() {
beforeEach(() => { beforeEach(() => {
// Mockup `getAccountData`/`setAccountData`. // Mockup `getAccountData`/`setAccountData`.
const dataStore = new Map(); const dataStore = new Map();
client.setAccountData = function(eventType, content) { client.setAccountData = function (eventType, content) {
dataStore.set(eventType, content); dataStore.set(eventType, content);
return Promise.resolve({}); return Promise.resolve({});
}; };
client.getAccountData = function(eventType) { client.getAccountData = function (eventType) {
const data = dataStore.get(eventType); const data = dataStore.get(eventType);
return new MatrixEvent({ return new MatrixEvent({
content: data, content: data,
@ -1461,23 +1513,23 @@ describe("MatrixClient", function() {
// Mockup `createRoom`/`getRoom`/`joinRoom`, including state. // Mockup `createRoom`/`getRoom`/`joinRoom`, including state.
const rooms = new Map(); const rooms = new Map();
client.createRoom = function(options: Options = {}) { client.createRoom = function (options: Options = {}) {
const roomId = options["_roomId"] || `!room-${rooms.size}:example.org`; const roomId = options["_roomId"] || `!room-${rooms.size}:example.org`;
const state = new Map<string, any>(); const state = new Map<string, any>();
const room = { const room = {
roomId, roomId,
_options: options, _options: options,
_state: state, _state: state,
getUnfilteredTimelineSet: function() { getUnfilteredTimelineSet: function () {
return { return {
getLiveTimeline: function() { getLiveTimeline: function () {
return { return {
getState: function(direction) { getState: function (direction) {
expect(direction).toBe(EventTimeline.FORWARDS); expect(direction).toBe(EventTimeline.FORWARDS);
return { return {
getStateEvents: function(type) { getStateEvents: function (type) {
const store = state.get(type) || {}; const store = state.get(type) || {};
return Object.keys(store).map(key => store[key]); return Object.keys(store).map((key) => store[key]);
}, },
}; };
}, },
@ -1489,15 +1541,15 @@ describe("MatrixClient", function() {
rooms.set(roomId, room); rooms.set(roomId, room);
return Promise.resolve({ room_id: roomId }); return Promise.resolve({ room_id: roomId });
}; };
client.getRoom = function(roomId) { client.getRoom = function (roomId) {
return rooms.get(roomId); return rooms.get(roomId);
}; };
client.joinRoom = async function(roomId) { client.joinRoom = async function (roomId) {
return this.getRoom(roomId)! || this.createRoom({ _roomId: roomId } as ICreateRoomOpts); return this.getRoom(roomId)! || this.createRoom({ _roomId: roomId } as ICreateRoomOpts);
}; };
// Mockup state events // Mockup state events
client.sendStateEvent = function(roomId, type, content) { client.sendStateEvent = function (roomId, type, content) {
const room = this.getRoom(roomId) as WrappedRoom; const room = this.getRoom(roomId) as WrappedRoom;
const state: Map<string, any> = room._state; const state: Map<string, any> = room._state;
let store = state.get(type); let store = state.get(type);
@ -1507,19 +1559,19 @@ describe("MatrixClient", function() {
} }
const eventId = `$event-${Math.random()}:example.org`; const eventId = `$event-${Math.random()}:example.org`;
store[eventId] = { store[eventId] = {
getId: function() { getId: function () {
return eventId; return eventId;
}, },
getRoomId: function() { getRoomId: function () {
return roomId; return roomId;
}, },
getContent: function() { getContent: function () {
return content; return content;
}, },
}; };
return Promise.resolve({ event_id: eventId }); return Promise.resolve({ event_id: eventId });
}; };
client.redactEvent = function(roomId, eventId) { client.redactEvent = function (roomId, eventId) {
const room = this.getRoom(roomId) as WrappedRoom; const room = this.getRoom(roomId) as WrappedRoom;
const state: Map<string, any> = room._state; const state: Map<string, any> = room._state;
for (const store of state.values()) { for (const store of state.values()) {

View File

@ -50,23 +50,27 @@ describe("MSC3089Branch", () => {
} }
}, },
}; };
indexEvent = ({ indexEvent = {
getRoomId: () => branchRoomId, getRoomId: () => branchRoomId,
getStateKey: () => fileEventId, getStateKey: () => fileEventId,
}); };
directory = new MSC3089TreeSpace(client, branchRoomId); directory = new MSC3089TreeSpace(client, branchRoomId);
branch = new MSC3089Branch(client, indexEvent, directory); branch = new MSC3089Branch(client, indexEvent, directory);
branch2 = new MSC3089Branch(client, { branch2 = new MSC3089Branch(
getRoomId: () => branchRoomId, client,
getStateKey: () => fileEventId2, {
} as MatrixEvent, directory); getRoomId: () => branchRoomId,
getStateKey: () => fileEventId2,
} as MatrixEvent,
directory,
);
}); });
it('should know the file event ID', () => { it("should know the file event ID", () => {
expect(branch.id).toEqual(fileEventId); expect(branch.id).toEqual(fileEventId);
}); });
it('should know if the file is active or not', () => { it("should know if the file is active or not", () => {
indexEvent.getContent = () => ({}); indexEvent.getContent = () => ({});
expect(branch.isActive).toBe(false); expect(branch.isActive).toBe(false);
indexEvent.getContent = () => ({ active: false }); indexEvent.getContent = () => ({ active: false });
@ -77,15 +81,16 @@ describe("MSC3089Branch", () => {
expect(branch.isActive).toBe(false); expect(branch.isActive).toBe(false);
}); });
it('should be able to delete the file', async () => { it("should be able to delete the file", async () => {
const eventIdOrder = [fileEventId, fileEventId2]; const eventIdOrder = [fileEventId, fileEventId2];
const stateFn = jest.fn() const stateFn = jest
.fn()
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => { .mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
expect(roomId).toEqual(branchRoomId); expect(roomId).toEqual(branchRoomId);
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value
expect(content).toMatchObject({}); expect(content).toMatchObject({});
expect(content['active']).toBeUndefined(); expect(content["active"]).toBeUndefined();
expect(stateKey).toEqual(eventIdOrder[stateFn.mock.calls.length - 1]); expect(stateKey).toEqual(eventIdOrder[stateFn.mock.calls.length - 1]);
return Promise.resolve(); // return value not used return Promise.resolve(); // return value not used
@ -109,7 +114,7 @@ describe("MSC3089Branch", () => {
expect(redactFn).toHaveBeenCalledTimes(2); expect(redactFn).toHaveBeenCalledTimes(2);
}); });
it('should know its name', async () => { it("should know its name", async () => {
const name = "My File.txt"; const name = "My File.txt";
indexEvent.getContent = () => ({ active: true, name: name }); indexEvent.getContent = () => ({ active: true, name: name });
@ -118,10 +123,11 @@ describe("MSC3089Branch", () => {
expect(res).toEqual(name); expect(res).toEqual(name);
}); });
it('should be able to change its name', async () => { it("should be able to change its name", async () => {
const name = "My File.txt"; const name = "My File.txt";
indexEvent.getContent = () => ({ active: true, retained: true }); indexEvent.getContent = () => ({ active: true, retained: true });
const stateFn = jest.fn() const stateFn = jest
.fn()
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => { .mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
expect(roomId).toEqual(branchRoomId); expect(roomId).toEqual(branchRoomId);
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value
@ -141,7 +147,7 @@ describe("MSC3089Branch", () => {
expect(stateFn).toHaveBeenCalledTimes(1); expect(stateFn).toHaveBeenCalledTimes(1);
}); });
it('should be v1 by default', () => { it("should be v1 by default", () => {
indexEvent.getContent = () => ({ active: true }); indexEvent.getContent = () => ({ active: true });
const res = branch.version; const res = branch.version;
@ -149,7 +155,7 @@ describe("MSC3089Branch", () => {
expect(res).toEqual(1); expect(res).toEqual(1);
}); });
it('should be vN when set', () => { it("should be vN when set", () => {
indexEvent.getContent = () => ({ active: true, version: 3 }); indexEvent.getContent = () => ({ active: true, version: 3 });
const res = branch.version; const res = branch.version;
@ -157,7 +163,7 @@ describe("MSC3089Branch", () => {
expect(res).toEqual(3); expect(res).toEqual(3);
}); });
it('should be unlocked by default', async () => { it("should be unlocked by default", async () => {
indexEvent.getContent = () => ({ active: true }); indexEvent.getContent = () => ({ active: true });
const res = branch.isLocked(); const res = branch.isLocked();
@ -165,7 +171,7 @@ describe("MSC3089Branch", () => {
expect(res).toEqual(false); expect(res).toEqual(false);
}); });
it('should use lock status from index event', async () => { it("should use lock status from index event", async () => {
indexEvent.getContent = () => ({ active: true, locked: true }); indexEvent.getContent = () => ({ active: true, locked: true });
const res = branch.isLocked(); const res = branch.isLocked();
@ -173,10 +179,11 @@ describe("MSC3089Branch", () => {
expect(res).toEqual(true); expect(res).toEqual(true);
}); });
it('should be able to change its locked status', async () => { it("should be able to change its locked status", async () => {
const locked = true; const locked = true;
indexEvent.getContent = () => ({ active: true, retained: true }); indexEvent.getContent = () => ({ active: true, retained: true });
const stateFn = jest.fn() const stateFn = jest
.fn()
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => { .mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
expect(roomId).toEqual(branchRoomId); expect(roomId).toEqual(branchRoomId);
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value
@ -196,16 +203,17 @@ describe("MSC3089Branch", () => {
expect(stateFn).toHaveBeenCalledTimes(1); expect(stateFn).toHaveBeenCalledTimes(1);
}); });
it('should be able to return event information', async () => { it("should be able to return event information", async () => {
const mxcLatter = "example.org/file"; const mxcLatter = "example.org/file";
const fileContent = { isFile: "not quite", url: "mxc://" + mxcLatter }; const fileContent = { isFile: "not quite", url: "mxc://" + mxcLatter };
const fileEvent = { getId: () => fileEventId, getOriginalContent: () => ({ file: fileContent }) }; const fileEvent = { getId: () => fileEventId, getOriginalContent: () => ({ file: fileContent }) };
staticRoom.getUnfilteredTimelineSet = () => ({ staticRoom.getUnfilteredTimelineSet = () =>
findEventById: (eventId) => { ({
expect(eventId).toEqual(fileEventId); findEventById: (eventId) => {
return fileEvent; expect(eventId).toEqual(fileEventId);
}, return fileEvent;
}) as EventTimelineSet; },
} as EventTimelineSet);
client.mxcUrlToHttp = (mxc: string) => { client.mxcUrlToHttp = (mxc: string) => {
expect(mxc).toEqual("mxc://" + mxcLatter); expect(mxc).toEqual("mxc://" + mxcLatter);
return `https://example.org/_matrix/media/v1/download/${mxcLatter}`; return `https://example.org/_matrix/media/v1/download/${mxcLatter}`;
@ -217,20 +225,21 @@ describe("MSC3089Branch", () => {
expect(res).toMatchObject({ expect(res).toMatchObject({
info: fileContent, info: fileContent,
// Escape regex from MDN guides: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions // Escape regex from MDN guides: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
httpUrl: expect.stringMatching(`.+${mxcLatter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`), httpUrl: expect.stringMatching(`.+${mxcLatter.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`),
}); });
}); });
it('should be able to return the event object', async () => { it("should be able to return the event object", async () => {
const mxcLatter = "example.org/file"; const mxcLatter = "example.org/file";
const fileContent = { isFile: "not quite", url: "mxc://" + mxcLatter }; const fileContent = { isFile: "not quite", url: "mxc://" + mxcLatter };
const fileEvent = { getId: () => fileEventId, getOriginalContent: () => ({ file: fileContent }) }; const fileEvent = { getId: () => fileEventId, getOriginalContent: () => ({ file: fileContent }) };
staticRoom.getUnfilteredTimelineSet = () => ({ staticRoom.getUnfilteredTimelineSet = () =>
findEventById: (eventId) => { ({
expect(eventId).toEqual(fileEventId); findEventById: (eventId) => {
return fileEvent; expect(eventId).toEqual(fileEventId);
}, return fileEvent;
}) as EventTimelineSet; },
} as EventTimelineSet);
client.mxcUrlToHttp = (mxc: string) => { client.mxcUrlToHttp = (mxc: string) => {
expect(mxc).toEqual("mxc://" + mxcLatter); expect(mxc).toEqual("mxc://" + mxcLatter);
return `https://example.org/_matrix/media/v1/download/${mxcLatter}`; return `https://example.org/_matrix/media/v1/download/${mxcLatter}`;
@ -242,14 +251,15 @@ describe("MSC3089Branch", () => {
expect(res).toBe(fileEvent); expect(res).toBe(fileEvent);
}); });
it('should create new versions of itself', async () => { it("should create new versions of itself", async () => {
const canaryName = "canary"; const canaryName = "canary";
const canaryContents = "contents go here"; const canaryContents = "contents go here";
const canaryFile = {} as IEncryptedFile; const canaryFile = {} as IEncryptedFile;
const canaryAddl = { canary: true }; const canaryAddl = { canary: true };
indexEvent.getContent = () => ({ active: true, retained: true }); indexEvent.getContent = () => ({ active: true, retained: true });
const stateKeyOrder = [fileEventId2, fileEventId]; const stateKeyOrder = [fileEventId2, fileEventId];
const stateFn = jest.fn() const stateFn = jest
.fn()
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => { .mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
expect(roomId).toEqual(branchRoomId); expect(roomId).toEqual(branchRoomId);
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value
@ -273,26 +283,25 @@ describe("MSC3089Branch", () => {
}); });
client.sendStateEvent = stateFn; client.sendStateEvent = stateFn;
const createFn = jest.fn().mockImplementation(( const createFn = jest
name: string, .fn()
contents: ArrayBuffer, .mockImplementation(
info: Partial<IEncryptedFile>, (name: string, contents: ArrayBuffer, info: Partial<IEncryptedFile>, addl: IContent) => {
addl: IContent, expect(name).toEqual(canaryName);
) => { expect(contents).toBe(canaryContents);
expect(name).toEqual(canaryName); expect(info).toBe(canaryFile);
expect(contents).toBe(canaryContents); expect(addl).toMatchObject({
expect(info).toBe(canaryFile); ...canaryAddl,
expect(addl).toMatchObject({ "m.new_content": true,
...canaryAddl, "m.relates_to": {
"m.new_content": true, rel_type: RelationType.Replace,
"m.relates_to": { event_id: fileEventId,
"rel_type": RelationType.Replace, },
"event_id": fileEventId, });
},
});
return Promise.resolve({ event_id: fileEventId2 }); return Promise.resolve({ event_id: fileEventId2 });
}); },
);
directory.createFile = createFn; directory.createFile = createFn;
await branch.createNewVersion(canaryName, canaryContents, canaryFile, canaryAddl); await branch.createNewVersion(canaryName, canaryContents, canaryFile, canaryAddl);
@ -301,21 +310,27 @@ describe("MSC3089Branch", () => {
expect(createFn).toHaveBeenCalledTimes(1); expect(createFn).toHaveBeenCalledTimes(1);
}); });
it('should fetch file history', async () => { it("should fetch file history", async () => {
branch2.getFileEvent = () => Promise.resolve({ branch2.getFileEvent = () =>
replacingEventId: () => undefined, Promise.resolve({
getId: () => fileEventId2, replacingEventId: () => undefined,
} as MatrixEvent); getId: () => fileEventId2,
branch.getFileEvent = () => Promise.resolve({ } as MatrixEvent);
replacingEventId: () => fileEventId2, branch.getFileEvent = () =>
getId: () => fileEventId, Promise.resolve({
} as MatrixEvent); replacingEventId: () => fileEventId2,
getId: () => fileEventId,
} as MatrixEvent);
const events = [await branch.getFileEvent(), await branch2.getFileEvent(), { const events = [
replacingEventId: (): string | undefined => undefined, await branch.getFileEvent(),
getId: () => "$unknown", await branch2.getFileEvent(),
}]; {
staticRoom.getLiveTimeline = () => ({ getEvents: () => events }) as EventTimeline; replacingEventId: (): string | undefined => undefined,
getId: () => "$unknown",
},
];
staticRoom.getLiveTimeline = () => ({ getEvents: () => events } as EventTimeline);
directory.getFile = (evId: string) => { directory.getFile = (evId: string) => {
expect(evId).toEqual(fileEventId); expect(evId).toEqual(fileEventId);
@ -323,9 +338,6 @@ describe("MSC3089Branch", () => {
}; };
const results = await branch2.getVersionHistory(); const results = await branch2.getVersionHistory();
expect(results).toMatchObject([ expect(results).toMatchObject([branch2, branch]);
branch2,
branch,
]);
}); });
}); });

View File

@ -72,18 +72,19 @@ describe("MSC3089TreeSpace", () => {
}); });
} }
it('should populate the room reference', () => { it("should populate the room reference", () => {
expect(tree.room).toBe(room); expect(tree.room).toBe(room);
}); });
it('should proxy the ID member to room ID', () => { it("should proxy the ID member to room ID", () => {
expect(tree.id).toEqual(tree.roomId); expect(tree.id).toEqual(tree.roomId);
expect(tree.id).toEqual(roomId); expect(tree.id).toEqual(roomId);
}); });
it('should support setting the name of the space', async () => { it("should support setting the name of the space", async () => {
const newName = "NEW NAME"; const newName = "NEW NAME";
const fn = jest.fn() const fn = jest
.fn()
.mockImplementation((stateRoomId: string, eventType: EventType, content: any, stateKey: string) => { .mockImplementation((stateRoomId: string, eventType: EventType, content: any, stateKey: string) => {
expect(stateRoomId).toEqual(roomId); expect(stateRoomId).toEqual(roomId);
expect(eventType).toEqual(EventType.RoomName); expect(eventType).toEqual(EventType.RoomName);
@ -96,7 +97,7 @@ describe("MSC3089TreeSpace", () => {
expect(fn).toHaveBeenCalledTimes(1); expect(fn).toHaveBeenCalledTimes(1);
}); });
it('should support inviting users to the space', async () => { it("should support inviting users to the space", async () => {
const target = targetUser; const target = targetUser;
const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => { const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => {
expect(inviteRoomId).toEqual(roomId); expect(inviteRoomId).toEqual(roomId);
@ -108,7 +109,7 @@ describe("MSC3089TreeSpace", () => {
expect(fn).toHaveBeenCalledTimes(1); expect(fn).toHaveBeenCalledTimes(1);
}); });
it('should retry invites to the space', async () => { it("should retry invites to the space", async () => {
const target = targetUser; const target = targetUser;
const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => { const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => {
expect(inviteRoomId).toEqual(roomId); expect(inviteRoomId).toEqual(roomId);
@ -121,7 +122,7 @@ describe("MSC3089TreeSpace", () => {
expect(fn).toHaveBeenCalledTimes(2); expect(fn).toHaveBeenCalledTimes(2);
}); });
it('should not retry invite permission errors', async () => { it("should not retry invite permission errors", async () => {
const target = targetUser; const target = targetUser;
const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => { const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => {
expect(inviteRoomId).toEqual(roomId); expect(inviteRoomId).toEqual(roomId);
@ -141,7 +142,7 @@ describe("MSC3089TreeSpace", () => {
expect(fn).toHaveBeenCalledTimes(1); expect(fn).toHaveBeenCalledTimes(1);
}); });
it('should invite to subspaces', async () => { it("should invite to subspaces", async () => {
const target = targetUser; const target = targetUser;
const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => { const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => {
expect(inviteRoomId).toEqual(roomId); expect(inviteRoomId).toEqual(roomId);
@ -162,7 +163,7 @@ describe("MSC3089TreeSpace", () => {
expect(fn).toHaveBeenCalledTimes(4); expect(fn).toHaveBeenCalledTimes(4);
}); });
it('should share keys with invitees', async () => { it("should share keys with invitees", async () => {
const target = targetUser; const target = targetUser;
const sendKeysFn = jest.fn().mockImplementation((inviteRoomId: string, userIds: string[]) => { const sendKeysFn = jest.fn().mockImplementation((inviteRoomId: string, userIds: string[]) => {
expect(inviteRoomId).toEqual(roomId); expect(inviteRoomId).toEqual(roomId);
@ -190,7 +191,7 @@ describe("MSC3089TreeSpace", () => {
expect(historyFn).toHaveBeenCalledTimes(1); expect(historyFn).toHaveBeenCalledTimes(1);
}); });
it('should not share keys with invitees if inappropriate history visibility', async () => { it("should not share keys with invitees if inappropriate history visibility", async () => {
const target = targetUser; const target = targetUser;
const sendKeysFn = jest.fn().mockImplementation((inviteRoomId: string, userIds: string[]) => { const sendKeysFn = jest.fn().mockImplementation((inviteRoomId: string, userIds: string[]) => {
expect(inviteRoomId).toEqual(roomId); expect(inviteRoomId).toEqual(roomId);
@ -215,7 +216,8 @@ describe("MSC3089TreeSpace", () => {
async function evaluatePowerLevels(pls: any, role: TreePermissions, expectedPl: number) { async function evaluatePowerLevels(pls: any, role: TreePermissions, expectedPl: number) {
makePowerLevels(pls); makePowerLevels(pls);
const fn = jest.fn() const fn = jest
.fn()
.mockImplementation((stateRoomId: string, eventType: EventType, content: any, stateKey: string) => { .mockImplementation((stateRoomId: string, eventType: EventType, content: any, stateKey: string) => {
expect(stateRoomId).toEqual(roomId); expect(stateRoomId).toEqual(roomId);
expect(eventType).toEqual(EventType.RoomPowerLevels); expect(eventType).toEqual(EventType.RoomPowerLevels);
@ -240,80 +242,100 @@ describe("MSC3089TreeSpace", () => {
expect(finalPermissions).toEqual(role); expect(finalPermissions).toEqual(role);
} }
it('should support setting Viewer permissions', () => { it("should support setting Viewer permissions", () => {
return evaluatePowerLevels({ return evaluatePowerLevels(
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE, {
users_default: 1024, ...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
events_default: 1025, users_default: 1024,
events: { events_default: 1025,
[EventType.RoomPowerLevels]: 1026, events: {
[EventType.RoomPowerLevels]: 1026,
},
}, },
}, TreePermissions.Viewer, 1024); TreePermissions.Viewer,
1024,
);
}); });
it('should support setting Editor permissions', () => { it("should support setting Editor permissions", () => {
return evaluatePowerLevels({ return evaluatePowerLevels(
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE, {
users_default: 1024, ...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
events_default: 1025, users_default: 1024,
events: { events_default: 1025,
[EventType.RoomPowerLevels]: 1026, events: {
[EventType.RoomPowerLevels]: 1026,
},
}, },
}, TreePermissions.Editor, 1025); TreePermissions.Editor,
1025,
);
}); });
it('should support setting Owner permissions', () => { it("should support setting Owner permissions", () => {
return evaluatePowerLevels({ return evaluatePowerLevels(
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE, {
users_default: 1024, ...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
events_default: 1025, users_default: 1024,
events: { events_default: 1025,
[EventType.RoomPowerLevels]: 1026, events: {
[EventType.RoomPowerLevels]: 1026,
},
}, },
}, TreePermissions.Owner, 1026); TreePermissions.Owner,
1026,
);
}); });
it('should support demoting permissions', () => { it("should support demoting permissions", () => {
return evaluatePowerLevels({ return evaluatePowerLevels(
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE, {
users_default: 1024, ...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
events_default: 1025, users_default: 1024,
events: { events_default: 1025,
[EventType.RoomPowerLevels]: 1026, events: {
[EventType.RoomPowerLevels]: 1026,
},
users: {
[targetUser]: 2222,
},
}, },
users: { TreePermissions.Viewer,
[targetUser]: 2222, 1024,
}, );
}, TreePermissions.Viewer, 1024);
}); });
it('should support promoting permissions', () => { it("should support promoting permissions", () => {
return evaluatePowerLevels({ return evaluatePowerLevels(
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE, {
users_default: 1024, ...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
events_default: 1025, users_default: 1024,
events: { events_default: 1025,
[EventType.RoomPowerLevels]: 1026, events: {
[EventType.RoomPowerLevels]: 1026,
},
users: {
[targetUser]: 5,
},
}, },
users: { TreePermissions.Editor,
[targetUser]: 5, 1025,
}, );
}, TreePermissions.Editor, 1025);
}); });
it('should support defaults: Viewer', () => { it("should support defaults: Viewer", () => {
return evaluatePowerLevels({}, TreePermissions.Viewer, 0); return evaluatePowerLevels({}, TreePermissions.Viewer, 0);
}); });
it('should support defaults: Editor', () => { it("should support defaults: Editor", () => {
return evaluatePowerLevels({}, TreePermissions.Editor, 50); return evaluatePowerLevels({}, TreePermissions.Editor, 50);
}); });
it('should support defaults: Owner', () => { it("should support defaults: Owner", () => {
return evaluatePowerLevels({}, TreePermissions.Owner, 100); return evaluatePowerLevels({}, TreePermissions.Owner, 100);
}); });
it('should create subdirectories', async () => { it("should create subdirectories", async () => {
const subspaceName = "subdirectory"; const subspaceName = "subdirectory";
const subspaceId = "!subspace:localhost"; const subspaceId = "!subspace:localhost";
const domain = "domain.example.com"; const domain = "domain.example.com";
@ -331,7 +353,8 @@ describe("MSC3089TreeSpace", () => {
expect(name).toEqual(subspaceName); expect(name).toEqual(subspaceName);
return new MSC3089TreeSpace(client, subspaceId); return new MSC3089TreeSpace(client, subspaceId);
}); });
const sendStateFn = jest.fn() const sendStateFn = jest
.fn()
.mockImplementation(async (roomId: string, eventType: EventType, content: any, stateKey: string) => { .mockImplementation(async (roomId: string, eventType: EventType, content: any, stateKey: string) => {
expect([tree.roomId, subspaceId]).toContain(roomId); expect([tree.roomId, subspaceId]).toContain(roomId);
if (roomId === subspaceId) { if (roomId === subspaceId) {
@ -361,7 +384,7 @@ describe("MSC3089TreeSpace", () => {
expect(sendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, content, subspaceId); expect(sendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, content, subspaceId);
}); });
it('should find subdirectories', () => { it("should find subdirectories", () => {
const firstChildRoom = "!one:example.org"; const firstChildRoom = "!one:example.org";
const secondChildRoom = "!two:example.org"; const secondChildRoom = "!two:example.org";
const thirdChildRoom = "!three:example.org"; // to ensure it doesn't end up in the subdirectories const thirdChildRoom = "!three:example.org"; // to ensure it doesn't end up in the subdirectories
@ -399,7 +422,7 @@ describe("MSC3089TreeSpace", () => {
expect(getFn).toHaveBeenCalledWith(thirdChildRoom); // check to make sure it tried expect(getFn).toHaveBeenCalledWith(thirdChildRoom); // check to make sure it tried
}); });
it('should find specific directories', () => { it("should find specific directories", () => {
client.getRoom = () => ({} as Room); // to appease the TreeSpace constructor client.getRoom = () => ({} as Room); // to appease the TreeSpace constructor
// Only mocking used API // Only mocking used API
@ -415,7 +438,7 @@ describe("MSC3089TreeSpace", () => {
expect(result).toBeFalsy(); expect(result).toBeFalsy();
}); });
it('should be able to delete itself', async () => { it("should be able to delete itself", async () => {
const delete1 = jest.fn().mockImplementation(() => Promise.resolve()); const delete1 = jest.fn().mockImplementation(() => Promise.resolve());
const subdir1 = { delete: delete1 } as any as MSC3089TreeSpace; // mock tested bits const subdir1 = { delete: delete1 } as any as MSC3089TreeSpace; // mock tested bits
@ -466,7 +489,7 @@ describe("MSC3089TreeSpace", () => {
expect(leaveFn).toHaveBeenCalledTimes(1); expect(leaveFn).toHaveBeenCalledTimes(1);
}); });
describe('get and set order', () => { describe("get and set order", () => {
// Danger: these are partial implementations for testing purposes only // Danger: these are partial implementations for testing purposes only
// @ts-ignore - "MatrixEvent is a value but used as a type", which is true but not important // @ts-ignore - "MatrixEvent is a value but used as a type", which is true but not important
@ -483,7 +506,7 @@ describe("MSC3089TreeSpace", () => {
const content: IContent = { const content: IContent = {
via: [staticDomain], via: [staticDomain],
}; };
if (order) content['order'] = order; if (order) content["order"] = order;
parentState.push({ parentState.push({
getType: () => EventType.SpaceChild, getType: () => EventType.SpaceChild,
getStateKey: () => roomId, getStateKey: () => roomId,
@ -511,7 +534,7 @@ describe("MSC3089TreeSpace", () => {
} }
function expectOrder(childRoomId: string, order: number) { function expectOrder(childRoomId: string, order: number) {
const child = childTrees.find(c => c.roomId === childRoomId); const child = childTrees.find((c) => c.roomId === childRoomId);
expect(child).toBeDefined(); expect(child).toBeDefined();
expect(child!.getOrder()).toEqual(order); expect(child!.getOrder()).toEqual(order);
} }
@ -523,10 +546,10 @@ describe("MSC3089TreeSpace", () => {
expect([EventType.SpaceParent, EventType.RoomCreate]).toContain(eventType); expect([EventType.SpaceParent, EventType.RoomCreate]).toContain(eventType);
if (eventType === EventType.RoomCreate) { if (eventType === EventType.RoomCreate) {
expect(stateKey).toEqual(""); expect(stateKey).toEqual("");
return childState[roomId].find(e => e.getType() === EventType.RoomCreate); return childState[roomId].find((e) => e.getType() === EventType.RoomCreate);
} else { } else {
expect(stateKey).toBeUndefined(); expect(stateKey).toBeUndefined();
return childState[roomId].filter(e => e.getType() === eventType); return childState[roomId].filter((e) => e.getType() === eventType);
} }
}, },
}, },
@ -541,22 +564,22 @@ describe("MSC3089TreeSpace", () => {
roomId: tree.roomId, roomId: tree.roomId,
currentState: { currentState: {
getStateEvents: (eventType: EventType, stateKey?: string) => { getStateEvents: (eventType: EventType, stateKey?: string) => {
expect([ expect([EventType.SpaceChild, EventType.RoomCreate, EventType.SpaceParent]).toContain(
EventType.SpaceChild, eventType,
EventType.RoomCreate, );
EventType.SpaceParent,
]).toContain(eventType);
if (eventType === EventType.RoomCreate) { if (eventType === EventType.RoomCreate) {
expect(stateKey).toEqual(""); expect(stateKey).toEqual("");
return parentState.filter(e => e.getType() === EventType.RoomCreate)[0]; return parentState.filter((e) => e.getType() === EventType.RoomCreate)[0];
} else { } else {
if (stateKey !== undefined) { if (stateKey !== undefined) {
expect(Object.keys(rooms)).toContain(stateKey); expect(Object.keys(rooms)).toContain(stateKey);
expect(stateKey).not.toEqual(tree.roomId); expect(stateKey).not.toEqual(tree.roomId);
return parentState.find(e => e.getType() === eventType && e.getStateKey() === stateKey); return parentState.find(
(e) => e.getType() === eventType && e.getStateKey() === stateKey,
);
} // else fine } // else fine
return parentState.filter(e => e.getType() === eventType); return parentState.filter((e) => e.getType() === eventType);
} }
}, },
}, },
@ -567,18 +590,23 @@ describe("MSC3089TreeSpace", () => {
(<any>tree).room = parentRoom; // override readonly (<any>tree).room = parentRoom; // override readonly
client.getRoom = (r) => rooms[r ?? ""]; client.getRoom = (r) => rooms[r ?? ""];
clientSendStateFn = jest.fn() clientSendStateFn = jest
.fn()
.mockImplementation((roomId: string, eventType: EventType, content: any, stateKey: string) => { .mockImplementation((roomId: string, eventType: EventType, content: any, stateKey: string) => {
expect(roomId).toEqual(tree.roomId); expect(roomId).toEqual(tree.roomId);
expect(eventType).toEqual(EventType.SpaceChild); expect(eventType).toEqual(EventType.SpaceChild);
expect(content).toMatchObject(expect.objectContaining({ expect(content).toMatchObject(
via: expect.any(Array), expect.objectContaining({
order: expect.any(String), via: expect.any(Array),
})); order: expect.any(String),
}),
);
expect(Object.keys(rooms)).toContain(stateKey); expect(Object.keys(rooms)).toContain(stateKey);
expect(stateKey).not.toEqual(tree.roomId); expect(stateKey).not.toEqual(tree.roomId);
const stateEvent = parentState.find(e => e.getType() === eventType && e.getStateKey() === stateKey); const stateEvent = parentState.find(
(e) => e.getType() === eventType && e.getStateKey() === stateKey,
);
expect(stateEvent).toBeDefined(); expect(stateEvent).toBeDefined();
stateEvent.getContent = () => content; stateEvent.getContent = () => content;
@ -587,7 +615,7 @@ describe("MSC3089TreeSpace", () => {
client.sendStateEvent = clientSendStateFn; client.sendStateEvent = clientSendStateFn;
}); });
it('should know when something is top level', () => { it("should know when something is top level", () => {
const a = "!a:example.org"; const a = "!a:example.org";
addSubspace(a); addSubspace(a);
@ -595,12 +623,12 @@ describe("MSC3089TreeSpace", () => {
expect(childTrees[0].isTopLevel).toBe(false); // a bit of a hack to get at this, but it's fine expect(childTrees[0].isTopLevel).toBe(false); // a bit of a hack to get at this, but it's fine
}); });
it('should return -1 for top level spaces', () => { it("should return -1 for top level spaces", () => {
// The tree is what we've defined as top level, so it should work // The tree is what we've defined as top level, so it should work
expect(tree.getOrder()).toEqual(-1); expect(tree.getOrder()).toEqual(-1);
}); });
it('should throw when setting an order at the top level space', async () => { it("should throw when setting an order at the top level space", async () => {
try { try {
// The tree is what we've defined as top level, so it should work // The tree is what we've defined as top level, so it should work
await tree.setOrder(2); await tree.setOrder(2);
@ -612,7 +640,7 @@ describe("MSC3089TreeSpace", () => {
} }
}); });
it('should return a stable order for unordered children', () => { it("should return a stable order for unordered children", () => {
const a = "!a:example.org"; const a = "!a:example.org";
const b = "!b:example.org"; const b = "!b:example.org";
const c = "!c:example.org"; const c = "!c:example.org";
@ -627,7 +655,7 @@ describe("MSC3089TreeSpace", () => {
expectOrder(c, 2); expectOrder(c, 2);
}); });
it('should return a stable order for ordered children', () => { it("should return a stable order for ordered children", () => {
const a = "!a:example.org"; const a = "!a:example.org";
const b = "!b:example.org"; const b = "!b:example.org";
const c = "!c:example.org"; const c = "!c:example.org";
@ -642,7 +670,7 @@ describe("MSC3089TreeSpace", () => {
expectOrder(a, 2); expectOrder(a, 2);
}); });
it('should return a stable order for partially ordered children', () => { it("should return a stable order for partially ordered children", () => {
const a = "!a:example.org"; const a = "!a:example.org";
const b = "!b:example.org"; const b = "!b:example.org";
const c = "!c:example.org"; const c = "!c:example.org";
@ -660,7 +688,7 @@ describe("MSC3089TreeSpace", () => {
expectOrder(a, 2); expectOrder(a, 2);
}); });
it('should return a stable order if the create event timestamps are the same', () => { it("should return a stable order if the create event timestamps are the same", () => {
const a = "!a:example.org"; const a = "!a:example.org";
const b = "!b:example.org"; const b = "!b:example.org";
const c = "!c:example.org"; const c = "!c:example.org";
@ -675,7 +703,7 @@ describe("MSC3089TreeSpace", () => {
expectOrder(c, 2); expectOrder(c, 2);
}); });
it('should return a stable order if there are no known create events', () => { it("should return a stable order if there are no known create events", () => {
const a = "!a:example.org"; const a = "!a:example.org";
const b = "!b:example.org"; const b = "!b:example.org";
const c = "!c:example.org"; const c = "!c:example.org";
@ -692,7 +720,7 @@ describe("MSC3089TreeSpace", () => {
// XXX: These tests rely on `getOrder()` re-calculating and not caching values. // XXX: These tests rely on `getOrder()` re-calculating and not caching values.
it('should allow reordering within unordered children', async () => { it("should allow reordering within unordered children", async () => {
const a = "!a:example.org"; const a = "!a:example.org";
const b = "!b:example.org"; const b = "!b:example.org";
const c = "!c:example.org"; const c = "!c:example.org";
@ -704,32 +732,47 @@ describe("MSC3089TreeSpace", () => {
// Order of this state is validated by other tests. // Order of this state is validated by other tests.
const treeA = childTrees.find(c => c.roomId === a); const treeA = childTrees.find((c) => c.roomId === a);
expect(treeA).toBeDefined(); expect(treeA).toBeDefined();
await treeA!.setOrder(1); await treeA!.setOrder(1);
expect(clientSendStateFn).toHaveBeenCalledTimes(3); expect(clientSendStateFn).toHaveBeenCalledTimes(3);
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({ expect(clientSendStateFn).toHaveBeenCalledWith(
via: [staticDomain], // should retain domain independent of client.getDomain() tree.roomId,
EventType.SpaceChild,
expect.objectContaining({
via: [staticDomain], // should retain domain independent of client.getDomain()
// Because of how the reordering works (maintain stable ordering before moving), we end up calling this // Because of how the reordering works (maintain stable ordering before moving), we end up calling this
// function twice for the same room. // function twice for the same room.
order: DEFAULT_ALPHABET[0], order: DEFAULT_ALPHABET[0],
}), a); }),
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({ a,
via: [staticDomain], // should retain domain independent of client.getDomain() );
order: DEFAULT_ALPHABET[1], expect(clientSendStateFn).toHaveBeenCalledWith(
}), b); tree.roomId,
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({ EventType.SpaceChild,
via: [staticDomain], // should retain domain independent of client.getDomain() expect.objectContaining({
order: DEFAULT_ALPHABET[2], via: [staticDomain], // should retain domain independent of client.getDomain()
}), a); order: DEFAULT_ALPHABET[1],
}),
b,
);
expect(clientSendStateFn).toHaveBeenCalledWith(
tree.roomId,
EventType.SpaceChild,
expect.objectContaining({
via: [staticDomain], // should retain domain independent of client.getDomain()
order: DEFAULT_ALPHABET[2],
}),
a,
);
expectOrder(a, 1); expectOrder(a, 1);
expectOrder(b, 0); expectOrder(b, 0);
expectOrder(c, 2); expectOrder(c, 2);
}); });
it('should allow reordering within ordered children', async () => { it("should allow reordering within ordered children", async () => {
const a = "!a:example.org"; const a = "!a:example.org";
const b = "!b:example.org"; const b = "!b:example.org";
const c = "!c:example.org"; const c = "!c:example.org";
@ -741,21 +784,26 @@ describe("MSC3089TreeSpace", () => {
// Order of this state is validated by other tests. // Order of this state is validated by other tests.
const treeA = childTrees.find(c => c.roomId === a); const treeA = childTrees.find((c) => c.roomId === a);
expect(treeA).toBeDefined(); expect(treeA).toBeDefined();
await treeA!.setOrder(1); await treeA!.setOrder(1);
expect(clientSendStateFn).toHaveBeenCalledTimes(1); expect(clientSendStateFn).toHaveBeenCalledTimes(1);
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({ expect(clientSendStateFn).toHaveBeenCalledWith(
via: [staticDomain], // should retain domain independent of client.getDomain() tree.roomId,
order: 'Y', EventType.SpaceChild,
}), a); expect.objectContaining({
via: [staticDomain], // should retain domain independent of client.getDomain()
order: "Y",
}),
a,
);
expectOrder(a, 1); expectOrder(a, 1);
expectOrder(b, 0); expectOrder(b, 0);
expectOrder(c, 2); expectOrder(c, 2);
}); });
it('should allow reordering within partially ordered children', async () => { it("should allow reordering within partially ordered children", async () => {
const a = "!a:example.org"; const a = "!a:example.org";
const b = "!b:example.org"; const b = "!b:example.org";
const c = "!c:example.org"; const c = "!c:example.org";
@ -769,22 +817,27 @@ describe("MSC3089TreeSpace", () => {
// Order of this state is validated by other tests. // Order of this state is validated by other tests.
const treeA = childTrees.find(c => c.roomId === a); const treeA = childTrees.find((c) => c.roomId === a);
expect(treeA).toBeDefined(); expect(treeA).toBeDefined();
await treeA!.setOrder(2); await treeA!.setOrder(2);
expect(clientSendStateFn).toHaveBeenCalledTimes(1); expect(clientSendStateFn).toHaveBeenCalledTimes(1);
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({ expect(clientSendStateFn).toHaveBeenCalledWith(
via: [staticDomain], // should retain domain independent of client.getDomain() tree.roomId,
order: 'Z', EventType.SpaceChild,
}), a); expect.objectContaining({
via: [staticDomain], // should retain domain independent of client.getDomain()
order: "Z",
}),
a,
);
expectOrder(a, 2); expectOrder(a, 2);
expectOrder(b, 3); expectOrder(b, 3);
expectOrder(c, 1); expectOrder(c, 1);
expectOrder(d, 0); expectOrder(d, 0);
}); });
it('should support moving upwards', async () => { it("should support moving upwards", async () => {
const a = "!a:example.org"; const a = "!a:example.org";
const b = "!b:example.org"; const b = "!b:example.org";
const c = "!c:example.org"; const c = "!c:example.org";
@ -798,22 +851,27 @@ describe("MSC3089TreeSpace", () => {
// Order of this state is validated by other tests. // Order of this state is validated by other tests.
const treeB = childTrees.find(c => c.roomId === b); const treeB = childTrees.find((c) => c.roomId === b);
expect(treeB).toBeDefined(); expect(treeB).toBeDefined();
await treeB!.setOrder(2); await treeB!.setOrder(2);
expect(clientSendStateFn).toHaveBeenCalledTimes(1); expect(clientSendStateFn).toHaveBeenCalledTimes(1);
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({ expect(clientSendStateFn).toHaveBeenCalledWith(
via: [staticDomain], // should retain domain independent of client.getDomain() tree.roomId,
order: 'Y', EventType.SpaceChild,
}), b); expect.objectContaining({
via: [staticDomain], // should retain domain independent of client.getDomain()
order: "Y",
}),
b,
);
expectOrder(a, 0); expectOrder(a, 0);
expectOrder(b, 2); expectOrder(b, 2);
expectOrder(c, 1); expectOrder(c, 1);
expectOrder(d, 3); expectOrder(d, 3);
}); });
it('should support moving downwards', async () => { it("should support moving downwards", async () => {
const a = "!a:example.org"; const a = "!a:example.org";
const b = "!b:example.org"; const b = "!b:example.org";
const c = "!c:example.org"; const c = "!c:example.org";
@ -827,22 +885,27 @@ describe("MSC3089TreeSpace", () => {
// Order of this state is validated by other tests. // Order of this state is validated by other tests.
const treeC = childTrees.find(ch => ch.roomId === c); const treeC = childTrees.find((ch) => ch.roomId === c);
expect(treeC).toBeDefined(); expect(treeC).toBeDefined();
await treeC!.setOrder(1); await treeC!.setOrder(1);
expect(clientSendStateFn).toHaveBeenCalledTimes(1); expect(clientSendStateFn).toHaveBeenCalledTimes(1);
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({ expect(clientSendStateFn).toHaveBeenCalledWith(
via: [staticDomain], // should retain domain independent of client.getDomain() tree.roomId,
order: 'U', EventType.SpaceChild,
}), c); expect.objectContaining({
via: [staticDomain], // should retain domain independent of client.getDomain()
order: "U",
}),
c,
);
expectOrder(a, 0); expectOrder(a, 0);
expectOrder(b, 2); expectOrder(b, 2);
expectOrder(c, 1); expectOrder(c, 1);
expectOrder(d, 3); expectOrder(d, 3);
}); });
it('should support moving over the partial ordering boundary', async () => { it("should support moving over the partial ordering boundary", async () => {
const a = "!a:example.org"; const a = "!a:example.org";
const b = "!b:example.org"; const b = "!b:example.org";
const c = "!c:example.org"; const c = "!c:example.org";
@ -856,19 +919,29 @@ describe("MSC3089TreeSpace", () => {
// Order of this state is validated by other tests. // Order of this state is validated by other tests.
const treeB = childTrees.find(ch => ch.roomId === b); const treeB = childTrees.find((ch) => ch.roomId === b);
expect(treeB).toBeDefined(); expect(treeB).toBeDefined();
await treeB!.setOrder(2); await treeB!.setOrder(2);
expect(clientSendStateFn).toHaveBeenCalledTimes(2); expect(clientSendStateFn).toHaveBeenCalledTimes(2);
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({ expect(clientSendStateFn).toHaveBeenCalledWith(
via: [staticDomain], // should retain domain independent of client.getDomain() tree.roomId,
order: 'W', EventType.SpaceChild,
}), c); expect.objectContaining({
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({ via: [staticDomain], // should retain domain independent of client.getDomain()
via: [staticDomain], // should retain domain independent of client.getDomain() order: "W",
order: 'X', }),
}), b); c,
);
expect(clientSendStateFn).toHaveBeenCalledWith(
tree.roomId,
EventType.SpaceChild,
expect.objectContaining({
via: [staticDomain], // should retain domain independent of client.getDomain()
order: "X",
}),
b,
);
expectOrder(a, 0); expectOrder(a, 0);
expectOrder(b, 2); expectOrder(b, 2);
expectOrder(c, 1); expectOrder(c, 1);
@ -876,7 +949,7 @@ describe("MSC3089TreeSpace", () => {
}); });
}); });
it('should upload files', async () => { it("should upload files", async () => {
const mxc = "mxc://example.org/file"; const mxc = "mxc://example.org/file";
const fileInfo = { const fileInfo = {
mimetype: "text/plain", mimetype: "text/plain",
@ -910,7 +983,8 @@ describe("MSC3089TreeSpace", () => {
}); });
client.sendMessage = sendMsgFn; client.sendMessage = sendMsgFn;
const sendStateFn = jest.fn() const sendStateFn = jest
.fn()
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => { .mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
expect(roomId).toEqual(tree.roomId); expect(roomId).toEqual(tree.roomId);
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable
@ -935,7 +1009,7 @@ describe("MSC3089TreeSpace", () => {
expect(sendStateFn).toHaveBeenCalledTimes(1); expect(sendStateFn).toHaveBeenCalledTimes(1);
}); });
it('should upload file versions', async () => { it("should upload file versions", async () => {
const mxc = "mxc://example.org/file"; const mxc = "mxc://example.org/file";
const fileInfo = { const fileInfo = {
mimetype: "text/plain", mimetype: "text/plain",
@ -972,7 +1046,8 @@ describe("MSC3089TreeSpace", () => {
}); });
client.sendMessage = sendMsgFn; client.sendMessage = sendMsgFn;
const sendStateFn = jest.fn() const sendStateFn = jest
.fn()
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => { .mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
expect(roomId).toEqual(tree.roomId); expect(roomId).toEqual(tree.roomId);
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable
@ -997,7 +1072,7 @@ describe("MSC3089TreeSpace", () => {
expect(sendStateFn).toHaveBeenCalledTimes(1); expect(sendStateFn).toHaveBeenCalledTimes(1);
}); });
it('should support getting files', () => { it("should support getting files", () => {
const fileEventId = "$file"; const fileEventId = "$file";
const fileEvent = { forTest: true }; // MatrixEvent mock const fileEvent = { forTest: true }; // MatrixEvent mock
room.currentState = { room.currentState = {
@ -1013,7 +1088,7 @@ describe("MSC3089TreeSpace", () => {
expect(file!.indexEvent).toBe(fileEvent); expect(file!.indexEvent).toBe(fileEvent);
}); });
it('should return falsy for unknown files', () => { it("should return falsy for unknown files", () => {
const fileEventId = "$file"; const fileEventId = "$file";
room.currentState = { room.currentState = {
getStateEvents: (eventType: string, stateKey?: string): MatrixEvent[] | MatrixEvent | null => { getStateEvents: (eventType: string, stateKey?: string): MatrixEvent[] | MatrixEvent | null => {
@ -1027,7 +1102,7 @@ describe("MSC3089TreeSpace", () => {
expect(file).toBeFalsy(); expect(file).toBeFalsy();
}); });
it('should list files', () => { it("should list files", () => {
const firstFile = { getContent: () => ({ active: true }) }; const firstFile = { getContent: () => ({ active: true }) };
const secondFile = { getContent: () => ({ active: false }) }; // deliberately inactive const secondFile = { getContent: () => ({ active: false }) }; // deliberately inactive
room.currentState = { room.currentState = {
@ -1044,7 +1119,7 @@ describe("MSC3089TreeSpace", () => {
expect(files[0].indexEvent).toBe(firstFile); expect(files[0].indexEvent).toBe(firstFile);
}); });
it('should list all files', () => { it("should list all files", () => {
const firstFile = { getContent: () => ({ active: true }) }; const firstFile = { getContent: () => ({ active: true }) };
const secondFile = { getContent: () => ({ active: false }) }; // deliberately inactive const secondFile = { getContent: () => ({ active: false }) }; // deliberately inactive
room.currentState = { room.currentState = {

View File

@ -18,50 +18,46 @@ import { REFERENCE_RELATION } from "matrix-events-sdk";
import { MatrixEvent } from "../../../src"; import { MatrixEvent } from "../../../src";
import { M_BEACON_INFO } from "../../../src/@types/beacon"; import { M_BEACON_INFO } from "../../../src/@types/beacon";
import { import { isTimestampInDuration, Beacon, BeaconEvent } from "../../../src/models/beacon";
isTimestampInDuration,
Beacon,
BeaconEvent,
} from "../../../src/models/beacon";
import { makeBeaconEvent, makeBeaconInfoEvent } from "../../test-utils/beacon"; import { makeBeaconEvent, makeBeaconInfoEvent } from "../../test-utils/beacon";
jest.useFakeTimers(); jest.useFakeTimers();
describe('Beacon', () => { describe("Beacon", () => {
describe('isTimestampInDuration()', () => { describe("isTimestampInDuration()", () => {
const startTs = new Date('2022-03-11T12:07:47.592Z').getTime(); const startTs = new Date("2022-03-11T12:07:47.592Z").getTime();
const HOUR_MS = 3600000; const HOUR_MS = 3600000;
it('returns false when timestamp is before start time', () => { it("returns false when timestamp is before start time", () => {
// day before // day before
const timestamp = new Date('2022-03-10T12:07:47.592Z').getTime(); const timestamp = new Date("2022-03-10T12:07:47.592Z").getTime();
expect(isTimestampInDuration(startTs, HOUR_MS, timestamp)).toBe(false); expect(isTimestampInDuration(startTs, HOUR_MS, timestamp)).toBe(false);
}); });
it('returns false when timestamp is after start time + duration', () => { it("returns false when timestamp is after start time + duration", () => {
// 1 second later // 1 second later
const timestamp = new Date('2022-03-10T12:07:48.592Z').getTime(); const timestamp = new Date("2022-03-10T12:07:48.592Z").getTime();
expect(isTimestampInDuration(startTs, HOUR_MS, timestamp)).toBe(false); expect(isTimestampInDuration(startTs, HOUR_MS, timestamp)).toBe(false);
}); });
it('returns true when timestamp is exactly start time', () => { it("returns true when timestamp is exactly start time", () => {
expect(isTimestampInDuration(startTs, HOUR_MS, startTs)).toBe(true); expect(isTimestampInDuration(startTs, HOUR_MS, startTs)).toBe(true);
}); });
it('returns true when timestamp is exactly the end of the duration', () => { it("returns true when timestamp is exactly the end of the duration", () => {
expect(isTimestampInDuration(startTs, HOUR_MS, startTs + HOUR_MS)).toBe(true); expect(isTimestampInDuration(startTs, HOUR_MS, startTs + HOUR_MS)).toBe(true);
}); });
it('returns true when timestamp is within the duration', () => { it("returns true when timestamp is within the duration", () => {
const twoHourDuration = HOUR_MS * 2; const twoHourDuration = HOUR_MS * 2;
const now = startTs + HOUR_MS; const now = startTs + HOUR_MS;
expect(isTimestampInDuration(startTs, twoHourDuration, now)).toBe(true); expect(isTimestampInDuration(startTs, twoHourDuration, now)).toBe(true);
}); });
}); });
describe('Beacon', () => { describe("Beacon", () => {
const userId = '@user:server.org'; const userId = "@user:server.org";
const userId2 = '@user2:server.org'; const userId2 = "@user2:server.org";
const roomId = '$room:server.org'; const roomId = "$room:server.org";
// 14.03.2022 16:15 // 14.03.2022 16:15
const now = 1647270879403; const now = 1647270879403;
const HOUR_MS = 3600000; const HOUR_MS = 3600000;
@ -75,7 +71,7 @@ describe('Beacon', () => {
const advanceDateAndTime = (ms: number) => { const advanceDateAndTime = (ms: number) => {
// bc liveness check uses Date.now we have to advance this mock // bc liveness check uses Date.now we have to advance this mock
jest.spyOn(global.Date, 'now').mockReturnValue(Date.now() + ms); jest.spyOn(global.Date, "now").mockReturnValue(Date.now() + ms);
// then advance time for the interval by the same amount // then advance time for the interval by the same amount
jest.advanceTimersByTime(ms); jest.advanceTimersByTime(ms);
}; };
@ -89,7 +85,7 @@ describe('Beacon', () => {
isLive: true, isLive: true,
timestamp: now - HOUR_MS, timestamp: now - HOUR_MS,
}, },
'$live123', "$live123",
); );
notLiveBeaconEvent = makeBeaconInfoEvent( notLiveBeaconEvent = makeBeaconInfoEvent(
userId, userId,
@ -99,7 +95,7 @@ describe('Beacon', () => {
isLive: false, isLive: false,
timestamp: now - HOUR_MS, timestamp: now - HOUR_MS,
}, },
'$dead123', "$dead123",
); );
user2BeaconEvent = makeBeaconInfoEvent( user2BeaconEvent = makeBeaconInfoEvent(
userId2, userId2,
@ -109,18 +105,18 @@ describe('Beacon', () => {
isLive: true, isLive: true,
timestamp: now - HOUR_MS, timestamp: now - HOUR_MS,
}, },
'$user2live123', "$user2live123",
); );
// back to 'now' // back to 'now'
jest.spyOn(global.Date, 'now').mockReturnValue(now); jest.spyOn(global.Date, "now").mockReturnValue(now);
}); });
afterAll(() => { afterAll(() => {
jest.spyOn(global.Date, 'now').mockRestore(); jest.spyOn(global.Date, "now").mockRestore();
}); });
it('creates beacon from event', () => { it("creates beacon from event", () => {
const beacon = new Beacon(liveBeaconEvent); const beacon = new Beacon(liveBeaconEvent);
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId()); expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
@ -132,7 +128,7 @@ describe('Beacon', () => {
expect(beacon.beaconInfo).toBeTruthy(); expect(beacon.beaconInfo).toBeTruthy();
}); });
it('creates beacon without error from a malformed event', () => { it("creates beacon without error from a malformed event", () => {
const event = new MatrixEvent({ const event = new MatrixEvent({
type: M_BEACON_INFO.name, type: M_BEACON_INFO.name,
room_id: roomId, room_id: roomId,
@ -150,13 +146,13 @@ describe('Beacon', () => {
expect(beacon.beaconInfo).toBeTruthy(); expect(beacon.beaconInfo).toBeTruthy();
}); });
describe('isLive()', () => { describe("isLive()", () => {
it('returns false when beacon is explicitly set to not live', () => { it("returns false when beacon is explicitly set to not live", () => {
const beacon = new Beacon(notLiveBeaconEvent); const beacon = new Beacon(notLiveBeaconEvent);
expect(beacon.isLive).toEqual(false); expect(beacon.isLive).toEqual(false);
}); });
it('returns false when beacon is expired', () => { it("returns false when beacon is expired", () => {
const expiredBeaconEvent = makeBeaconInfoEvent( const expiredBeaconEvent = makeBeaconInfoEvent(
userId2, userId2,
roomId, roomId,
@ -165,13 +161,13 @@ describe('Beacon', () => {
isLive: true, isLive: true,
timestamp: now - HOUR_MS * 2, timestamp: now - HOUR_MS * 2,
}, },
'$user2live123', "$user2live123",
); );
const beacon = new Beacon(expiredBeaconEvent); const beacon = new Beacon(expiredBeaconEvent);
expect(beacon.isLive).toEqual(false); expect(beacon.isLive).toEqual(false);
}); });
it('returns false when beacon timestamp is in future by an hour', () => { it("returns false when beacon timestamp is in future by an hour", () => {
const beaconStartsInHour = makeBeaconInfoEvent( const beaconStartsInHour = makeBeaconInfoEvent(
userId2, userId2,
roomId, roomId,
@ -180,13 +176,13 @@ describe('Beacon', () => {
isLive: true, isLive: true,
timestamp: now + HOUR_MS, timestamp: now + HOUR_MS,
}, },
'$user2live123', "$user2live123",
); );
const beacon = new Beacon(beaconStartsInHour); const beacon = new Beacon(beaconStartsInHour);
expect(beacon.isLive).toEqual(false); expect(beacon.isLive).toEqual(false);
}); });
it('returns true when beacon timestamp is one minute in the future', () => { it("returns true when beacon timestamp is one minute in the future", () => {
const beaconStartsInOneMin = makeBeaconInfoEvent( const beaconStartsInOneMin = makeBeaconInfoEvent(
userId2, userId2,
roomId, roomId,
@ -195,13 +191,13 @@ describe('Beacon', () => {
isLive: true, isLive: true,
timestamp: now + 60000, timestamp: now + 60000,
}, },
'$user2live123', "$user2live123",
); );
const beacon = new Beacon(beaconStartsInOneMin); const beacon = new Beacon(beaconStartsInOneMin);
expect(beacon.isLive).toEqual(true); expect(beacon.isLive).toEqual(true);
}); });
it('returns true when beacon timestamp is one minute before expiry', () => { it("returns true when beacon timestamp is one minute before expiry", () => {
// this test case is to check the start time leniency doesn't affect // this test case is to check the start time leniency doesn't affect
// strict expiry time checks // strict expiry time checks
const expiresInOneMin = makeBeaconInfoEvent( const expiresInOneMin = makeBeaconInfoEvent(
@ -212,13 +208,13 @@ describe('Beacon', () => {
isLive: true, isLive: true,
timestamp: now - HOUR_MS + 60000, timestamp: now - HOUR_MS + 60000,
}, },
'$user2live123', "$user2live123",
); );
const beacon = new Beacon(expiresInOneMin); const beacon = new Beacon(expiresInOneMin);
expect(beacon.isLive).toEqual(true); expect(beacon.isLive).toEqual(true);
}); });
it('returns false when beacon timestamp is one minute after expiry', () => { it("returns false when beacon timestamp is one minute after expiry", () => {
// this test case is to check the start time leniency doesn't affect // this test case is to check the start time leniency doesn't affect
// strict expiry time checks // strict expiry time checks
const expiredOneMinAgo = makeBeaconInfoEvent( const expiredOneMinAgo = makeBeaconInfoEvent(
@ -229,21 +225,21 @@ describe('Beacon', () => {
isLive: true, isLive: true,
timestamp: now - HOUR_MS - 60000, timestamp: now - HOUR_MS - 60000,
}, },
'$user2live123', "$user2live123",
); );
const beacon = new Beacon(expiredOneMinAgo); const beacon = new Beacon(expiredOneMinAgo);
expect(beacon.isLive).toEqual(false); expect(beacon.isLive).toEqual(false);
}); });
it('returns true when beacon was created in past and not yet expired', () => { it("returns true when beacon was created in past and not yet expired", () => {
// liveBeaconEvent was created 1 hour ago // liveBeaconEvent was created 1 hour ago
const beacon = new Beacon(liveBeaconEvent); const beacon = new Beacon(liveBeaconEvent);
expect(beacon.isLive).toEqual(true); expect(beacon.isLive).toEqual(true);
}); });
}); });
describe('update()', () => { describe("update()", () => {
it('does not update with different event', () => { it("does not update with different event", () => {
const beacon = new Beacon(liveBeaconEvent); const beacon = new Beacon(liveBeaconEvent);
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId()); expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
@ -253,15 +249,12 @@ describe('Beacon', () => {
expect(beacon.identifier).toEqual(`${roomId}_${userId}`); expect(beacon.identifier).toEqual(`${roomId}_${userId}`);
}); });
it('does not update with an older event', () => { it("does not update with an older event", () => {
const beacon = new Beacon(liveBeaconEvent); const beacon = new Beacon(liveBeaconEvent);
const emitSpy = jest.spyOn(beacon, 'emit').mockClear(); const emitSpy = jest.spyOn(beacon, "emit").mockClear();
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId()); expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
const oldUpdateEvent = makeBeaconInfoEvent( const oldUpdateEvent = makeBeaconInfoEvent(userId, roomId);
userId,
roomId,
);
// less than the original event // less than the original event
oldUpdateEvent.event.origin_server_ts = liveBeaconEvent.event.origin_server_ts! - 1000; oldUpdateEvent.event.origin_server_ts = liveBeaconEvent.event.origin_server_ts! - 1000;
@ -271,23 +264,27 @@ describe('Beacon', () => {
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId()); expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
}); });
it('updates event', () => { it("updates event", () => {
const beacon = new Beacon(liveBeaconEvent); const beacon = new Beacon(liveBeaconEvent);
const emitSpy = jest.spyOn(beacon, 'emit'); const emitSpy = jest.spyOn(beacon, "emit");
expect(beacon.isLive).toEqual(true); expect(beacon.isLive).toEqual(true);
const updatedBeaconEvent = makeBeaconInfoEvent( const updatedBeaconEvent = makeBeaconInfoEvent(
userId, roomId, { timeout: HOUR_MS * 3, isLive: false }, '$live123'); userId,
roomId,
{ timeout: HOUR_MS * 3, isLive: false },
"$live123",
);
beacon.update(updatedBeaconEvent); beacon.update(updatedBeaconEvent);
expect(beacon.isLive).toEqual(false); expect(beacon.isLive).toEqual(false);
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.Update, updatedBeaconEvent, beacon); expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.Update, updatedBeaconEvent, beacon);
}); });
it('emits livenesschange event when beacon liveness changes', () => { it("emits livenesschange event when beacon liveness changes", () => {
const beacon = new Beacon(liveBeaconEvent); const beacon = new Beacon(liveBeaconEvent);
const emitSpy = jest.spyOn(beacon, 'emit'); const emitSpy = jest.spyOn(beacon, "emit");
expect(beacon.isLive).toEqual(true); expect(beacon.isLive).toEqual(true);
@ -304,12 +301,12 @@ describe('Beacon', () => {
}); });
}); });
describe('monitorLiveness()', () => { describe("monitorLiveness()", () => {
it('does not set a monitor interval when beacon is not live', () => { it("does not set a monitor interval when beacon is not live", () => {
// beacon was created an hour ago // beacon was created an hour ago
// and has a 3hr duration // and has a 3hr duration
const beacon = new Beacon(notLiveBeaconEvent); const beacon = new Beacon(notLiveBeaconEvent);
const emitSpy = jest.spyOn(beacon, 'emit'); const emitSpy = jest.spyOn(beacon, "emit");
beacon.monitorLiveness(); beacon.monitorLiveness();
@ -321,7 +318,7 @@ describe('Beacon', () => {
expect(emitSpy).not.toHaveBeenCalled(); expect(emitSpy).not.toHaveBeenCalled();
}); });
it('checks liveness of beacon at expected start time', () => { it("checks liveness of beacon at expected start time", () => {
const futureBeaconEvent = makeBeaconInfoEvent( const futureBeaconEvent = makeBeaconInfoEvent(
userId, userId,
roomId, roomId,
@ -331,12 +328,12 @@ describe('Beacon', () => {
// start timestamp hour in future // start timestamp hour in future
timestamp: now + HOUR_MS, timestamp: now + HOUR_MS,
}, },
'$live123', "$live123",
); );
const beacon = new Beacon(futureBeaconEvent); const beacon = new Beacon(futureBeaconEvent);
expect(beacon.isLive).toBeFalsy(); expect(beacon.isLive).toBeFalsy();
const emitSpy = jest.spyOn(beacon, 'emit'); const emitSpy = jest.spyOn(beacon, "emit");
beacon.monitorLiveness(); beacon.monitorLiveness();
@ -355,12 +352,12 @@ describe('Beacon', () => {
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.LivenessChange, false, beacon); expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.LivenessChange, false, beacon);
}); });
it('checks liveness of beacon at expected expiry time', () => { it("checks liveness of beacon at expected expiry time", () => {
// live beacon was created an hour ago // live beacon was created an hour ago
// and has a 3hr duration // and has a 3hr duration
const beacon = new Beacon(liveBeaconEvent); const beacon = new Beacon(liveBeaconEvent);
expect(beacon.isLive).toBeTruthy(); expect(beacon.isLive).toBeTruthy();
const emitSpy = jest.spyOn(beacon, 'emit'); const emitSpy = jest.spyOn(beacon, "emit");
beacon.monitorLiveness(); beacon.monitorLiveness();
advanceDateAndTime(HOUR_MS * 2 + 1); advanceDateAndTime(HOUR_MS * 2 + 1);
@ -369,7 +366,7 @@ describe('Beacon', () => {
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.LivenessChange, false, beacon); expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.LivenessChange, false, beacon);
}); });
it('clears monitor interval when re-monitoring liveness', () => { it("clears monitor interval when re-monitoring liveness", () => {
// live beacon was created an hour ago // live beacon was created an hour ago
// and has a 3hr duration // and has a 3hr duration
const beacon = new Beacon(liveBeaconEvent); const beacon = new Beacon(liveBeaconEvent);
@ -385,12 +382,12 @@ describe('Beacon', () => {
expect(beacon.livenessWatchTimeout).not.toEqual(oldMonitor); expect(beacon.livenessWatchTimeout).not.toEqual(oldMonitor);
}); });
it('destroy kills liveness monitor and emits', () => { it("destroy kills liveness monitor and emits", () => {
// live beacon was created an hour ago // live beacon was created an hour ago
// and has a 3hr duration // and has a 3hr duration
const beacon = new Beacon(liveBeaconEvent); const beacon = new Beacon(liveBeaconEvent);
expect(beacon.isLive).toBeTruthy(); expect(beacon.isLive).toBeTruthy();
const emitSpy = jest.spyOn(beacon, 'emit'); const emitSpy = jest.spyOn(beacon, "emit");
beacon.monitorLiveness(); beacon.monitorLiveness();
@ -407,10 +404,10 @@ describe('Beacon', () => {
}); });
}); });
describe('addLocations', () => { describe("addLocations", () => {
it('ignores locations when beacon is not live', () => { it("ignores locations when beacon is not live", () => {
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: false })); const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: false }));
const emitSpy = jest.spyOn(beacon, 'emit'); const emitSpy = jest.spyOn(beacon, "emit");
beacon.addLocations([ beacon.addLocations([
makeBeaconEvent(userId, { beaconInfoId: beacon.beaconInfoId, timestamp: now + 1 }), makeBeaconEvent(userId, { beaconInfoId: beacon.beaconInfoId, timestamp: now + 1 }),
@ -420,9 +417,9 @@ describe('Beacon', () => {
expect(emitSpy).not.toHaveBeenCalled(); expect(emitSpy).not.toHaveBeenCalled();
}); });
it('ignores locations outside the beacon live duration', () => { it("ignores locations outside the beacon live duration", () => {
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 })); const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
const emitSpy = jest.spyOn(beacon, 'emit'); const emitSpy = jest.spyOn(beacon, "emit");
beacon.addLocations([ beacon.addLocations([
// beacon has now + 60000 live period // beacon has now + 60000 live period
@ -435,7 +432,7 @@ describe('Beacon', () => {
it("should ignore invalid beacon events", () => { it("should ignore invalid beacon events", () => {
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 })); const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
const emitSpy = jest.spyOn(beacon, 'emit'); const emitSpy = jest.spyOn(beacon, "emit");
const ev = new MatrixEvent({ const ev = new MatrixEvent({
type: M_BEACON_INFO.name, type: M_BEACON_INFO.name,
@ -454,50 +451,48 @@ describe('Beacon', () => {
expect(emitSpy).not.toHaveBeenCalled(); expect(emitSpy).not.toHaveBeenCalled();
}); });
describe('when beacon is live with a start timestamp is in the future', () => { describe("when beacon is live with a start timestamp is in the future", () => {
it('ignores locations before the beacon start timestamp', () => { it("ignores locations before the beacon start timestamp", () => {
const startTimestamp = now + 60000; const startTimestamp = now + 60000;
const beacon = new Beacon(makeBeaconInfoEvent( const beacon = new Beacon(
userId, makeBeaconInfoEvent(userId, roomId, {
roomId, isLive: true,
{ isLive: true, timeout: 60000, timestamp: startTimestamp }, timeout: 60000,
)); timestamp: startTimestamp,
const emitSpy = jest.spyOn(beacon, 'emit'); }),
);
const emitSpy = jest.spyOn(beacon, "emit");
beacon.addLocations([ beacon.addLocations([
// beacon has now + 60000 live period // beacon has now + 60000 live period
makeBeaconEvent( makeBeaconEvent(userId, {
userId, beaconInfoId: beacon.beaconInfoId,
{ // now < location timestamp < beacon timestamp
beaconInfoId: beacon.beaconInfoId, timestamp: now + 10,
// now < location timestamp < beacon timestamp }),
timestamp: now + 10,
},
),
]); ]);
expect(beacon.latestLocationState).toBeFalsy(); expect(beacon.latestLocationState).toBeFalsy();
expect(emitSpy).not.toHaveBeenCalled(); expect(emitSpy).not.toHaveBeenCalled();
}); });
it('sets latest location when location timestamp is after startTimestamp', () => { it("sets latest location when location timestamp is after startTimestamp", () => {
const startTimestamp = now + 60000; const startTimestamp = now + 60000;
const beacon = new Beacon(makeBeaconInfoEvent( const beacon = new Beacon(
userId, makeBeaconInfoEvent(userId, roomId, {
roomId, isLive: true,
{ isLive: true, timeout: 600000, timestamp: startTimestamp }, timeout: 600000,
)); timestamp: startTimestamp,
const emitSpy = jest.spyOn(beacon, 'emit'); }),
);
const emitSpy = jest.spyOn(beacon, "emit");
beacon.addLocations([ beacon.addLocations([
// beacon has now + 600000 live period // beacon has now + 600000 live period
makeBeaconEvent( makeBeaconEvent(userId, {
userId, beaconInfoId: beacon.beaconInfoId,
{ // now < beacon timestamp < location timestamp
beaconInfoId: beacon.beaconInfoId, timestamp: startTimestamp + 10,
// now < beacon timestamp < location timestamp }),
timestamp: startTimestamp + 10,
},
),
]); ]);
expect(beacon.latestLocationState).toBeTruthy(); expect(beacon.latestLocationState).toBeTruthy();
@ -505,23 +500,21 @@ describe('Beacon', () => {
}); });
}); });
it('sets latest location state to most recent location', () => { it("sets latest location state to most recent location", () => {
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 })); const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
const emitSpy = jest.spyOn(beacon, 'emit'); const emitSpy = jest.spyOn(beacon, "emit");
const locations = [ const locations = [
// older // older
makeBeaconEvent( makeBeaconEvent(userId, { beaconInfoId: beacon.beaconInfoId, uri: "geo:foo", timestamp: now + 1 }),
userId, { beaconInfoId: beacon.beaconInfoId, uri: 'geo:foo', timestamp: now + 1 },
),
// newer // newer
makeBeaconEvent( makeBeaconEvent(userId, {
userId, { beaconInfoId: beacon.beaconInfoId, uri: 'geo:bar', timestamp: now + 10000 }, beaconInfoId: beacon.beaconInfoId,
), uri: "geo:bar",
timestamp: now + 10000,
}),
// not valid // not valid
makeBeaconEvent( makeBeaconEvent(userId, { beaconInfoId: beacon.beaconInfoId, uri: "geo:baz", timestamp: now - 5 }),
userId, { beaconInfoId: beacon.beaconInfoId, uri: 'geo:baz', timestamp: now - 5 },
),
]; ];
beacon.addLocations(locations); beacon.addLocations(locations);
@ -529,7 +522,7 @@ describe('Beacon', () => {
const expectedLatestLocation = { const expectedLatestLocation = {
description: undefined, description: undefined,
timestamp: now + 10000, timestamp: now + 10000,
uri: 'geo:bar', uri: "geo:bar",
}; };
// the newest valid location // the newest valid location
@ -538,32 +531,40 @@ describe('Beacon', () => {
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.LocationUpdate, expectedLatestLocation); expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.LocationUpdate, expectedLatestLocation);
}); });
it('ignores locations that are less recent that the current latest location', () => { it("ignores locations that are less recent that the current latest location", () => {
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 })); const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
const olderLocation = makeBeaconEvent( const olderLocation = makeBeaconEvent(userId, {
userId, { beaconInfoId: beacon.beaconInfoId, uri: 'geo:foo', timestamp: now + 1 }, beaconInfoId: beacon.beaconInfoId,
); uri: "geo:foo",
const newerLocation = makeBeaconEvent( timestamp: now + 1,
userId, { beaconInfoId: beacon.beaconInfoId, uri: 'geo:bar', timestamp: now + 10000 }, });
); const newerLocation = makeBeaconEvent(userId, {
beaconInfoId: beacon.beaconInfoId,
uri: "geo:bar",
timestamp: now + 10000,
});
beacon.addLocations([newerLocation]); beacon.addLocations([newerLocation]);
// latest location set to newerLocation // latest location set to newerLocation
expect(beacon.latestLocationState).toEqual(expect.objectContaining({ expect(beacon.latestLocationState).toEqual(
uri: 'geo:bar', expect.objectContaining({
})); uri: "geo:bar",
}),
);
expect(beacon.latestLocationEvent).toEqual(newerLocation); expect(beacon.latestLocationEvent).toEqual(newerLocation);
const emitSpy = jest.spyOn(beacon, 'emit').mockClear(); const emitSpy = jest.spyOn(beacon, "emit").mockClear();
// add older location // add older location
beacon.addLocations([olderLocation]); beacon.addLocations([olderLocation]);
// no change // no change
expect(beacon.latestLocationState).toEqual(expect.objectContaining({ expect(beacon.latestLocationState).toEqual(
uri: 'geo:bar', expect.objectContaining({
})); uri: "geo:bar",
}),
);
// no emit // no emit
expect(emitSpy).not.toHaveBeenCalled(); expect(emitSpy).not.toHaveBeenCalled();
}); });

View File

@ -19,8 +19,8 @@ import { emitPromise } from "../../test-utils/test-utils";
import { EventType } from "../../../src"; import { EventType } from "../../../src";
import { Crypto } from "../../../src/crypto"; import { Crypto } from "../../../src/crypto";
describe('MatrixEvent', () => { describe("MatrixEvent", () => {
it('should create copies of itself', () => { it("should create copies of itself", () => {
const a = new MatrixEvent({ const a = new MatrixEvent({
type: "com.example.test", type: "com.example.test",
content: { content: {
@ -38,7 +38,7 @@ describe('MatrixEvent', () => {
// The other properties we're not super interested in, honestly. // The other properties we're not super interested in, honestly.
}); });
it('should compare itself to other events using json', () => { it("should compare itself to other events using json", () => {
const a = new MatrixEvent({ const a = new MatrixEvent({
type: "com.example.test", type: "com.example.test",
content: { content: {
@ -122,36 +122,37 @@ describe('MatrixEvent', () => {
describe(".attemptDecryption", () => { describe(".attemptDecryption", () => {
let encryptedEvent: MatrixEvent; let encryptedEvent: MatrixEvent;
const eventId = 'test_encrypted_event'; const eventId = "test_encrypted_event";
beforeEach(() => { beforeEach(() => {
encryptedEvent = new MatrixEvent({ encryptedEvent = new MatrixEvent({
event_id: eventId, event_id: eventId,
type: 'm.room.encrypted', type: "m.room.encrypted",
content: { content: {
ciphertext: 'secrets', ciphertext: "secrets",
}, },
}); });
}); });
it('should retry decryption if a retry is queued', async () => { it("should retry decryption if a retry is queued", async () => {
const eventAttemptDecryptionSpy = jest.spyOn(encryptedEvent, 'attemptDecryption'); const eventAttemptDecryptionSpy = jest.spyOn(encryptedEvent, "attemptDecryption");
const crypto = { const crypto = {
decryptEvent: jest.fn() decryptEvent: jest
.fn()
.mockImplementationOnce(() => { .mockImplementationOnce(() => {
// schedule a second decryption attempt while // schedule a second decryption attempt while
// the first one is still running. // the first one is still running.
encryptedEvent.attemptDecryption(crypto); encryptedEvent.attemptDecryption(crypto);
const error = new Error("nope"); const error = new Error("nope");
error.name = 'DecryptionError'; error.name = "DecryptionError";
return Promise.reject(error); return Promise.reject(error);
}) })
.mockImplementationOnce(() => { .mockImplementationOnce(() => {
return Promise.resolve({ return Promise.resolve({
clearEvent: { clearEvent: {
type: 'm.room.message', type: "m.room.message",
}, },
}); });
}), }),
@ -161,7 +162,7 @@ describe('MatrixEvent', () => {
expect(eventAttemptDecryptionSpy).toHaveBeenCalledTimes(2); expect(eventAttemptDecryptionSpy).toHaveBeenCalledTimes(2);
expect(crypto.decryptEvent).toHaveBeenCalledTimes(2); expect(crypto.decryptEvent).toHaveBeenCalledTimes(2);
expect(encryptedEvent.getType()).toEqual('m.room.message'); expect(encryptedEvent.getType()).toEqual("m.room.message");
}); });
}); });
}); });

View File

@ -22,7 +22,7 @@ import { TestClient } from "../../TestClient";
import { emitPromise, mkMessage } from "../../test-utils/test-utils"; import { emitPromise, mkMessage } from "../../test-utils/test-utils";
import { EventStatus } from "../../../src"; import { EventStatus } from "../../../src";
describe('Thread', () => { describe("Thread", () => {
describe("constructor", () => { describe("constructor", () => {
it("should explode for element-web#22141 logging", () => { it("should explode for element-web#22141 logging", () => {
// Logging/debugging for https://github.com/vector-im/element-web/issues/22141 // Logging/debugging for https://github.com/vector-im/element-web/issues/22141
@ -34,13 +34,7 @@ describe('Thread', () => {
it("includes pending events in replyCount", async () => { it("includes pending events in replyCount", async () => {
const myUserId = "@bob:example.org"; const myUserId = "@bob:example.org";
const testClient = new TestClient( const testClient = new TestClient(myUserId, "DEVICE", "ACCESS_TOKEN", undefined, { timelineSupport: false });
myUserId,
"DEVICE",
"ACCESS_TOKEN",
undefined,
{ timelineSupport: false },
);
const client = testClient.client; const client = testClient.client;
const room = new Room("123", client, myUserId, { const room = new Room("123", client, myUserId, {
pendingEventOrdering: PendingEventOrdering.Detached, pendingEventOrdering: PendingEventOrdering.Detached,
@ -82,13 +76,9 @@ describe('Thread', () => {
let room: Room; let room: Room;
beforeEach(() => { beforeEach(() => {
const testClient = new TestClient( const testClient = new TestClient(myUserId, "DEVICE", "ACCESS_TOKEN", undefined, {
myUserId, timelineSupport: false,
"DEVICE", });
"ACCESS_TOKEN",
undefined,
{ timelineSupport: false },
);
client = testClient.client; client = testClient.client;
room = new Room("123", client, myUserId); room = new Room("123", client, myUserId);

View File

@ -57,9 +57,9 @@ describe("fixNotificationCountOnDecryption", () => {
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0), decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
supportsExperimentalThreads: jest.fn().mockReturnValue(true), supportsExperimentalThreads: jest.fn().mockReturnValue(true),
}); });
mockClient.reEmitter = mock(ReEmitter, 'ReEmitter'); mockClient.reEmitter = mock(ReEmitter, "ReEmitter");
mockClient.canSupport = new Map(); mockClient.canSupport = new Map();
Object.keys(Feature).forEach(feature => { Object.keys(Feature).forEach((feature) => {
mockClient.canSupport.set(feature as Feature, ServerSupport.Stable); mockClient.canSupport.set(feature as Feature, ServerSupport.Stable);
}); });
@ -67,14 +67,17 @@ describe("fixNotificationCountOnDecryption", () => {
room.setUnreadNotificationCount(NotificationCountType.Total, 1); room.setUnreadNotificationCount(NotificationCountType.Total, 1);
room.setUnreadNotificationCount(NotificationCountType.Highlight, 0); room.setUnreadNotificationCount(NotificationCountType.Highlight, 0);
event = mkEvent({ event = mkEvent(
type: EventType.RoomMessage, {
content: { type: EventType.RoomMessage,
msgtype: MsgType.Text, content: {
body: "Hello world!", msgtype: MsgType.Text,
body: "Hello world!",
},
event: true,
}, },
event: true, mockClient,
}, mockClient); );
THREAD_ID = event.getId()!; THREAD_ID = event.getId()!;
threadEvent = mkEvent({ threadEvent = mkEvent({

View File

@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import MockHttpBackend from 'matrix-mock-request'; import MockHttpBackend from "matrix-mock-request";
import { MatrixClient, PUSHER_ENABLED } from "../../src/matrix"; import { MatrixClient, PUSHER_ENABLED } from "../../src/matrix";
import { mkPusher } from '../test-utils/test-utils'; import { mkPusher } from "../test-utils/test-utils";
const realSetTimeout = setTimeout; const realSetTimeout = setTimeout;
function flushPromises() { function flushPromises() {
return new Promise(r => { return new Promise((r) => {
realSetTimeout(r, 1); realSetTimeout(r, 1);
}); });
} }

Some files were not shown because too many files have changed in this diff Show More