Tests and coverage for web
This commit is contained in:
parent
53379a962d
commit
c599dff590
17 changed files with 593 additions and 171 deletions
|
@ -64,7 +64,7 @@
|
||||||
"start": "node start.js",
|
"start": "node start.js",
|
||||||
"setup": "node scripts/setup.js",
|
"setup": "node scripts/setup.js",
|
||||||
"addbot": "node addbot.js",
|
"addbot": "node addbot.js",
|
||||||
"test": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap test/test.js | tap-dot",
|
"test": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js | tap-dot",
|
||||||
"test-slow": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js -- --slow | tap-dot",
|
"test-slow": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js -- --slow | tap-dot",
|
||||||
"cover": "c8 -o test/coverage --skip-full -x db/migrations -x src/matrix/file.js -x src/matrix/api.js -x src/matrix/mreq.js -x src/d2m/converters/rlottie-wasm.js -r html -r text supertape --no-check-assertions-count --format fail --no-worker test/test.js -- --slow"
|
"cover": "c8 -o test/coverage --skip-full -x db/migrations -x src/matrix/file.js -x src/matrix/api.js -x src/matrix/mreq.js -x src/d2m/converters/rlottie-wasm.js -r html -r text supertape --no-check-assertions-count --format fail --no-worker test/test.js -- --slow"
|
||||||
}
|
}
|
||||||
|
|
4
src/types.d.ts
vendored
4
src/types.d.ts
vendored
|
@ -114,7 +114,7 @@ export namespace Event {
|
||||||
sender: string
|
sender: string
|
||||||
content: T
|
content: T
|
||||||
origin_server_ts: number
|
origin_server_ts: number
|
||||||
unsigned: any
|
unsigned?: any
|
||||||
event_id: string
|
event_id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,7 +137,7 @@ export namespace Event {
|
||||||
content: any
|
content: any
|
||||||
state_key: string
|
state_key: string
|
||||||
origin_server_ts: number
|
origin_server_ts: number
|
||||||
unsigned: any
|
unsigned?: any
|
||||||
event_id: string
|
event_id: string
|
||||||
user_id: string
|
user_id: string
|
||||||
age: number
|
age: number
|
||||||
|
|
|
@ -42,6 +42,7 @@ function render(event, filename, locals) {
|
||||||
{session} // Session is always session because it has to be trusted
|
{session} // Session is always session because it has to be trusted
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
|
/* c8 ignore start */
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
pugCache.set(path, async (event) => {
|
pugCache.set(path, async (event) => {
|
||||||
setResponseStatus(event, 500, "Internal Template Error")
|
setResponseStatus(event, 500, "Internal Template Error")
|
||||||
|
@ -49,6 +50,7 @@ function render(event, filename, locals) {
|
||||||
return e.toString()
|
return e.toString()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
/* c8 ignore stop */
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pugCache.has(path)) {
|
if (!pugCache.has(path)) {
|
||||||
|
|
|
@ -11,7 +11,7 @@ mixin badge-private
|
||||||
| Private
|
| Private
|
||||||
|
|
||||||
mixin discord(channel, radio=false)
|
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
|
.s-user-card.s-user-card__small
|
||||||
if !dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.ViewChannel)
|
if !dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.ViewChannel)
|
||||||
!= icons.Icons.IconLock
|
!= icons.Icons.IconLock
|
||||||
|
@ -49,29 +49,6 @@ mixin matrix(row, radio=false, badge="")
|
||||||
+badge-private
|
+badge-private
|
||||||
|
|
||||||
block body
|
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.]
|
|
||||||
|
|
||||||
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`
|
|
||||||
|
|
||||||
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
|
.s-page-title.mb24
|
||||||
h1.s-page-title--header= guild.name
|
h1.s-page-title--header= guild.name
|
||||||
|
|
||||||
|
@ -158,26 +135,6 @@ block body
|
||||||
p.s-description.m0 Shareable invite links, like Discord
|
p.s-description.m0 Shareable invite links, like Discord
|
||||||
p.s-description.m0 Publicly listed in directory, like Discord server discovery
|
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
|
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")
|
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
|
.fl-grow2.s-btn-group.fd-column.w40
|
||||||
|
|
22
src/web/pug/guild_access_denied.pug
Normal file
22
src/web/pug/guild_access_denied.pug
Normal 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.]
|
|
@ -1,7 +1,7 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
const assert = require("assert/strict")
|
const assert = require("assert/strict")
|
||||||
const {defineEventHandler, getValidatedRouterParams, sendRedirect, createError} = require("h3")
|
const {defineEventHandler, getValidatedRouterParams, sendRedirect, createError, H3Event} = require("h3")
|
||||||
const {z} = require("zod")
|
const {z} = require("zod")
|
||||||
|
|
||||||
/** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore
|
/** @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>>} */
|
/** @type {Map<string, Promise<string>>} */
|
||||||
const cache = new Map()
|
const cache = new Map()
|
||||||
|
|
||||||
|
@ -56,7 +65,8 @@ function defineMediaProxyHandler(domain) {
|
||||||
if (!timeUntilExpiry(refreshed)) promise = undefined
|
if (!timeUntilExpiry(refreshed)) promise = undefined
|
||||||
}
|
}
|
||||||
if (!promise) {
|
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)
|
cache.set(url, promise)
|
||||||
refreshed = await promise
|
refreshed = await promise
|
||||||
const time = timeUntilExpiry(refreshed)
|
const time = timeUntilExpiry(refreshed)
|
||||||
|
|
49
src/web/routes/download-discord.test.js
Normal file
49
src/web/routes/download-discord.test.js
Normal 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=/)
|
||||||
|
})
|
|
@ -1,7 +1,7 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
const assert = require("assert/strict")
|
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")
|
const {z} = require("zod")
|
||||||
|
|
||||||
/** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore
|
/** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore
|
||||||
|
@ -11,9 +11,6 @@ require("xxhash-wasm")().then(h => hasher = h)
|
||||||
|
|
||||||
const {sync, as, select} = require("../../passthrough")
|
const {sync, as, select} = require("../../passthrough")
|
||||||
|
|
||||||
/** @type {import("../../matrix/api")} */
|
|
||||||
const api = sync.require("../../matrix/api")
|
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
server_name: z.string(),
|
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 => {
|
as.router.get(`/download/matrix/:server_name/:media_id`, defineEventHandler(async event => {
|
||||||
const params = await getValidatedRouterParams(event, schema.params.parse)
|
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 res = await api.getMedia(`mxc://${params.server_name}/${params.media_id}`)
|
||||||
|
|
||||||
const contentType = res.headers.get("content-type")
|
const contentType = res.headers.get("content-type")
|
||||||
|
|
36
src/web/routes/download-matrix.test.js
Normal file
36
src/web/routes/download-matrix.test.js
Normal 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")
|
||||||
|
})
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
const assert = require("assert/strict")
|
const assert = require("assert/strict")
|
||||||
const {z} = require("zod")
|
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 {randomUUID} = require("crypto")
|
||||||
const {LRUCache} = require("lru-cache")
|
const {LRUCache} = require("lru-cache")
|
||||||
const Ty = require("../../types")
|
const Ty = require("../../types")
|
||||||
|
@ -14,9 +14,6 @@ const pugSync = sync.require("../pug-sync")
|
||||||
const createSpace = sync.require("../../d2m/actions/create-space")
|
const createSpace = sync.require("../../d2m/actions/create-space")
|
||||||
const {reg} = require("../../matrix/read-registration")
|
const {reg} = require("../../matrix/read-registration")
|
||||||
|
|
||||||
/** @type {import("../../matrix/api")} */
|
|
||||||
const api = sync.require("../../matrix/api")
|
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
guild: z.object({
|
guild: z.object({
|
||||||
guild_id: z.string().optional()
|
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 */
|
/** @type {LRUCache<string, string>} nonce to guild id */
|
||||||
const validNonce = new LRUCache({max: 200})
|
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 {guild_id} = await getValidatedQuery(event, schema.guild.parse)
|
||||||
const session = await useSession(event, {password: reg.as_token})
|
const session = await useSession(event, {password: reg.as_token})
|
||||||
const row = select("guild_space", ["space_id", "privacy_level"], {guild_id}).get()
|
const row = select("guild_space", ["space_id", "privacy_level"], {guild_id}).get()
|
||||||
|
// @ts-ignore
|
||||||
|
const guild = discord.guilds.get(guild_id)
|
||||||
|
|
||||||
// Permission problems
|
// Permission problems
|
||||||
if (!guild_id || !discord.guilds.has(guild_id) || !session.data.managedGuilds || !session.data.managedGuilds.includes(guild_id)) {
|
if (!guild_id || !guild || !session.data.managedGuilds || !session.data.managedGuilds.includes(guild_id)) {
|
||||||
return pugSync.render(event, "guild.pug", {guild_id})
|
return pugSync.render(event, "guild_access_denied.pug", {guild_id})
|
||||||
}
|
}
|
||||||
|
|
||||||
const nonce = randomUUID()
|
const nonce = randomUUID()
|
||||||
|
@ -92,11 +100,12 @@ as.router.get("/guild", defineEventHandler(async event => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Linked guild
|
// Linked guild
|
||||||
|
const api = getAPI(event)
|
||||||
const mods = await api.getStateEvent(row.space_id, "m.room.power_levels", "")
|
const mods = await api.getStateEvent(row.space_id, "m.room.power_levels", "")
|
||||||
const banned = await api.getMembers(row.space_id, "ban")
|
const banned = await api.getMembers(row.space_id, "ban")
|
||||||
const rooms = await api.getFullHierarchy(row.space_id)
|
const rooms = await api.getFullHierarchy(row.space_id)
|
||||||
const links = getChannelRoomsLinks(guild_id, rooms)
|
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 => {
|
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 => {
|
as.router.post("/api/invite", defineEventHandler(async event => {
|
||||||
const parsedBody = await readValidatedBody(event, schema.invite.parse)
|
const parsedBody = await readValidatedBody(event, schema.invite.parse)
|
||||||
const session = await useSession(event, {password: reg.as_token})
|
const session = await useSession(event, {password: reg.as_token})
|
||||||
|
const api = getAPI(event)
|
||||||
|
|
||||||
// Check guild ID or nonce
|
// Check guild ID or nonce
|
||||||
if (parsedBody.guild_id) {
|
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)
|
spaceMember = await api.getStateEvent(spaceID, "m.room.member", parsedBody.mxid)
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
if (!spaceMember || spaceMember.membership !== "invite" || spaceMember.membership !== "join") {
|
if (!spaceMember || !["invite", "join"].includes(spaceMember.membership)) {
|
||||||
// Invite
|
// Invite
|
||||||
await api.inviteToRoom(spaceID, parsedBody.mxid)
|
await api.inviteToRoom(spaceID, parsedBody.mxid)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Permissions
|
// Permissions
|
||||||
if (parsedBody.permissions === "moderator") {
|
const powerLevel = parsedBody.permissions === "moderator" ? 50 : 0
|
||||||
await api.setUserPowerCascade(spaceID, parsedBody.mxid, 50)
|
await api.setUserPowerCascade(spaceID, parsedBody.mxid, powerLevel)
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedBody.guild_id) {
|
if (parsedBody.guild_id) {
|
||||||
return sendRedirect(event, `/guild?guild_id=${guild_id}`, 302)
|
return sendRedirect(event, `/guild?guild_id=${guild_id}`, 302)
|
199
src/web/routes/guild.test.js
Normal file
199
src/web/routes/guild.test.js
Normal 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
17
src/web/routes/qr.test.js
Normal 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/)
|
||||||
|
})
|
|
@ -24,7 +24,7 @@ pugSync.createRoute(as.router, "/ok", "ok.pug")
|
||||||
sync.require("./routes/download-matrix")
|
sync.require("./routes/download-matrix")
|
||||||
sync.require("./routes/download-discord")
|
sync.require("./routes/download-discord")
|
||||||
sync.require("./routes/guild-settings")
|
sync.require("./routes/guild-settings")
|
||||||
sync.require("./routes/invite")
|
sync.require("./routes/guild")
|
||||||
sync.require("./routes/link")
|
sync.require("./routes/link")
|
||||||
sync.require("./routes/oauth")
|
sync.require("./routes/oauth")
|
||||||
sync.require("./routes/qr")
|
sync.require("./routes/qr")
|
||||||
|
@ -33,6 +33,7 @@ sync.require("./routes/qr")
|
||||||
|
|
||||||
function compressResponse(event, response) {
|
function compressResponse(event, response) {
|
||||||
if (!getRequestHeader(event, "accept-encoding")?.includes("gzip")) return
|
if (!getRequestHeader(event, "accept-encoding")?.includes("gzip")) return
|
||||||
|
/* c8 ignore next */
|
||||||
if (typeof response.body !== "string") return
|
if (typeof response.body !== "string") return
|
||||||
/** @type {ReadableStream} */ // @ts-ignore
|
/** @type {ReadableStream} */ // @ts-ignore
|
||||||
const stream = new Response(response.body).body
|
const stream = new Response(response.body).body
|
||||||
|
|
32
src/web/server.test.js
Normal file
32
src/web/server.test.js
Normal 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)
|
||||||
|
})
|
|
@ -163,9 +163,13 @@ INSERT INTO member_power (mxid, room_id, power_level) VALUES
|
||||||
INSERT INTO lottie (sticker_id, mxc_url) VALUES
|
INSERT INTO lottie (sticker_id, mxc_url) VALUES
|
||||||
('860171525772279849', 'mxc://cadence.moe/ZtvvVbwMIdUZeovWVyGVFCeR');
|
('860171525772279849', 'mxc://cadence.moe/ZtvvVbwMIdUZeovWVyGVFCeR');
|
||||||
|
|
||||||
INSERT INTO "auto_emoji" ("name","emoji_id","guild_id") VALUES
|
INSERT INTO auto_emoji (name, emoji_id, guild_id) VALUES
|
||||||
('L1','1144820033948762203','529176156398682115'),
|
('L1', '1144820033948762203', '529176156398682115'),
|
||||||
('L2','1144820084079087647','529176156398682115'),
|
('L2', '1144820084079087647', '529176156398682115'),
|
||||||
('_','_','529176156398682115');
|
('_', '_', '529176156398682115');
|
||||||
|
|
||||||
|
INSERT INTO media_proxy (permitted_hash) VALUES
|
||||||
|
(-429802515645771439),
|
||||||
|
(4558604729745184757);
|
||||||
|
|
||||||
COMMIT;
|
COMMIT;
|
||||||
|
|
16
test/test.js
16
test/test.js
|
@ -20,9 +20,9 @@ const {reg} = require("../src/matrix/read-registration")
|
||||||
reg.ooye.discord_token = "Njg0MjgwMTkyNTUzODQ0NzQ3.Xl3zlw.baby"
|
reg.ooye.discord_token = "Njg0MjgwMTkyNTUzODQ0NzQ3.Xl3zlw.baby"
|
||||||
reg.ooye.server_origin = "https://matrix.cadence.moe" // so that tests will pass even when hard-coded
|
reg.ooye.server_origin = "https://matrix.cadence.moe" // so that tests will pass even when hard-coded
|
||||||
reg.ooye.server_name = "cadence.moe"
|
reg.ooye.server_name = "cadence.moe"
|
||||||
reg.id = "baby" // don't actually take authenticated actions on the server
|
reg.id = "baby"
|
||||||
reg.as_token = "baby"
|
reg.as_token = "don't actually take authenticated actions on the server"
|
||||||
reg.hs_token = "baby"
|
reg.hs_token = "don't actually take authenticated actions on the server"
|
||||||
reg.ooye.bridge_origin = "https://bridge.example.org"
|
reg.ooye.bridge_origin = "https://bridge.example.org"
|
||||||
|
|
||||||
const sync = new HeatSync({watchFS: false})
|
const sync = new HeatSync({watchFS: false})
|
||||||
|
@ -31,6 +31,9 @@ const discord = {
|
||||||
guilds: new Map([
|
guilds: new Map([
|
||||||
[data.guild.general.id, data.guild.general]
|
[data.guild.general.id, data.guild.general]
|
||||||
]),
|
]),
|
||||||
|
guildChannelMap: new Map([
|
||||||
|
[data.guild.general.id, [data.channel.general.id]],
|
||||||
|
]),
|
||||||
application: {
|
application: {
|
||||||
id: "684280192553844747"
|
id: "684280192553844747"
|
||||||
},
|
},
|
||||||
|
@ -97,7 +100,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not
|
||||||
])
|
])
|
||||||
}, {timeout: 60000})
|
}, {timeout: 60000})
|
||||||
}
|
}
|
||||||
/* c8 ignore end */
|
/* c8 ignore stop */
|
||||||
|
|
||||||
const p = migrate.migrate(db)
|
const p = migrate.migrate(db)
|
||||||
test("migrate: migration works", async t => {
|
test("migrate: migration works", async t => {
|
||||||
|
@ -142,4 +145,9 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not
|
||||||
require("../src/discord/interactions/permissions.test")
|
require("../src/discord/interactions/permissions.test")
|
||||||
require("../src/discord/interactions/privacy.test")
|
require("../src/discord/interactions/privacy.test")
|
||||||
require("../src/discord/interactions/reactions.test")
|
require("../src/discord/interactions/reactions.test")
|
||||||
|
require("../src/web/server.test")
|
||||||
|
require("../src/web/routes/download-discord.test")
|
||||||
|
require("../src/web/routes/download-matrix.test")
|
||||||
|
require("../src/web/routes/guild.test")
|
||||||
|
require("../src/web/routes/qr.test")
|
||||||
})()
|
})()
|
||||||
|
|
69
test/web.js
Normal file
69
test/web.js
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
const passthrough = require("../src/passthrough")
|
||||||
|
const h3 = require("h3")
|
||||||
|
const http = require("http")
|
||||||
|
const {SnowTransfer} = require("snowtransfer")
|
||||||
|
|
||||||
|
class Router {
|
||||||
|
constructor() {
|
||||||
|
/** @type {Map<string, h3.EventHandler>} */
|
||||||
|
this.routes = new Map()
|
||||||
|
for (const method of ["get", "post", "put", "patch", "delete"]) {
|
||||||
|
this[method] = function(url, handler) {
|
||||||
|
const key = `${method} ${url}`
|
||||||
|
this.routes.set(`${key}`, handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} method
|
||||||
|
* @param {string} inputUrl
|
||||||
|
* @param {{event?: any, params?: any, body?: any, sessionData?: any, api?: Partial<import("../src/matrix/api")>, snow?: {[k in keyof SnowTransfer]?: Partial<SnowTransfer[k]>}, headers?: any}} [options]
|
||||||
|
*/
|
||||||
|
test(method, inputUrl, options = {}) {
|
||||||
|
const url = new URL(inputUrl, "http://a")
|
||||||
|
const key = `${method} ${options.route || url.pathname}`
|
||||||
|
/* c8 ignore next */
|
||||||
|
if (!this.routes.has(key)) throw new Error(`Route not found: "${key}"`)
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
method: method.toUpperCase(),
|
||||||
|
headers: options.headers || {},
|
||||||
|
url
|
||||||
|
}
|
||||||
|
const event = options.event || {}
|
||||||
|
|
||||||
|
if (typeof options.body === "object" && options.body.constructor === Object) {
|
||||||
|
options.body = JSON.stringify(options.body)
|
||||||
|
req.headers["content-type"] = "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.routes.get(key)(Object.assign(event, {
|
||||||
|
method: method.toUpperCase(),
|
||||||
|
path: `${url.pathname}${url.search}`,
|
||||||
|
_requestBody: options.body,
|
||||||
|
node: {
|
||||||
|
req,
|
||||||
|
res: new http.ServerResponse(req)
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
api: options.api,
|
||||||
|
params: options.params,
|
||||||
|
snow: options.snow,
|
||||||
|
sessions: {
|
||||||
|
h3: {
|
||||||
|
id: "h3",
|
||||||
|
createdAt: 0,
|
||||||
|
data: options.sessionData || {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const router = new Router()
|
||||||
|
|
||||||
|
passthrough.as = {router}
|
||||||
|
|
||||||
|
module.exports.router = router
|
Loading…
Add table
Add a link
Reference in a new issue