Harden bot, storage and web API

This commit is contained in:
Shinuwa 2026-06-30 17:25:00 +02:00
parent e974bc6660
commit 20624a5c9c
43 changed files with 1265 additions and 1012 deletions

49
test/auth.test.js Normal file
View file

@ -0,0 +1,49 @@
import test from "node:test";
import assert from "node:assert/strict";
Object.assign(process.env, {
NODE_ENV: "test",
DISCORD_GUILD_ID: "1",
SHINUWA_USER_ID: "1",
WEB_SERVER_PORT: "3000",
WEB_API_KEY: "test-key",
DISCORD_APPLICATION_ID: "1",
DISCORD_TOKEN: "test-token",
VOICE_CHANNEL_ID: "1",
TEXT_CHANNEL_ID: "1",
GAME_SERVERS_MESSAGE_ID: "1",
});
const { authMiddleware } = await import("../src/web/middlewares/auth.js");
function runAuth(authorization) {
let nextCalled = false;
let statusCode;
let payload;
const request = { headers: { authorization } };
const response = {
status(code) {
statusCode = code;
return this;
},
json(value) {
payload = value;
return this;
},
};
authMiddleware(request, response, () => {
nextCalled = true;
});
return { nextCalled, statusCode, payload };
}
test("API authentication accepts only an exact Bearer token", () => {
assert.equal(runAuth("Bearer test-key").nextCalled, true);
assert.deepEqual(runAuth("test-key"), {
nextCalled: false,
statusCode: 401,
payload: { error: "Unauthorized" },
});
assert.equal(runAuth("Bearer wrong-key").statusCode, 401);
});

58
test/commands.test.js Normal file
View file

