Files
MineGeo/tests/request-security.test.ts

104 lines
3.1 KiB
TypeScript

import assert from "node:assert/strict";
import { test } from "node:test";
import { browserAccount, limitRoomCreation } from "../lib/request-security";
import { jsonBody, jsonBodyLimit } from "../lib/json-body";
import { ServiceError } from "../lib/service-error";
test("device credentials cannot authorize staff access or create replacement credentials", async () => {
let lookups = 0;
for (const authorization of ["Bearer mg_device-token", "Basic ignored", ""]) {
await assert.rejects(
browserAccount({ headers: new Headers({ authorization }) }, async () => {
lookups++;
return { id: "administrator", role: "admin" };
}),
(error) => error instanceof ServiceError && error.status === 403,
);
}
assert.equal(lookups, 0);
await assert.rejects(
browserAccount({ headers: new Headers() }, async () => null),
(error) => error instanceof ServiceError && error.status === 401,
);
assert.deepEqual(
await browserAccount({ headers: new Headers() }, async () => ({
id: "owner",
})),
{ id: "owner" },
);
});
test("clearing a multiplayer cookie cannot reset the account's room creation limit", async () => {
const counts = new Map<string, number>();
const limit = async (key: string, maximum: number, seconds: number) => {
assert.equal(seconds, 3600);
const count = (counts.get(key) ?? 0) + 1;
counts.set(key, count);
if (count > maximum) throw new Error("Too many requests");
};
for (let i = 0; i < 20; i++)
await limitRoomCreation("owner", `cookie-${i}`, limit);
await assert.rejects(
limitRoomCreation("owner", "fresh-cookie", limit),
/Too many/,
);
assert.equal(counts.has("multiplayer:create:fresh-cookie"), false);
await assert.doesNotReject(
limitRoomCreation("another-owner", "other-cookie", limit),
);
});
test("JSON bodies enforce the byte limit while streaming and report HTTP 413", async () => {
let cancelled = false;
let chunk = 0;
const oversized = new ReadableStream<Uint8Array<ArrayBuffer>>(
{
pull(controller) {
chunk++;
controller.enqueue(new Uint8Array(jsonBodyLimit / 2 + 1));
},
cancel() {
cancelled = true;
},
},
{ highWaterMark: 0 },
);
await assert.rejects(
jsonBody({ body: oversized }),
(error) => error instanceof ServiceError && error.status === 413,
);
assert.equal(cancelled, true);
assert.equal(chunk, 2);
const atLimit = '"' + "x".repeat(jsonBodyLimit - 2) + '"';
assert.equal(
(
await jsonBody(
new Request("https://minegeo.example", {
method: "POST",
body: atLimit,
}),
)
).length,
jsonBodyLimit - 2,
);
assert.deepEqual(
await jsonBody(
new Request("https://minegeo.example", {
method: "POST",
body: JSON.stringify({ message: "Привет (:" }),
}),
),
{ message: "Привет (:" },
);
await assert.rejects(
jsonBody({ body: null }),
(error) => error instanceof ServiceError && error.status === 400,
);
await assert.rejects(
jsonBody(
new Request("https://minegeo.example", { method: "POST", body: "{" }),
),
SyntaxError,
);
});