165 lines
6.2 KiB
JavaScript
165 lines
6.2 KiB
JavaScript
const assert = require("assert");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const {
|
|
generateCommandPreview,
|
|
findDynamicSegments,
|
|
normalizePreviewText,
|
|
previewParts
|
|
} = require("../src/services/command-preview");
|
|
const {
|
|
DELAY_MS,
|
|
issueConfirmation,
|
|
consumeConfirmation,
|
|
isDestructivePath
|
|
} = require("../src/services/destructive-confirm");
|
|
const { db, migrate } = require("../src/services/db");
|
|
|
|
async function run() {
|
|
migrate();
|
|
const columns = db.prepare("PRAGMA table_info(custom_commands)").all().map((row) => row.name);
|
|
for (const column of [
|
|
"preview_text",
|
|
"preview_status",
|
|
"preview_error",
|
|
"preview_generated_at",
|
|
"preview_dynamic_segments"
|
|
]) {
|
|
assert(columns.includes(column), `Missing custom command preview column: ${column}`);
|
|
}
|
|
|
|
const preview = await generateCommandPreview({
|
|
language: "js",
|
|
code: `
|
|
function run(ctx) {
|
|
ctx.reply("Hello " + ctx.user.username);
|
|
return "Balance 1234, id " + ctx.user.id + ", date " + new Date().toISOString();
|
|
}
|
|
`
|
|
});
|
|
assert.equal(preview.preview_status, "ready");
|
|
assert.match(preview.preview_text, /Hello some_user/);
|
|
assert.equal(preview.preview_text.includes("Some User"), false);
|
|
const segments = JSON.parse(preview.preview_dynamic_segments);
|
|
for (const type of ["username", "number", "id", "date"]) {
|
|
assert(segments.some((segment) => segment.type === type), `Missing ${type} dynamic segment`);
|
|
}
|
|
assert(previewParts(preview.preview_text, segments).some((part) => part.dynamic));
|
|
assert.equal(normalizePreviewText("Some User and some-user"), "some_user and some_user");
|
|
assert(findDynamicSegments("some_user 42 user_123 2026-01-02T12:34:56.000Z").length, 4);
|
|
|
|
const noSideEffects = await generateCommandPreview({
|
|
language: "js",
|
|
code: `
|
|
async function run(ctx) {
|
|
await ctx.economy.add(ctx.user.id, 999999);
|
|
await ctx.inventory.remove(ctx.user.id, "all");
|
|
await ctx.moderation.ban(ctx.user.id);
|
|
await ctx.files.write("outside.txt", "blocked");
|
|
ctx.db.prepare("DELETE FROM users").run();
|
|
return "Balance " + await ctx.economy.getBalance(ctx.user.id);
|
|
}
|
|
`
|
|
});
|
|
assert.equal(noSideEffects.preview_status, "ready");
|
|
assert.equal(noSideEffects.preview_text, "Balance 1234");
|
|
|
|
const readOnlyFetch = await generateCommandPreview({
|
|
language: "js",
|
|
code: `
|
|
async function run() {
|
|
const response = await fetch("data:application/json,%7B%22message%22%3A%22preview%22%7D");
|
|
const data = await response.json();
|
|
return data.message;
|
|
}
|
|
`
|
|
});
|
|
assert.equal(readOnlyFetch.preview_status, "ready");
|
|
assert.equal(readOnlyFetch.preview_text, "preview");
|
|
|
|
const blockedWriteFetch = await generateCommandPreview({
|
|
language: "js",
|
|
code: `
|
|
async function run() {
|
|
await fetch("data:text/plain,no", { method: "POST", body: "write" });
|
|
return "no";
|
|
}
|
|
`
|
|
});
|
|
assert.equal(blockedWriteFetch.preview_status, "unavailable");
|
|
assert.match(blockedWriteFetch.preview_error, /read-only GET and HEAD/i);
|
|
|
|
const blocked = await generateCommandPreview({
|
|
language: "js",
|
|
code: "function run() { return process.cwd(); }"
|
|
});
|
|
assert.equal(blocked.preview_status, "unavailable");
|
|
assert.match(blocked.preview_error, /blocks filesystem, process/i);
|
|
|
|
const blockedImport = await generateCommandPreview({
|
|
language: "python",
|
|
code: "import os\ndef run(ctx):\n return os.getcwd()"
|
|
});
|
|
assert.equal(blockedImport.preview_status, "unavailable");
|
|
assert.match(blockedImport.preview_error, /blocks imports/i);
|
|
|
|
const timedOut = await generateCommandPreview({
|
|
language: "js",
|
|
code: "function run() { while (true) {} }"
|
|
});
|
|
assert.equal(timedOut.preview_status, "unavailable");
|
|
assert.match(timedOut.preview_error, /timed out/i);
|
|
|
|
const req = fakeRequest();
|
|
assert.equal(isDestructivePath("/admin/commands/1/delete"), true);
|
|
assert.equal(isDestructivePath("/admin/commands/1/update"), false);
|
|
const first = issueConfirmation(req, "/admin/commands/1/delete");
|
|
assert.equal(first.delay_seconds, DELAY_MS / 1000);
|
|
assert.equal(consumeConfirmation(req, "/admin/commands/1/delete", first.token).reason, "too_early");
|
|
|
|
const second = issueConfirmation(req, "/admin/commands/1/delete");
|
|
req.session.destructive_confirmations[second.token].not_before = Date.now() - 1;
|
|
assert.equal(consumeConfirmation(req, "/admin/pages/1/delete", second.token).reason, "action_mismatch");
|
|
|
|
const third = issueConfirmation(req, "/admin/commands/1/delete");
|
|
req.session.destructive_confirmations[third.token].not_before = Date.now() - 1;
|
|
assert.equal(consumeConfirmation(req, "/admin/commands/1/delete", third.token).valid, true);
|
|
assert.equal(consumeConfirmation(req, "/admin/commands/1/delete", third.token).valid, false);
|
|
|
|
const appScript = fs.readFileSync(path.join(__dirname, "..", "src", "web", "public", "app.js"), "utf8");
|
|
const layout = fs.readFileSync(path.join(__dirname, "..", "src", "web", "views", "partials", "layout-bottom.ejs"), "utf8");
|
|
const commandView = fs.readFileSync(path.join(__dirname, "..", "src", "web", "views", "admin-commands.ejs"), "utf8");
|
|
assert(appScript.includes("${confirmLabel(form, submitter)} in ${remaining}"));
|
|
assert(appScript.includes('button.disabled = remaining > 0'));
|
|
assert(appScript.includes('fetch("/api/destructive-confirmations"'));
|
|
assert(appScript.includes("event.preventDefault();"));
|
|
assert(appScript.includes("expiryTimer"));
|
|
assert(appScript.includes("resetDestructive"));
|
|
assert(layout.includes("data-destructive-modal"));
|
|
assert(layout.includes("data-destructive-confirm disabled"));
|
|
assert(commandView.includes("Preview unavailable"));
|
|
assert(commandView.includes("preview-dynamic"));
|
|
assert(commandView.includes(">Static<"));
|
|
assert(commandView.includes(">Dynamic<"));
|
|
assert.equal(commandView.includes('"Advanced (" + command.language + ")"'), false);
|
|
|
|
console.log("Command preview and destructive confirmation verification passed.");
|
|
}
|
|
|
|
function fakeRequest() {
|
|
return {
|
|
session: {
|
|
user: { id: "admin-user", isAdmin: true }
|
|
},
|
|
body: {},
|
|
get() {
|
|
return null;
|
|
}
|
|
};
|
|
}
|
|
|
|
run().catch((error) => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
});
|