From b2078620be1d9369a48a93085f86d43436856c31 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 8 Feb 2025 16:05:35 +1300 Subject: [PATCH] Code coverage for matrix log in & guild settings --- src/web/pug/guild_access_denied.pug | 2 +- src/web/routes/guild-settings.js | 13 +- src/web/routes/guild-settings.test.js | 83 +++++++++++ src/web/routes/guild.test.js | 14 +- src/web/routes/link.test.js | 26 ---- src/web/routes/log-in-with-matrix.js | 23 +-- src/web/routes/log-in-with-matrix.test.js | 171 ++++++++++++++++++++++ test/test.js | 2 + 8 files changed, 284 insertions(+), 50 deletions(-) create mode 100644 src/web/routes/guild-settings.test.js create mode 100644 src/web/routes/log-in-with-matrix.test.js diff --git a/src/web/pug/guild_access_denied.pug b/src/web/pug/guild_access_denied.pug index 6e88e81..1476697 100644 --- a/src/web/pug/guild_access_denied.pug +++ b/src/web/pug/guild_access_denied.pug @@ -1,7 +1,7 @@ extends includes/template.pug block body - if !session.data.user_id + if !session.data.userID .s-empty-state.wmx4.p48 != icons.Spots.SpotEmptyXL p You need to log in to manage your servers. diff --git a/src/web/routes/guild-settings.js b/src/web/routes/guild-settings.js index 31bd10f..9c02198 100644 --- a/src/web/routes/guild-settings.js +++ b/src/web/routes/guild-settings.js @@ -2,13 +2,19 @@ const assert = require("assert/strict") const {z} = require("zod") -const {defineEventHandler, useSession, createError, readValidatedBody, getRequestHeader, setResponseHeader, sendRedirect} = require("h3") +const {defineEventHandler, useSession, createError, readValidatedBody, getRequestHeader, setResponseHeader, sendRedirect, H3Event} = require("h3") const {as, db, sync, select} = require("../../passthrough") const {reg} = require("../../matrix/read-registration") -/** @type {import("../../d2m/actions/create-space")} */ -const createSpace = sync.require("../../d2m/actions/create-space") +/** + * @param {H3Event} event + * @returns {import("../../d2m/actions/create-space")} + */ +function getCreateSpace(event) { + /* c8 ignore next */ + return event.context.createSpace || sync.require("../../d2m/actions/create-space") +} /** @type {["invite", "link", "directory"]} */ const levels = ["invite", "link", "directory"] @@ -48,6 +54,7 @@ as.router.post("/api/privacy-level", defineEventHandler(async event => { const session = await useSession(event, {password: reg.as_token}) if (!(session.data.managedGuilds || []).concat(session.data.matrixGuilds || []).includes(parsedBody.guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't change settings for a guild you don't have Manage Server permissions in"}) + const createSpace = getCreateSpace(event) const i = levels.indexOf(parsedBody.level) assert.notEqual(i, -1) db.prepare("UPDATE guild_space SET privacy_level = ? WHERE guild_id = ?").run(i, parsedBody.guild_id) diff --git a/src/web/routes/guild-settings.test.js b/src/web/routes/guild-settings.test.js new file mode 100644 index 0000000..19c36af --- /dev/null +++ b/src/web/routes/guild-settings.test.js @@ -0,0 +1,83 @@ +// @ts-check + +const tryToCatch = require("try-to-catch") +const {router, test} = require("../../../test/web") +const {select} = require("../../passthrough") +const {MatrixServerError} = require("../../matrix/mreq") + +test("web autocreate: checks permissions", async t => { + const [error] = await tryToCatch(() => router.test("post", "/api/autocreate", { + body: { + guild_id: "66192955777486848" + } + })) + t.equal(error.data, "Can't change settings for a guild you don't have Manage Server permissions in") +}) + + +test("web autocreate: turns off autocreate and does htmx page refresh when guild not linked", async t => { + const event = {} + await router.test("post", "/api/autocreate", { + sessionData: { + managedGuilds: ["66192955777486848"] + }, + body: { + guild_id: "66192955777486848", + // autocreate is false + }, + headers: { + "hx-request": "true" + }, + event + }) + t.equal(event.node.res.getHeader("hx-refresh"), "true") + t.equal(select("guild_active", "autocreate", {guild_id: "66192955777486848"}).pluck().get(), 0) +}) + +test("web autocreate: turns on autocreate and issues 302 when not using htmx", async t => { + const event = {} + await router.test("post", "/api/autocreate", { + sessionData: { + managedGuilds: ["66192955777486848"] + }, + body: { + guild_id: "66192955777486848", + autocreate: "yes" + }, + event + }) + t.equal(event.node.res.getHeader("location"), "") + t.equal(select("guild_active", "autocreate", {guild_id: "66192955777486848"}).pluck().get(), 1) +}) + +test("web privacy level: checks permissions", async t => { + const [error] = await tryToCatch(() => router.test("post", "/api/privacy-level", { + body: { + guild_id: "112760669178241024", + level: "directory" + } + })) + t.equal(error.data, "Can't change settings for a guild you don't have Manage Server permissions in") +}) + +test("web privacy level: updates privacy level", async t => { + let called = 0 + await router.test("post", "/api/privacy-level", { + sessionData: { + managedGuilds: ["112760669178241024"] + }, + body: { + guild_id: "112760669178241024", + level: "directory" + }, + createSpace: { + async syncSpaceFully(guildID) { + called++ + t.equal(guildID, "112760669178241024") + return "" + } + } + }) + t.equal(called, 1) + t.equal(select("guild_space", "privacy_level", {guild_id: "112760669178241024"}).pluck().get(), 2) // directory = 2 +}) diff --git a/src/web/routes/guild.test.js b/src/web/routes/guild.test.js index 5546cd4..02c6767 100644 --- a/src/web/routes/guild.test.js +++ b/src/web/routes/guild.test.js @@ -11,27 +11,27 @@ test("web guild: access denied when not logged in", async t => { sessionData: { }, }) - t.match(html, /You need to log in to manage your servers./) + t.has(html, "You need to log in to manage your servers.") }) test("web guild: asks to select guild if not selected", async t => { const html = await router.test("get", "/guild", { sessionData: { - user_id: "1", + userID: "1", managedGuilds: [] }, }) - t.match(html, /Select a server from the top right corner to continue./) + t.has(html, "Select a server from the top right corner to continue.") }) test("web guild: access denied when guild id messed up", async t => { const html = await router.test("get", "/guild?guild_id=1", { sessionData: { - user_id: "1", + userID: "1", managedGuilds: [] }, }) - t.match(html, /the selected server doesn't exist/) + t.has(html, "the selected server doesn't exist") }) test("web invite: access denied with invalid nonce", async t => { @@ -44,7 +44,6 @@ test("web invite: access denied with invalid nonce", async t => { test("web guild: can view unbridged guild", async t => { const html = await router.test("get", "/guild?guild_id=66192955777486848", { sessionData: { - user_id: "1", managedGuilds: ["66192955777486848"] } }) @@ -54,7 +53,6 @@ test("web guild: can view unbridged guild", async t => { test("web guild: unbridged self-service guild prompts log in to matrix", async t => { const html = await router.test("get", "/guild?guild_id=665289423482519565", { sessionData: { - user_id: "1", managedGuilds: ["665289423482519565"] } }) @@ -66,7 +64,6 @@ test("web guild: unbridged self-service guild asks to be invited", async t => { const html = await router.test("get", "/guild?guild_id=665289423482519565", { sessionData: { mxid: "@user:example.org", - user_id: "1", managedGuilds: ["665289423482519565"] } }) @@ -77,7 +74,6 @@ test("web guild: unbridged self-service guild shows available spaces", async t = const html = await router.test("get", "/guild?guild_id=665289423482519565", { sessionData: { mxid: "@cadence:cadence.moe", - user_id: "1", managedGuilds: ["665289423482519565"] } }) diff --git a/src/web/routes/link.test.js b/src/web/routes/link.test.js index 234ae51..65e2a95 100644 --- a/src/web/routes/link.test.js +++ b/src/web/routes/link.test.js @@ -21,7 +21,6 @@ test("web link space: access denied when not logged in to Discord", async t => { test("web link space: access denied when not logged in to Matrix", async t => { const [error] = await tryToCatch(() => router.test("post", "/api/link-space", { sessionData: { - user_id: "1", managedGuilds: ["665289423482519565"] }, body: { @@ -35,7 +34,6 @@ test("web link space: access denied when not logged in to Matrix", async t => { test("web link space: access denied when bot was invited by different user", async t => { const [error] = await tryToCatch(() => router.test("post", "/api/link-space", { sessionData: { - user_id: "1", managedGuilds: ["665289423482519565"], mxid: "@user:example.org" }, @@ -50,7 +48,6 @@ test("web link space: access denied when bot was invited by different user", asy test("web link space: access denied when guild is already in use", async t => { const [error] = await tryToCatch(() => router.test("post", "/api/link-space", { sessionData: { - user_id: "1", managedGuilds: ["112760669178241024"], mxid: "@cadence:cadence.moe" }, @@ -66,7 +63,6 @@ test("web link space: check that OOYE is joined", async t => { let called = 0 const [error] = await tryToCatch(() => router.test("post", "/api/link-space", { sessionData: { - user_id: "1", managedGuilds: ["665289423482519565"], mxid: "@cadence:cadence.moe" }, @@ -92,7 +88,6 @@ test("web link space: check that OOYE has PL 100 (not missing)", async t => { let called = 0 const [error] = await tryToCatch(() => router.test("post", "/api/link-space", { sessionData: { - user_id: "1", managedGuilds: ["665289423482519565"], mxid: "@cadence:cadence.moe" }, @@ -121,7 +116,6 @@ test("web link space: check that OOYE has PL 100 (not users_default)", async t = let called = 0 const [error] = await tryToCatch(() => router.test("post", "/api/link-space", { sessionData: { - user_id: "1", managedGuilds: ["665289423482519565"], mxid: "@cadence:cadence.moe" }, @@ -151,7 +145,6 @@ test("web link space: check that OOYE has PL 100 (not 50)", async t => { let called = 0 const [error] = await tryToCatch(() => router.test("post", "/api/link-space", { sessionData: { - user_id: "1", managedGuilds: ["665289423482519565"], mxid: "@cadence:cadence.moe" }, @@ -181,7 +174,6 @@ test("web link space: check that inviting user has PL 50", async t => { let called = 0 const [error] = await tryToCatch(() => router.test("post", "/api/link-space", { sessionData: { - user_id: "1", managedGuilds: ["665289423482519565"], mxid: "@cadence:cadence.moe" }, @@ -211,7 +203,6 @@ test("web link space: successfully adds entry to database and loads page", async let called = 0 await router.test("post", "/api/link-space", { sessionData: { - user_id: "1", managedGuilds: ["665289423482519565"], mxid: "@cadence:cadence.moe" }, @@ -241,7 +232,6 @@ test("web link space: successfully adds entry to database and loads page", async // check that the guild info page now loads const html = await router.test("get", "/guild?guild_id=665289423482519565", { sessionData: { - user_id: "1", managedGuilds: ["665289423482519565"], mxid: "@cadence:cadence.moe" }, @@ -278,7 +268,6 @@ test("web link room: access denied when not logged in to Discord", async t => { test("web link room: check that guild exists", async t => { const [error] = await tryToCatch(() => router.test("post", "/api/link", { sessionData: { - user_id: "1", managedGuilds: ["1"] }, body: { @@ -293,7 +282,6 @@ test("web link room: check that guild exists", async t => { test("web link room: check that channel exists", async t => { const [error] = await tryToCatch(() => router.test("post", "/api/link", { sessionData: { - user_id: "1", managedGuilds: ["665289423482519565"] }, body: { @@ -308,7 +296,6 @@ test("web link room: check that channel exists", async t => { test("web link room: check that channel is part of guild", async t => { const [error] = await tryToCatch(() => router.test("post", "/api/link", { sessionData: { - user_id: "1", managedGuilds: ["665289423482519565"] }, body: { @@ -323,7 +310,6 @@ test("web link room: check that channel is part of guild", async t => { test("web link room: check that channel is not already linked", async t => { const [error] = await tryToCatch(() => router.test("post", "/api/link", { sessionData: { - user_id: "1", managedGuilds: ["112760669178241024"] }, body: { @@ -339,7 +325,6 @@ test("web link room: checks the autocreate setting if the space doesn't exist ye let called = 0 const [error] = await tryToCatch(() => router.test("post", "/api/link", { sessionData: { - user_id: "1", managedGuilds: ["665289423482519565"] }, body: { @@ -366,7 +351,6 @@ test("web link room: check that room is part of space (event missing)", async t let called = 0 const [error] = await tryToCatch(() => router.test("post", "/api/link", { sessionData: { - user_id: "1", managedGuilds: ["665289423482519565"] }, body: { @@ -392,7 +376,6 @@ test("web link room: check that room is part of space (event empty)", async t => let called = 0 const [error] = await tryToCatch(() => router.test("post", "/api/link", { sessionData: { - user_id: "1", managedGuilds: ["665289423482519565"] }, body: { @@ -418,7 +401,6 @@ test("web link room: check that bridge is joined to room", async t => { let called = 0 const [error] = await tryToCatch(() => router.test("post", "/api/link", { sessionData: { - user_id: "1", managedGuilds: ["665289423482519565"] }, body: { @@ -449,7 +431,6 @@ test("web link room: check that bridge has PL 100 in target room (event missing) let called = 0 const [error] = await tryToCatch(() => router.test("post", "/api/link", { sessionData: { - user_id: "1", managedGuilds: ["665289423482519565"] }, body: { @@ -484,7 +465,6 @@ test("web link room: check that bridge has PL 100 in target room (users default) let called = 0 const [error] = await tryToCatch(() => router.test("post", "/api/link", { sessionData: { - user_id: "1", managedGuilds: ["665289423482519565"] }, body: { @@ -519,7 +499,6 @@ test("web link room: successfully calls createRoom", async t => { let called = 0 await router.test("post", "/api/link", { sessionData: { - user_id: "1", managedGuilds: ["665289423482519565"] }, body: { @@ -578,7 +557,6 @@ test("web unlink room: access denied if not logged in to Discord", async t => { test("web unlink room: checks that guild exists", async t => { const [error] = await tryToCatch(() => router.test("post", "/api/unlink", { sessionData: { - user_id: "1", managedGuilds: ["2"] }, body: { @@ -592,7 +570,6 @@ test("web unlink room: checks that guild exists", async t => { test("web unlink room: checks that the channel is part of the guild", async t => { const [error] = await tryToCatch(() => router.test("post", "/api/unlink", { sessionData: { - user_id: "1", managedGuilds: ["665289423482519565"] }, body: { @@ -607,7 +584,6 @@ test("web unlink room: successfully calls unbridgeDeletedChannel when the channe let called = 0 await router.test("post", "/api/unlink", { sessionData: { - user_id: "1", managedGuilds: ["665289423482519565"] }, body: { @@ -628,7 +604,6 @@ test("web unlink room: successfully calls unbridgeDeletedChannel when the channe let called = 0 await router.test("post", "/api/unlink", { sessionData: { - user_id: "1", managedGuilds: ["112760669178241024"] }, body: { @@ -649,7 +624,6 @@ test("web unlink room: checks that the channel is bridged", async t => { db.prepare("DELETE FROM channel_room WHERE channel_id = '665310973967597573'").run() const [error] = await tryToCatch(() => router.test("post", "/api/unlink", { sessionData: { - user_id: "1", managedGuilds: ["665289423482519565"] }, body: { diff --git a/src/web/routes/log-in-with-matrix.js b/src/web/routes/log-in-with-matrix.js index cfab928..f3d3ed5 100644 --- a/src/web/routes/log-in-with-matrix.js +++ b/src/web/routes/log-in-with-matrix.js @@ -2,25 +2,16 @@ const {z} = require("zod") const {randomUUID} = require("crypto") -const {defineEventHandler, getValidatedQuery, sendRedirect, readValidatedBody, useSession, createError, getRequestHeader} = require("h3") -const {SnowTransfer} = require("snowtransfer") -const DiscordTypes = require("discord-api-types/v10") -const fetch = require("node-fetch") -const getRelativePath = require("get-relative-path") +const {defineEventHandler, getValidatedQuery, sendRedirect, readValidatedBody, useSession, createError, getRequestHeader, H3Event} = require("h3") const {LRUCache} = require("lru-cache") -const {as, db, select, from} = require("../../passthrough") -const {id} = require("../../../addbot") +const {as, db} = require("../../passthrough") const {reg} = require("../../matrix/read-registration") const {sync} = require("../../passthrough") const assert = require("assert").strict /** @type {import("../pug-sync")} */ const pugSync = sync.require("../pug-sync") -/** @type {import("../../matrix/api")} */ -const api = sync.require("../../matrix/api") - -const redirect_uri = `${reg.ooye.bridge_origin}/oauth` const schema = { form: z.object({ @@ -31,6 +22,15 @@ const schema = { }) } +/** + * @param {H3Event} event + * @returns {import("../../matrix/api")} + */ +function getAPI(event) { + /* c8 ignore next */ + return event.context.api || sync.require("../../matrix/api") +} + /** @type {LRUCache} token to mxid */ const validToken = new LRUCache({max: 200}) @@ -67,6 +67,7 @@ as.router.get("/log-in-with-matrix", defineEventHandler(async event => { })) as.router.post("/api/log-in-with-matrix", defineEventHandler(async event => { + const api = getAPI(event) const {mxid} = await readValidatedBody(event, schema.form.parse) let roomID = null diff --git a/src/web/routes/log-in-with-matrix.test.js b/src/web/routes/log-in-with-matrix.test.js new file mode 100644 index 0000000..5e8e2da --- /dev/null +++ b/src/web/routes/log-in-with-matrix.test.js @@ -0,0 +1,171 @@ +// @ts-check + +const tryToCatch = require("try-to-catch") +const {router, test} = require("../../../test/web") +const {MatrixServerError} = require("../../matrix/mreq") + +// ***** first request ***** + +test("log in with matrix: shows web page with form on first request", async t => { + const html = await router.test("get", "/log-in-with-matrix", { + }) + t.has(html, `hx-post="/api/log-in-with-matrix"`) +}) + +// ***** second request ***** + +let token + +test("log in with matrix: sends message when there is no m.direct data", async t => { + const event = {} + let called = 0 + await router.test("post", "/api/log-in-with-matrix", { + body: { + mxid: "@cadence:cadence.moe" + }, + api: { + async getAccountData(type) { + called++ + t.equal(type, "m.direct") + throw new MatrixServerError({errcode: "M_NOT_FOUND"}) + }, + async createRoom() { + called++ + return "!created:cadence.moe" + }, + async setAccountData(type, content) { + called++ + t.equal(type, "m.direct") + t.deepEqual(content, {"@cadence:cadence.moe": ["!created:cadence.moe"]}) + }, + async sendEvent(roomID, type, content) { + called++ + t.equal(roomID, "!created:cadence.moe") + t.equal(type, "m.room.message") + token = content.body.match(/log-in-with-matrix\?token=([a-f0-9-]+)/)[1] + t.ok(token, "log in token not issued") + return "" + } + }, + event + }) + t.match(event.node.res.getHeader("location"), /Please check your inbox on Matrix/) + t.equal(called, 4) +}) + +test("log in with matrix: does not send another message when a log in is in progress", async t => { + const event = {} + await router.test("post", "/api/log-in-with-matrix", { + body: { + mxid: "@cadence:cadence.moe" + }, + event + }) + t.match(event.node.res.getHeader("location"), /We already sent you a link on Matrix/) +}) + +test("log in with matrix: reuses room from m.direct", async t => { + const event = {} + let called = 0 + await router.test("post", "/api/log-in-with-matrix", { + body: { + mxid: "@user1:example.org" + }, + api: { + async getAccountData(type) { + called++ + t.equal(type, "m.direct") + return {"@user1:example.org": ["!existing:cadence.moe"]} + }, + async getStateEvent(roomID, type, key) { + called++ + t.equal(roomID, "!existing:cadence.moe") + t.equal(type, "m.room.member") + t.equal(key, "@user1:example.org") + return {membership: "join"} + }, + async sendEvent(roomID) { + called++ + t.equal(roomID, "!existing:cadence.moe") + return "" + } + }, + event + }) + t.match(event.node.res.getHeader("location"), /Please check your inbox on Matrix/) + t.equal(called, 3) +}) + +test("log in with matrix: reuses room from m.direct, reinviting if user has left", async t => { + const event = {} + let called = 0 + await router.test("post", "/api/log-in-with-matrix", { + body: { + mxid: "@user2:example.org" + }, + api: { + async getAccountData(type) { + called++ + t.equal(type, "m.direct") + return {"@user2:example.org": ["!existing:cadence.moe"]} + }, + async getStateEvent(roomID, type, key) { + called++ + t.equal(roomID, "!existing:cadence.moe") + t.equal(type, "m.room.member") + t.equal(key, "@user2:example.org") + throw new MatrixServerError({errcode: "M_NOT_FOUND"}) + }, + async inviteToRoom(roomID, mxid) { + called++ + t.equal(roomID, "!existing:cadence.moe") + t.equal(mxid, "@user2:example.org") + }, + async sendEvent(roomID) { + called++ + t.equal(roomID, "!existing:cadence.moe") + return "" + } + }, + event + }) + t.match(event.node.res.getHeader("location"), /Please check your inbox on Matrix/) + t.equal(called, 4) +}) + +// ***** third request ***** + + +test("log in with matrix: does not use up token when requested by Synapse URL previewer", async t => { + const event = {} + const [error] = await tryToCatch(() => router.test("get", `/log-in-with-matrix?token=${token}`, { + headers: { + "user-agent": "Synapse (bot; +https://github.com/matrix-org/synapse)" + }, + event + })) + t.equal(error.data, "Sorry URL previewer, you can't have this URL.") +}) + +test("log in with matrix: does not use up token when requested by Discord URL previewer", async t => { + const event = {} + const [error] = await tryToCatch(() => router.test("get", `/log-in-with-matrix?token=${token}`, { + headers: { + "user-agent": "Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)" + }, + event + })) + t.equal(error.data, "Sorry URL previewer, you can't have this URL.") +}) + +test("log in with matrix: successful request when using valid token", async t => { + const event = {} + await router.test("get", `/log-in-with-matrix?token=${token}`, {event}) + t.equal(event.node.res.getHeader("location"), "./") +}) + +test("log in with matrix: won't log in again if token has been used", async t => { + const event = {} + await router.test("get", `/log-in-with-matrix?token=${token}`, {event}) + t.equal(event.node.res.getHeader("location"), "https://bridge.example.org/log-in-with-matrix") +}) diff --git a/test/test.js b/test/test.js index 36ec1ff..bc226a3 100644 --- a/test/test.js +++ b/test/test.js @@ -157,5 +157,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("../src/web/routes/download-discord.test") require("../src/web/routes/download-matrix.test") require("../src/web/routes/guild.test") + require("../src/web/routes/guild-settings.test") require("../src/web/routes/link.test") + require("../src/web/routes/log-in-with-matrix.test") })()