Tests and coverage for web

This commit is contained in:
Cadence Ember 2024-12-24 01:06:19 +13:00
parent 53379a962d
commit c599dff590
17 changed files with 593 additions and 171 deletions

4
src/types.d.ts vendored
View file

@ -114,7 +114,7 @@ export namespace Event {
sender: string
content: T
origin_server_ts: number
unsigned: any
unsigned?: any
event_id: string
}
@ -137,7 +137,7 @@ export namespace Event {
content: any
state_key: string
origin_server_ts: number
unsigned: any
unsigned?: any
event_id: string
user_id: string
age: number

View file

@ -42,6 +42,7 @@ function render(event, filename, locals) {
{session} // Session is always session because it has to be trusted
))
})
/* c8 ignore start */
} catch (e) {
pugCache.set(path, async (event) => {
setResponseStatus(event, 500, "Internal Template Error")
@ -49,6 +50,7 @@ function render(event, filename, locals) {
return e.toString()
})
}
/* c8 ignore stop */
}
if (!pugCache.has(path)) {

View file

@ -11,7 +11,7 @@ mixin badge-private
| Private
mixin discord(channel, radio=false)
- let permissions = dUtils.getPermissions([], discord.guilds.get(channel.guild_id).roles, "", channel.permission_overwrites)
- let permissions = dUtils.getPermissions([], discord.guilds.get(channel.guild_id).roles, null, channel.permission_overwrites)
.s-user-card.s-user-card__small
if !dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.ViewChannel)
!= icons.Icons.IconLock
@ -49,153 +49,110 @@ mixin matrix(row, radio=false, badge="")
+badge-private
block body
if !guild_id && session.data.managedGuilds
.s-empty-state.wmx4.p48
!= icons.Spots.SpotEmptyXL
p Select a server from the top right corner to continue.
p If the server you're looking for isn't there, try #[a(href="/oauth?action=add") logging in again.]
.s-page-title.mb24
h1.s-page-title--header= guild.name
else if !session.data.managedGuilds
.s-empty-state.wmx4.p48
!= icons.Spots.SpotEmptyXL
p You need to log in to manage your servers.
a.s-btn.s-btn__icon.s-btn__filled(href="/oauth")
!= icons.Icons.IconDiscord
= ` Log in with Discord`
.d-flex.g16
.fl-grow1
h2.fs-headline1 Invite a Matrix user
else if !discord.guilds.has(guild_id) || !session.data.managedGuilds || !session.data.managedGuilds.includes(guild_id)
.s-empty-state.wmx4.p48
!= icons.Spots.SpotAlertXL
p Either the selected server doesn't exist, or you don't have the Manage Server permission on Discord.
p If you've checked your permissions, try #[a(href="/oauth") logging in again.]
else
- let guild = discord.guilds.get(guild_id)
.s-page-title.mb24
h1.s-page-title--header= guild.name
.d-flex.g16
.fl-grow1
h2.fs-headline1 Invite a Matrix user
form.d-grid.g-af-column.gy4.gx8.jc-start(method="post" action="/api/invite" style="grid-template-rows: repeat(2, auto)")
label.s-label(for="mxid") Matrix ID
input.fl-grow1.s-input.wmx3#mxid(name="mxid" required placeholder="@user:example.org")
label.s-label(for="permissions") Permissions
.s-select
select#permissions(name="permissions")
option(value="default") Default
option(value="moderator") Moderator
input(type="hidden" name="guild_id" value=guild_id)
.grid--row-start2
button.s-btn.s-btn__filled.htmx-indicator Invite
div
-
let size = 105
let p = new URLSearchParams()
p.set("data", `https://bridge.cadence.moe/invite?nonce=${nonce}`)
img(width=size height=size src=`/qr?${p}`)
h2.mt48.fs-headline1 Moderation
h2.mt48.fs-headline1 Matrix setup
h3.mt32.fs-category Linked channels
.s-card.bs-sm.p0
.s-table-container
table.s-table.s-table__bx-simple
each row in linkedChannelsWithDetails
tr
td.w40: +discord(row.channel)
td.p2: button.s-btn.s-btn__muted.s-btn__xs!= icons.Icons.IconLinkSm
td: +matrix(row)
else
tr
td(colspan="3")
.s-empty-state No channels linked between Discord and Matrix yet...
h3.mt32.fs-category Auto-create
.s-card
form.d-flex.ai-center.g8
label.s-label.fl-grow1(for="autocreate")
| Create new Matrix rooms automatically
p.s-description If you want, OOYE can automatically create new Matrix rooms and link them when an unlinked Discord channel is spoken in.
- let value = !!select("guild_active", "autocreate", {guild_id}).pluck().get()
form.d-grid.g-af-column.gy4.gx8.jc-start(method="post" action="/api/invite" style="grid-template-rows: repeat(2, auto)")
label.s-label(for="mxid") Matrix ID
input.fl-grow1.s-input.wmx3#mxid(name="mxid" required placeholder="@user:example.org")
label.s-label(for="permissions") Permissions
.s-select
select#permissions(name="permissions")
option(value="default") Default
option(value="moderator") Moderator
input(type="hidden" name="guild_id" value=guild_id)
input.s-toggle-switch.order-last#autocreate(name="autocreate" type="checkbox" hx-post="/api/autocreate" hx-indicator="#autocreate-loading" hx-disabled-elt="this" checked=value)
.is-loading#autocreate-loading
.grid--row-start2
button.s-btn.s-btn__filled.htmx-indicator Invite
div
-
let size = 105
let p = new URLSearchParams()
p.set("data", `https://bridge.cadence.moe/invite?nonce=${nonce}`)
img(width=size height=size src=`/qr?${p}`)
h3.mt32.fs-category Privacy level
.s-card
form(hx-post="/api/privacy-level" hx-trigger="change" hx-indicator="#privacy-level-loading" hx-disabled-elt="this")
input(type="hidden" name="guild_id" value=guild_id)
.d-flex.ai-center.mb4
label.s-label.fl-grow1
| How people can join on Matrix
span.is-loading#privacy-level-loading
.s-toggle-switch.s-toggle-switch__multiple.s-toggle-switch__incremental.d-grid.gx16.ai-center(style="grid-template-columns: auto 1fr")
input(type="radio" name="level" value="directory" id="privacy-level-directory" checked=(privacy_level === 2))
label.d-flex.gx8.jc-center.grid--row-start3(for="privacy-level-directory")
!= icons.Icons.IconPlusSm
!= icons.Icons.IconInternationalSm
.fl-grow1 Directory
h2.mt48.fs-headline1 Moderation
input(type="radio" name="level" value="link" id="privacy-level-link" checked=(privacy_level === 1))
label.d-flex.gx8.jc-center.grid--row-start2(for="privacy-level-link")
!= icons.Icons.IconPlusSm
!= icons.Icons.IconLinkSm
.fl-grow1 Link
h2.mt48.fs-headline1 Matrix setup
input(type="radio" name="level" value="invite" id="privacy-level-invite" checked=(privacy_level === 0))
label.d-flex.gx8.jc-center.grid--row-start1(for="privacy-level-invite")
svg.svg-icon(width="14" height="14" viewBox="0 0 14 14")
!= icons.Icons.IconLockSm
.fl-grow1 Invite
h3.mt32.fs-category Linked channels
p.s-description.m0 In-app direct invite from another user
p.s-description.m0 Shareable invite links, like Discord
p.s-description.m0 Publicly listed in directory, like Discord server discovery
//-
fieldset.s-check-group
legend.s-label How people can join on Matrix
.s-check-control
input.s-radio(type="radio" name="privacy-level" id="privacy-level-invite" value="invite" checked)
label.s-label(for="privacy-level-invite")
| Invite
p.s-description In-app direct invite on Matrix; invite command on Discord; invite form on web
.s-check-control
input.s-radio(type="radio" name="privacy-level" id="privacy-level-link" value="link")
label.s-label(for="privacy-level-link")
| Link
p.s-description All of the above, and shareable invite links (like Discord)
.s-check-control
input.s-radio(type="radio" name="privacy-level" id="privacy-level-directory" value="directory")
label.s-label(for="privacy-level-directory")
| Public
p.s-description All of the above, and publicly visible in the Matrix space directory (like Server Discovery)
h3.mt32.fs-category Manually link channels
form.d-flex.g16.ai-start(hx-post="/api/privacy-level" hx-trigger="submit" hx-disabled-elt="this")
.fl-grow2.s-btn-group.fd-column.w40
each channel in unlinkedChannels
input.s-btn--radio(type="radio" name="discord" id=channel.id value=channel.id)
label.s-btn.s-btn__muted.ta-left.truncate(for=channel.id)
+discord(channel, true, "Announcement")
.s-card.bs-sm.p0
.s-table-container
table.s-table.s-table__bx-simple
each row in linkedChannelsWithDetails
tr
td.w40: +discord(row.channel)
td.p2: button.s-btn.s-btn__muted.s-btn__xs!= icons.Icons.IconLinkSm
td: +matrix(row)
else
.s-empty-state.p8 All Discord channels are linked.
.fl-grow1.s-btn-group.fd-column.w30
each room in unlinkedRooms
input.s-btn--radio(type="radio" name="matrix" id=room.room_id value=room.room_id)
label.s-btn.s-btn__muted.ta-left.truncate(for=room.room_id)
+matrix(room, true)
else
.s-empty-state.p8 All Matrix rooms are linked.
tr
td(colspan="3")
.s-empty-state No channels linked between Discord and Matrix yet...
h3.mt32.fs-category Auto-create
.s-card
form.d-flex.ai-center.g8
label.s-label.fl-grow1(for="autocreate")
| Create new Matrix rooms automatically
p.s-description If you want, OOYE can automatically create new Matrix rooms and link them when an unlinked Discord channel is spoken in.
- let value = !!select("guild_active", "autocreate", {guild_id}).pluck().get()
input(type="hidden" name="guild_id" value=guild_id)
div
button.s-btn.s-btn__icon.s-btn__filled.htmx-indicator
!= icons.Icons.IconMerge
= ` Link`
input.s-toggle-switch.order-last#autocreate(name="autocreate" type="checkbox" hx-post="/api/autocreate" hx-indicator="#autocreate-loading" hx-disabled-elt="this" checked=value)
.is-loading#autocreate-loading
h3.mt32.fs-category Privacy level
.s-card
form(hx-post="/api/privacy-level" hx-trigger="change" hx-indicator="#privacy-level-loading" hx-disabled-elt="this")
input(type="hidden" name="guild_id" value=guild_id)
.d-flex.ai-center.mb4
label.s-label.fl-grow1
| How people can join on Matrix
span.is-loading#privacy-level-loading
.s-toggle-switch.s-toggle-switch__multiple.s-toggle-switch__incremental.d-grid.gx16.ai-center(style="grid-template-columns: auto 1fr")
input(type="radio" name="level" value="directory" id="privacy-level-directory" checked=(privacy_level === 2))
label.d-flex.gx8.jc-center.grid--row-start3(for="privacy-level-directory")
!= icons.Icons.IconPlusSm
!= icons.Icons.IconInternationalSm
.fl-grow1 Directory
input(type="radio" name="level" value="link" id="privacy-level-link" checked=(privacy_level === 1))
label.d-flex.gx8.jc-center.grid--row-start2(for="privacy-level-link")
!= icons.Icons.IconPlusSm
!= icons.Icons.IconLinkSm
.fl-grow1 Link
input(type="radio" name="level" value="invite" id="privacy-level-invite" checked=(privacy_level === 0))
label.d-flex.gx8.jc-center.grid--row-start1(for="privacy-level-invite")
svg.svg-icon(width="14" height="14" viewBox="0 0 14 14")
!= icons.Icons.IconLockSm
.fl-grow1 Invite
p.s-description.m0 In-app direct invite from another user
p.s-description.m0 Shareable invite links, like Discord
p.s-description.m0 Publicly listed in directory, like Discord server discovery
h3.mt32.fs-category Manually link channels
form.d-flex.g16.ai-start(hx-post="/api/privacy-level" hx-trigger="submit" hx-disabled-elt="this")
.fl-grow2.s-btn-group.fd-column.w40
each channel in unlinkedChannels
input.s-btn--radio(type="radio" name="discord" id=channel.id value=channel.id)
label.s-btn.s-btn__muted.ta-left.truncate(for=channel.id)
+discord(channel, true, "Announcement")
else
.s-empty-state.p8 All Discord channels are linked.
.fl-grow1.s-btn-group.fd-column.w30
each room in unlinkedRooms
input.s-btn--radio(type="radio" name="matrix" id=room.room_id value=room.room_id)
label.s-btn.s-btn__muted.ta-left.truncate(for=room.room_id)
+matrix(room, true)
else
.s-empty-state.p8 All Matrix rooms are linked.
input(type="hidden" name="guild_id" value=guild_id)
div
button.s-btn.s-btn__icon.s-btn__filled.htmx-indicator
!= icons.Icons.IconMerge
= ` Link`

