Tests and coverage for web
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
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")
|
||||
|
||||
@@ -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 {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)
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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/)
|
||||
})
|
||||
Reference in New Issue
Block a user