diff --git a/.github/workflows/codechecks.js.yml b/.github/workflows/codechecks.js.yml index 6ad2f42e..d1564fc2 100644 --- a/.github/workflows/codechecks.js.yml +++ b/.github/workflows/codechecks.js.yml @@ -18,7 +18,7 @@ jobs: strategy: matrix: - node-version: [14.x] + node-version: [18.1.0] steps: - name: Checkout source diff --git a/package.json b/package.json index fdbe017e..2b88642c 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "escodegen": "^2.0.0", "eslint": "^7.32.0", "fake-indexeddb": "^3.1.2", + "fs-extra": "^11.1.1", "impunity": "^1.0.9", "mdn-polyfills": "^5.20.0", "merge-options": "^3.0.4", diff --git a/playwright/plugins/dex/index.ts b/playwright/plugins/dex/index.ts index f45257fc..a5efde10 100644 --- a/playwright/plugins/dex/index.ts +++ b/playwright/plugins/dex/index.ts @@ -14,15 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -/// - import * as path from "path"; import * as os from "os"; import * as fse from "fs-extra"; import {dockerRun, dockerStop } from "../docker"; -// A cypress plugins to add command to start & stop dex instances +// A plugin that adds command to start & stop dex instances interface DexConfig { configDir: string; diff --git a/playwright/plugins/docker/index.ts b/playwright/plugins/docker/index.ts index b0080d6c..75b9d90f 100644 --- a/playwright/plugins/docker/index.ts +++ b/playwright/plugins/docker/index.ts @@ -14,8 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -/// - import * as os from "os"; import * as childProcess from "child_process"; import * as fse from "fs-extra"; @@ -36,7 +34,8 @@ export function dockerRun(args: { } return new Promise((resolve, reject) => { - childProcess.execFile('docker', [ + childProcess.execFile('sudo', [ + "docker", "run", "--name", args.containerName, "-d", @@ -57,7 +56,8 @@ export function dockerExec(args: { params: string[]; }): Promise { return new Promise((resolve, reject) => { - childProcess.execFile("docker", [ + childProcess.execFile("sudo", [ + "docker", "exec", args.containerId, ...args.params, ], { encoding: 'utf8' }, (err, stdout, stderr) => { @@ -79,10 +79,12 @@ export function dockerCreateNetwork(args: { networkName: string; }): Promise { return new Promise((resolve, reject) => { - childProcess.execFile("docker", [ + childProcess.execFile("sudo", [ + "docker", "network", "create", - args.networkName + args.networkName, + "--subnet", "172.18.0.0/16" ], { encoding: 'utf8' }, (err, stdout, stderr) => { if(err) { if (stderr.includes(`network with name ${args.networkName} already exists`)) { @@ -106,7 +108,8 @@ export async function dockerLogs(args: { const stderrFile = args.stderrFile ? await fse.open(args.stderrFile, "w") : "ignore"; await new Promise((resolve) => { - childProcess.spawn("docker", [ + childProcess.spawn("sudo", [ + "docker", "logs", args.containerId, ], { @@ -122,7 +125,8 @@ export function dockerStop(args: { containerId: string; }): Promise { return new Promise((resolve, reject) => { - childProcess.execFile('docker', [ + childProcess.execFile('sudo', [ + "docker", "stop", args.containerId, ], err => { @@ -138,7 +142,8 @@ export function dockerRm(args: { containerId: string; }): Promise { return new Promise((resolve, reject) => { - childProcess.execFile('docker', [ + childProcess.execFile('sudo', [ + "docker", "rm", args.containerId, ], err => { diff --git a/playwright/plugins/synapsedocker/index.ts b/playwright/plugins/synapsedocker/index.ts index 390888cd..ecfc29f0 100644 --- a/playwright/plugins/synapsedocker/index.ts +++ b/playwright/plugins/synapsedocker/index.ts @@ -14,8 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -/// - import * as path from "path"; import * as os from "os"; import * as crypto from "crypto"; @@ -25,7 +23,7 @@ import {dockerCreateNetwork, dockerExec, dockerLogs, dockerRun, dockerStop} from import {request} from "@playwright/test"; -// A cypress plugins to add command to start & stop synapses in +// A plugin to add command to start & stop synapses in // docker with preset templates. interface SynapseConfig { diff --git a/playwright/tests/invite.spec.ts b/playwright/tests/invite.spec.ts new file mode 100644 index 00000000..90648bf4 --- /dev/null +++ b/playwright/tests/invite.spec.ts @@ -0,0 +1,86 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +import {test} from '@playwright/test'; +import {synapseStart, synapseStop, registerUser} from "../plugins/synapsedocker"; +import type {SynapseInstance} from "../plugins/synapsedocker"; + +test.describe("Login", () => { + let synapse: SynapseInstance; + + test.beforeEach(async () => { + synapse = await synapseStart("default"); + }); + + test.afterEach(async () => { + await synapseStop(synapse.synapseId); + }); + + test("Invite user using /invite command from composer", async ({ page }) => { + const user1 = ["foobaraccount", "password123"] as const; + const user2 = ["foobaraccount2", "password123"] as const; + await registerUser(synapse, ...user1); + const { userId } = await registerUser(synapse, ...user2); + await page.goto("/"); + await page.locator("#homeserver").fill(""); + await page.locator("#homeserver").type(synapse.baseUrl); + const [username, password] = user1; + await page.locator("#username").type(username); + await page.locator("#password").type(password); + await page.getByText("Log In", { exact: true }).click(); + await page.locator(".SessionView").waitFor(); + // Create the room + await page.getByLabel("Create room").click(); + await page.getByText("Create Room").click(); + await page.locator("#name").type("My Room"); + await page.locator(".CreateRoomView_detailsForm") + .getByRole("button", { name: "Create room" }) + .click(); + await page.locator(".RoomList") + .locator("li") + .first() + .click(); + await page.locator(".MessageComposer_input textarea").type(`/invite ${userId}`); + await page.keyboard.press("Enter"); + await page.locator(".AnnouncementView").last().getByText("was invited to the room").waitFor(); + }); + + test("Error is shown when using /invite command from composer", async ({ page }) => { + const user1 = ["foobaraccount", "password123"] as const; + await registerUser(synapse, ...user1); + await page.goto("/"); + await page.locator("#homeserver").fill(""); + await page.locator("#homeserver").type(synapse.baseUrl); + const [username, password] = user1; + await page.locator("#username").type(username); + await page.locator("#password").type(password); + await page.getByText("Log In", { exact: true }).click(); + await page.locator(".SessionView").waitFor(); + // Create the room + await page.getByLabel("Create room").click(); + await page.getByText("Create Room").click(); + await page.locator("#name").type("My Room"); + await page.locator(".CreateRoomView_detailsForm") + .getByRole("button", { name: "Create room" }) + .click(); + await page.locator(".RoomList") + .locator("li") + .first() + .click(); + await page.locator(".MessageComposer_input textarea").type("/invite foobar"); + await page.keyboard.press("Enter"); + await page.locator(".RoomView").locator(".ErrorView").waitFor(); + }); +}); diff --git a/scripts/test-app.sh b/scripts/test-app.sh index cfbd37a3..548ea6c4 100755 --- a/scripts/test-app.sh +++ b/scripts/test-app.sh @@ -1,7 +1,7 @@ #!/bin/bash # Make sure docker is available -if ! docker info > /dev/null 2>&1; then +if ! docker --version > /dev/null 2>&1; then echo "You need to intall docker before you can run the tests!" exit 1 fi diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 81835fb0..1e2c9af6 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -273,6 +273,14 @@ export class RoomViewModel extends ErrorReportViewModel { this.reportError(new Error("join syntax: /join ")); } break; + case "invite": + if (args.length === 1) { + const userId = args[0]; + await this._room.inviteUser(userId); + } else { + this.reportError(new Error("invite syntax: /invite ")); + } + break; case "shrug": message = "¯\\_(ツ)_/¯ " + args.join(" "); msgtype = "m.text"; diff --git a/src/matrix/net/HomeServerApi.ts b/src/matrix/net/HomeServerApi.ts index c5f90555..eebc692a 100644 --- a/src/matrix/net/HomeServerApi.ts +++ b/src/matrix/net/HomeServerApi.ts @@ -267,6 +267,15 @@ export class HomeServerApi { return this._get("/pushers", undefined, undefined, options); } + invite(roomId: string, userId: string, reason?: string, options?: BaseRequestOptions): IHomeServerRequest { + return this._post( + `/rooms/${encodeURIComponent(roomId)}/invite`, + {}, + { user_id: userId, reason }, + options + ); + } + join(roomId: string, options?: BaseRequestOptions): IHomeServerRequest { return this._post(`/rooms/${encodeURIComponent(roomId)}/join`, {}, {}, options); } diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 47da3c03..73074f7a 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -470,6 +470,13 @@ export class Room extends BaseRoom { }); } + async inviteUser(userId, reason) { + if (!userId) { + throw new Error("userId is null/undefined"); + } + await this._hsApi.invite(this.id, userId, reason).response(); + } + /* called by BaseRoom to pass pendingEvents when opening the timeline */ _getPendingEvents() { return this._sendQueue.pendingEvents; diff --git a/yarn.lock b/yarn.lock index f7540f2d..268b6e6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -83,12 +83,12 @@ fastq "^1.6.0" "@playwright/test@^1.27.1": - version "1.32.1" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.32.1.tgz#749c9791adb048c266277a39ba0f7e33fe593ffe" - integrity sha512-FTwjCuhlm1qHUGf4hWjfr64UMJD/z0hXYbk+O387Ioe6WdyZQ+0TBDAc6P+pHjx2xCv1VYNgrKbYrNixFWy4Dg== + version "1.37.1" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.37.1.tgz#e7f44ae0faf1be52d6360c6bbf689fd0057d9b6f" + integrity sha512-bq9zTli3vWJo8S3LwB91U0qDNQDpEXnw7knhxLM0nwDvexQAwx9tO8iKDZSqqneVq+URd/WIoz+BALMqUTgdSg== dependencies: "@types/node" "*" - playwright-core "1.32.1" + playwright-core "1.37.1" optionalDependencies: fsevents "2.3.2" @@ -1000,6 +1000,15 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== +fs-extra@^11.1.1: + version "11.1.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d" + integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -1065,6 +1074,11 @@ globby@^11.0.3: merge2 "^1.3.0" slash "^3.0.0" +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -1199,6 +1213,15 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -1384,10 +1407,10 @@ picomatch@^2.2.3: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== -playwright-core@1.32.1: - version "1.32.1" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.32.1.tgz#5a10c32403323b07d75ea428ebeed866a80b76a1" - integrity sha512-KZYUQC10mXD2Am1rGlidaalNGYk3LU1vZqqNk0gT4XPty1jOqgup8KDP8l2CUlqoNKhXM5IfGjWgW37xvGllBA== +playwright-core@1.37.1: + version "1.37.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.37.1.tgz#cb517d52e2e8cb4fa71957639f1cd105d1683126" + integrity sha512-17EuQxlSIYCmEMwzMqusJ2ztDgJePjrbttaefgdsiqeLWidjYz9BxXaTaZWxH1J95SHGk6tjE+dwgWILJoUZfA== postcss-css-variables@^0.18.0: version "0.18.0" @@ -1714,6 +1737,11 @@ typeson@^6.0.0: resolved "https://registry.yarnpkg.com/typeson/-/typeson-6.1.0.tgz#5b2a53705a5f58ff4d6f82f965917cabd0d7448b" integrity sha512-6FTtyGr8ldU0pfbvW/eOZrEtEkczHRUtduBnA90Jh9kMPCiFNnXIon3vF41N0S4tV1HHQt4Hk1j4srpESziCaA== +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"