View file

@ -0,0 +1,22 @@
extends includes/template.pug
block body
if !session.data.managedGuilds
.s-empty-state.wmx4.p48
!= icons.Spots.SpotEmptyXL
p You need to log in to manage your servers.
a.s-btn.s-btn__icon.s-btn__filled(href="/oauth")
!= icons.Icons.IconDiscord
= ` Log in with Discord`
else if !guild_id
.s-empty-state.wmx4.p48
!= icons.Spots.SpotEmptyXL
p Select a server from the top right corner to continue.
p If the server you're looking for isn't there, try #[a(href="/oauth?action=add") logging in again.]
else if !discord.guilds.has(guild_id) || !session.data.managedGuilds.includes(guild_id)
.s-empty-state.wmx4.p48
!= icons.Spots.SpotAlertXL
p Either the selected server doesn't exist, or you don't have the Manage Server permission on Discord.
p If you've checked your permissions, try #[a(href="/oauth") logging in again.]

View file

@ -1,7 +1,7 @@
// @ts-check
const assert = require("assert/strict")
const {defineEventHandler, getValidatedRouterParams, sendRedirect, createError} = require("h3")
const {defineEventHandler, getValidatedRouterParams, sendRedirect, createError, H3Event} = require("h3")
const {z} = require("zod")
/** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore
@ -19,6 +19,15 @@ const schema = {
})
}
/**
* @param {H3Event} event
* @returns {import("snowtransfer").SnowTransfer}
*/
function getSnow(event) {
/* c8 ignore next */
return event.context.snow || discord.snow
}
/** @type {Map<string, Promise<string>>} */
const cache = new Map()
@ -56,7 +65,8 @@ function defineMediaProxyHandler(domain) {
if (!timeUntilExpiry(refreshed)) promise = undefined
}
if (!promise) {
promise = discord.snow.channel.refreshAttachmentURLs([url]).then(x => x.refreshed_urls[0].refreshed)
const snow = getSnow(event)
promise = snow.channel.refreshAttachmentURLs([url]).then(x => x.refreshed_urls[0].refreshed)
cache.set(url, promise)
refreshed = await promise
const time = timeUntilExpiry(refreshed)

View file

@ -0,0 +1,49 @@
// @ts-check
const tryToCatch = require("try-to-catch")
const {test} = require("supertape")
const {router} = require("../../../test/web")
const {MatrixServerError} = require("../../matrix/mreq")
const snow = {
channel: {
async refreshAttachmentURLs(attachments) {
if (typeof attachments === "string") attachments = [attachments]
return {
refreshed_urls: attachments.map(a => ({
original: a,
refreshed: a + `?ex=${Math.floor(Date.now() / 1000 + 3600).toString(16)}`
}))
}
}
}
}
test("web download discord: access denied if not a known attachment", async t => {
const [error] = await tryToCatch(() =>
router.test("get", "/download/discordcdn/:channel_id/:attachment_id/:file_name", {
params: {
channel_id: "1",
attachment_id: "2",
file_name: "image.png"
},
snow
})
)
t.ok(error)
})
test("web download discord: works if a known attachment", async t => {
const event = {}
await router.test("get", "/download/discordcdn/:channel_id/:attachment_id/:file_name", {
params: {
channel_id: "655216173696286746",
attachment_id: "1314358913482621010",
file_name: "image.png"
},
event,
snow
})
t.equal(event.node.res.statusCode, 302)
t.match(event.node.res.getHeader("location"), /https:\/\/cdn.discordapp.com\/attachments\/655216173696286746\/1314358913482621010\/image\.png\?ex=/)
})

View file

@ -1,7 +1,7 @@
// @ts-check
const assert = require("assert/strict")
const {defineEventHandler, getValidatedRouterParams, setResponseStatus, setResponseHeader, sendStream, createError} = require("h3")
const {defineEventHandler, getValidatedRouterParams, setResponseStatus, setResponseHeader, sendStream, createError, H3Event} = require("h3")
const {z} = require("zod")
/** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore
@ -11,9 +11,6 @@ require("xxhash-wasm")().then(h => hasher = h)
const {sync, as, select} = require("../../passthrough")
/** @type {import("../../matrix/api")} */
const api = sync.require("../../matrix/api")
const schema = {
params: z.object({
server_name: z.string(),
@ -21,6 +18,15 @@ const schema = {
})
}
/**
* @param {H3Event} event
* @returns {import("../../matrix/api")}
*/
function getAPI(event) {
/* c8 ignore next */
return event.context.api || sync.require("../../matrix/api")
}
as.router.get(`/download/matrix/:server_name/:media_id`, defineEventHandler(async event => {
const params = await getValidatedRouterParams(event, schema.params.parse)
@ -36,6 +42,7 @@ as.router.get(`/download/matrix/:server_name/:media_id`, defineEventHandler(asyn
})
}
const api = getAPI(event)
const res = await api.getMedia(`mxc://${params.server_name}/${params.media_id}`)
const contentType = res.headers.get("content-type")

View file

@ -0,0 +1,36 @@
// @ts-check
const tryToCatch = require("try-to-catch")
const {test} = require("supertape")
const {router} = require("../../../test/web")
const fetch = require("node-fetch")
test("web download matrix: access denied if not a known attachment", async t => {
const [error] = await tryToCatch(() =>
router.test("get", "/download/matrix/:server_name/:media_id", {
params: {
server_name: "cadence.moe",
media_id: "1"
}
})
)
t.ok(error)
})
test("web download matrix: works if a known attachment", async t => {
const event = {}
await router.test("get", "/download/matrix/:server_name/:media_id", {
params: {
server_name: "cadence.moe",
media_id: "KrwlqopRyMxnEBcWDgpJZPxh",
},
event,
api: {
async getMedia(mxc, init) {
return new fetch.Response("", {status: 200, headers: {"content-type": "image/png"}})
}
}
})
t.equal(event.node.res.statusCode, 200)
t.equal(event.node.res.getHeader("content-type"), "image/png")
})

View file

@ -2,7 +2,7 @@
const assert = require("assert/strict")
const {z} = require("zod")
const {defineEventHandler, sendRedirect, useSession, createError, getValidatedQuery, readValidatedBody} = require("h3")
const {H3Event, defineEventHandler, sendRedirect, useSession, createError, getValidatedQuery, readValidatedBody} = require("h3")
const {randomUUID} = require("crypto")
const {LRUCache} = require("lru-cache")
const Ty = require("../../types")
@ -14,9 +14,6 @@ const pugSync = sync.require("../pug-sync")
const createSpace = sync.require("../../d2m/actions/create-space")
const {reg} = require("../../matrix/read-registration")
/** @type {import("../../matrix/api")} */
const api = sync.require("../../matrix/api")
const schema = {
guild: z.object({
guild_id: z.string().optional()
@ -32,6 +29,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<string, string>} nonce to guild id */
const validNonce = new LRUCache({max: 200})
@ -76,10 +82,12 @@ as.router.get("/guild", defineEventHandler(async event => {
const {guild_id} = await getValidatedQuery(event, schema.guild.parse)
const session = await useSession(event, {password: reg.as_token})
const row = select("guild_space", ["space_id", "privacy_level"], {guild_id}).get()
// @ts-ignore
const guild = discord.guilds.get(guild_id)
// Permission problems
if (!guild_id || !discord.guilds.has(guild_id) || !session.data.managedGuilds || !session.data.managedGuilds.includes(guild_id)) {
return pugSync.render(event, "guild.pug", {guild_id})
if (!guild_id || !guild || !session.data.managedGuilds || !session.data.managedGuilds.includes(guild_id)) {
return pugSync.render(event, "guild_access_denied.pug", {guild_id})
}
const nonce = randomUUID()
@ -92,11 +100,12 @@ as.router.get("/guild", defineEventHandler(async event => {
}
// Linked guild
const api = getAPI(event)
const mods = await api.getStateEvent(row.space_id, "m.room.power_levels", "")
const banned = await api.getMembers(row.space_id, "ban")
const rooms = await api.getFullHierarchy(row.space_id)
const links = getChannelRoomsLinks(guild_id, rooms)
return pugSync.render(event, "guild.pug", {guild_id, nonce, mods, banned, ...links, ...row})
return pugSync.render(event, "guild.pug", {guild, guild_id, nonce, mods, banned, ...links, ...row})
}))
as.router.get("/invite", defineEventHandler(async event => {
@ -110,6 +119,7 @@ as.router.get("/invite", defineEventHandler(async event => {
as.router.post("/api/invite", defineEventHandler(async event => {
const parsedBody = await readValidatedBody(event, schema.invite.parse)
const session = await useSession(event, {password: reg.as_token})
const api = getAPI(event)
// Check guild ID or nonce
if (parsedBody.guild_id) {
@ -136,15 +146,14 @@ as.router.post("/api/invite", defineEventHandler(async event => {
spaceMember = await api.getStateEvent(spaceID, "m.room.member", parsedBody.mxid)
} catch (e) {}
if (!spaceMember || spaceMember.membership !== "invite" || spaceMember.membership !== "join") {
if (!spaceMember || !["invite", "join"].includes(spaceMember.membership)) {
// Invite
await api.inviteToRoom(spaceID, parsedBody.mxid)
}
// Permissions
if (parsedBody.permissions === "moderator") {
await api.setUserPowerCascade(spaceID, parsedBody.mxid, 50)
}
const powerLevel = parsedBody.permissions === "moderator" ? 50 : 0
await api.setUserPowerCascade(spaceID, parsedBody.mxid, powerLevel)
if (parsedBody.guild_id) {
return sendRedirect(event, `/guild?guild_id=${guild_id}`, 302)

View file

@ -0,0 +1,199 @@
// @ts-check
const tryToCatch = require("try-to-catch")
const {test} = require("supertape")
const {router} = require("../../../test/web")
const {MatrixServerError} = require("../../matrix/mreq")
let nonce
test("web guild: access denied when not logged in", async t => {
const content = await router.test("get", "/guild?guild_id=112760669178241024", {
sessionData: {
},
})
t.match(content, /You need to log in to manage your servers./)
})
test("web guild: asks to select guild if not selected", async t => {
const content = await router.test("get", "/guild", {
sessionData: {
managedGuilds: []
},
})
t.match(content, /Select a server from the top right corner to continue./)
})
test("web guild: access denied when guild id messed up", async t => {
const content = await router.test("get", "/guild?guild_id=1", {
sessionData: {
managedGuilds: []
},
})
t.match(content, /the selected server doesn't exist/)
})
test("web invite: access denied with invalid nonce", async t => {
const content = await router.test("get", "/invite?nonce=1")
t.match(content, /This QR code has expired./)
})
test("web guild: can view guild", async t => {
const content = await router.test("get", "/guild?guild_id=112760669178241024", {
sessionData: {
managedGuilds: ["112760669178241024"]
},
api: {
async getStateEvent(roomID, type, key) {
return {}
},
async getMembers(roomID, membership) {
return {chunk: []}
},
async getFullHierarchy(roomID) {
return []
}
}
})
t.match(content, /<h1[^<]*Psychonauts 3/)
nonce = content.match(/nonce%3D([a-f0-9-]+)/)?.[1]
t.ok(nonce)
})
test("web invite: page loads with valid nonce", async t => {
const content = await router.test("get", `/invite?nonce=${nonce}`)
t.match(content, /Invite a Matrix user/)
})
test("api invite: access denied with nothing", async t => {
const [error] = await tryToCatch(() =>
router.test("post", `/api/invite`, {
body: {
mxid: "@cadence:cadence.moe",
permissions: "moderator"
}
})
)
t.equal(error.message, "Missing guild ID")
})
test("api invite: access denied when not in guild", async t => {
const [error] = await tryToCatch(() =>
router.test("post", `/api/invite`, {
body: {
mxid: "@cadence:cadence.moe",
permissions: "moderator",
guild_id: "112760669178241024"
}
})
)
t.equal(error.message, "Forbidden")
})
test("api invite: can invite with valid nonce", async t => {
let called = 0
const [error] = await tryToCatch(() =>
router.test("post", `/api/invite`, {
body: {
mxid: "@cadence:cadence.moe",
permissions: "moderator",
nonce
},
api: {
async getStateEvent(roomID, type, key) {
called++
return {membership: "leave"}
},
async inviteToRoom(roomID, mxidToInvite, mxid) {
t.equal(roomID, "!jjWAGMeQdNrVZSSfvz:cadence.moe")
called++
},
async setUserPowerCascade(roomID, mxid, power) {
t.equal(power, 50) // moderator
called++
}
}
})
)
t.notOk(error)
t.equal(called, 3)
})
test("api invite: access denied when nonce has been used", async t => {
const [error] = await tryToCatch(() =>
router.test("post", `/api/invite`, {
body: {
mxid: "@cadence:cadence.moe",
permissions: "moderator",
nonce
}
})
)
t.equal(error.message, "Nonce expired")
})
test("api invite: can invite to a moderated guild", async t => {
let called = 0
const [error] = await tryToCatch(() =>
router.test("post", `/api/invite`, {
body: {
mxid: "@cadence:cadence.moe",
permissions: "default",
guild_id: "112760669178241024"
},
sessionData: {
managedGuilds: ["112760669178241024"]
},
api: {
async getStateEvent(roomID, type, key) {
called++
throw new MatrixServerError({errcode: "M_NOT_FOUND", error: "Event not found or something"})
},
async inviteToRoom(roomID, mxidToInvite, mxid) {
t.equal(roomID, "!jjWAGMeQdNrVZSSfvz:cadence.moe")
called++
},
async setUserPowerCascade(roomID, mxid, power) {
t.equal(power, 0)
called++
}
}
})
)
t.notOk(error)
t.equal(called, 3)
})
test("api invite: does not reinvite joined users", async t => {
let called = 0
const [error] = await tryToCatch(() =>
router.test("post", `/api/invite`, {
body: {
mxid: "@cadence:cadence.moe",
permissions: "default",
guild_id: "112760669178241024"
},
sessionData: {
managedGuilds: ["112760669178241024"]
},
api: {
async getStateEvent(roomID, type, key) {
called++
return {membership: "join"}
},
async setUserPowerCascade(roomID, mxid, power) {
t.equal(power, 0)
called++
}
}
})
)
t.notOk(error)
t.equal(called, 2)
})

17
src/web/routes/qr.test.js Normal file
View file

@ -0,0 +1,17 @@
const {test} = require("supertape")
const {router} = require("../../../test/web")
const getStream = require("get-stream")
test("web qr: returns svg", async t => {
/** @type {Response} */
const res = await router.test("get", "/qr?data=hello+world", {
params: {
server_name: "cadence.moe",
media_id: "1"
}
})
t.equal(res.status, 200)
t.equal(res.headers.get("content-type"), "image/svg+xml")
const content = await getStream(res.body)
t.match(content, /<svg/)
})

View file

@ -24,7 +24,7 @@ pugSync.createRoute(as.router, "/ok", "ok.pug")
sync.require("./routes/download-matrix")
sync.require("./routes/download-discord")
sync.require("./routes/guild-settings")
sync.require("./routes/invite")
sync.require("./routes/guild")
sync.require("./routes/link")
sync.require("./routes/oauth")
sync.require("./routes/qr")
@ -33,6 +33,7 @@ sync.require("./routes/qr")
function compressResponse(event, response) {
if (!getRequestHeader(event, "accept-encoding")?.includes("gzip")) return
/* c8 ignore next */
if (typeof response.body !== "string") return
/** @type {ReadableStream} */ // @ts-ignore
const stream = new Response(response.body).body

32
src/web/server.test.js Normal file
View file

@ -0,0 +1,32 @@
// @ts-check
const {test} = require("supertape")
const {router} = require("../../test/web")
require("./server")
test("web server: can get home", async t => {
t.match(await router.test("get", "/", {}), /Add the bot to your Discord server./)
})
test("web server: can get htmx", async t => {
t.match(await router.test("get", "/static/htmx.min.js", {}), /htmx=/)
})
test("web server: can get css", async t => {
t.match(await router.test("get", "/static/stacks.min.css", {}), /--stacks-/)
})
test("web server: can get icon", async t => {
const content = await router.test("get", "/icon.png", {})
t.ok(content instanceof Buffer)
})
test("web server: compresses static resources", async t => {
const content = await router.test("get", "/static/stacks.min.css", {
headers: {
"accept-encoding": "gzip"
}
})
t.ok(content instanceof ReadableStream)
})