@ -0,0 +1,58 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
const ENVIRONMENT = {
NODE_ENV: "test",
DISCORD_GUILD_ID: "1",
SHINUWA_USER_ID: "1",
WEB_SERVER_PORT: "3000",
WEB_API_KEY: "test-key",
DISCORD_APPLICATION_ID: "1",
DISCORD_TOKEN: "test-token",
VOICE_CHANNEL_ID: "1",
TEXT_CHANNEL_ID: "1",
GAME_SERVERS_MESSAGE_ID: "1",
};
Object.assign(process.env, ENVIRONMENT);
async function loadCommands() {
const files = fs
.readdirSync("src/commands", { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.flatMap((directory) =>
fs
.readdirSync(`src/commands/${directory.name}`)
.filter((file) => file.endsWith(".js"))
.map((file) => `../src/commands/${directory.name}/${file}`)
);
return Promise.all(files.map((file) => import(file)));
}
test("all Discord commands expose valid command data", async () => {
const modules = await loadCommands();
assert.equal(modules.length, 9);
for (const module of modules) {
assert.doesNotThrow(() => module.command.data.toJSON());
assert.equal(typeof module.command.execute, "function");
}
});
test("administrative commands require Discord permissions", async () => {
const modules = await loadCommands();
const protectedCommands = new Set([
"deletemsgs",
"muteall",
"unmuteall",
"deafall",
"undeafall",
]);
for (const module of modules) {
const data = module.command.data.toJSON();
if (protectedCommands.has(data.name)) {
assert.notEqual(data.default_member_permissions, undefined, data.name);
assert.notEqual(data.default_member_permissions, null, data.name);
}
}
});

52
test/musicQueue.test.js Normal file
View file

@ -0,0 +1,52 @@
import test from "node:test";
import assert from "node:assert/strict";
Object.assign(process.env, {
NODE_ENV: "test",
DISCORD_GUILD_ID: "1",
SHINUWA_USER_ID: "1",
WEB_SERVER_PORT: "3000",
WEB_API_KEY: "test-key",
DISCORD_APPLICATION_ID: "1",
DISCORD_TOKEN: "test-token",
VOICE_CHANNEL_ID: "1",
TEXT_CHANNEL_ID: "1",
GAME_SERVERS_MESSAGE_ID: "1",
});
const { MusicQueue } = await import("../src/music/queue.js");
test("playNext shares one in-flight operation", async () => {
const queue = Object.create(MusicQueue.prototype);
queue.playNextPromise = null;
let calls = 0;
let resolveOperation;
queue.runPlayNext = () => {
calls += 1;
return new Promise((resolve) => {
resolveOperation = resolve;
});
};
const first = queue.playNext();
const second = queue.playNext();
assert.equal(first, second);
assert.equal(calls, 1);
resolveOperation();
await first;
assert.equal(queue.playNextPromise, null);
});
test("queue snapshots do not expose filesystem paths", () => {
const queue = Object.create(MusicQueue.prototype);
queue.currentTrack = { id: "current", path: "/private/current.mp3" };
queue.queue = [{ id: "next", path: "/private/next.mp3" }];
queue.playbackStatus = "playing";
assert.deepEqual(queue.snapshot(), {
current: { id: "current" },
upcoming: [{ id: "next" }],
status: "playing",
});
});

21
test/serializers.test.js Normal file
View file

@ -0,0 +1,21 @@
import test from "node:test";
import assert from "node:assert/strict";
import { serializeTrack, serializeTracks } from "../src/web/serializers.js";
test("serializeTrack removes internal filesystem fields", () => {
const result = serializeTrack({
id: "track-1",
title: "Test",
path: "/private/storage/test.mp3",
storedName: "test.mp3",
});
assert.deepEqual(result, { id: "track-1", title: "Test" });
});
test("serializeTracks handles an array and null tracks", () => {
assert.deepEqual(serializeTracks([{ id: "1", path: "/tmp/1" }]), [
{ id: "1" },
]);
assert.equal(serializeTrack(null), null);
});

View file

@ -0,0 +1,83 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
test("the library persists portable entries and restores absolute runtime paths", async () => {
const originalDirectory = process.cwd();
const temporaryDirectory = fs.mkdtempSync(
path.join(os.tmpdir(), "palico-storage-")
);
try {
process.chdir(temporaryDirectory);
const storage = await import(
`../src/web/storage/storageManager.js?test=${Date.now()}`
);
storage.ensureStorageLayout();
const uploadedPath = path.join(storage.TEMP_DIR, "upload");
fs.writeFileSync(uploadedPath, "fake audio content");
const track = storage.registerUploadedFile(
{
path: uploadedPath,
originalname: "sample.mp3",
size: 18,
mimetype: "audio/mpeg",
},
{ permanent: true, uploader: "test", title: "Sample" }
);
const persisted = JSON.parse(
fs.readFileSync(
path.join(temporaryDirectory, "storage/runtime/library.json"),
"utf8"
)
);
assert.equal(persisted.length, 1);
assert.equal("path" in persisted[0], false);
const restored = storage.findPermanentTrack(track.libraryTrackId);
assert.equal(path.dirname(restored.path), storage.PERMANENT_DIR);
assert.equal(fs.existsSync(restored.path), true);
} finally {
process.chdir(originalDirectory);
fs.rmSync(temporaryDirectory, { recursive: true, force: true });
}
});
test("legacy library entries migrate without their absolute paths", async () => {
const originalDirectory = process.cwd();
const temporaryDirectory = fs.mkdtempSync(
path.join(os.tmpdir(), "palico-migration-")
);
try {
process.chdir(temporaryDirectory);
fs.mkdirSync("storage", { recursive: true });
fs.writeFileSync(
"storage/library.json",
JSON.stringify([
{
id: "legacy",
storedName: "legacy.mp3",
path: "/old/server/storage/permanent/legacy.mp3",
},
])
);
const storage = await import(
`../src/web/storage/storageManager.js?migration=${Date.now()}`
);
storage.ensureStorageLayout();
const migrated = JSON.parse(
fs.readFileSync("storage/runtime/library.json", "utf8")
);
assert.deepEqual(migrated, [{ id: "legacy", storedName: "legacy.mp3" }]);
} finally {
process.chdir(originalDirectory);
fs.rmSync(temporaryDirectory, { recursive: true, force: true });
}
});