Harden bot, storage and web API
This commit is contained in:
parent
e974bc6660
commit
20624a5c9c
43 changed files with 1265 additions and 1012 deletions
49
test/auth.test.js
Normal file
49
test/auth.test.js
Normal 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
58
test/commands.test.js
Normal 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
52
test/musicQueue.test.js
Normal 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
21
test/serializers.test.js
Normal 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);
|
||||
});
|
||||
83
test/storageManager.test.js
Normal file
83
test/storageManager.test.js
Normal 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 });
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue