tests / bridge.test.cjs
tests / bridge.test.cjs
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const runtime = require("../scripts/runtime.cjs");
test("Unicode output and paths with spaces survive process transport", async () => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "web security test "));
const file = path.join(directory, "child with spaces.cjs");
fs.writeFileSync(file, 'let data=""; process.stdin.on("data",s=>data+=s); process.stdin.on("end",()=>process.stdout.write(data));');
const text = JSON.stringify({ observation: "未確認です。日本語定型文", data: "x".repeat(70000) });
const result = await runtime.run(process.execPath, [file], { input: text });
assert.equal(result.stdout, text);
});
test("cancelled processes stop and don't return partial reports", async () => {
const ctl = new AbortController();
const result = runtime.run(process.execPath, ["-e", "setTimeout(()=>{},30000)"], { signal: ctl.signal });
setTimeout(() => ctl.abort(), 100);
await assert.rejects(result, /Cancelled/);
});
test("timeout and output limits fail instead of truncating a successful result", async () => {
await assert.rejects(runtime.run(process.execPath, ["-e", "setTimeout(()=>{},30000)"], { timeout: 100 }), /timed out/);
await assert.rejects(runtime.run(process.execPath, ["-e", "console.log('x'.repeat(10000))"], { maxBytes: 1000 }), /exceeded/);
});
test("Python bridge preserves scanner output and masks cookie values", { skip: !process.env.WEB_SECURITY_TEST_PYTHON }, async () => {
const python = process.env.WEB_SECURITY_TEST_PYTHON;
const root = path.resolve(__dirname, "..");
const reportDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "web-security-reports-test-"));
const result = await runtime.invoke("explain_capabilities", {}, { python, reportDirectory });
assert.equal(JSON.parse(result).version, "0.6.0");
assert.equal(JSON.parse(result).report_storage.directory, reportDirectory);
const code = `import sys,json,httpx;sys.path.insert(0,${JSON.stringify(path.join(root,"python"))});import server
r=httpx.Response(200,headers={'content-type':'text/html','set-cookie':'PHPSESSID=SECRET_HUB_TEST'},text='<script src="https://cdn.jsdelivr.net/npm/axios/a.js"></script>',request=httpx.Request('GET','https://example.com/'))
server._fetch=lambda url:(r,[url])
sys.stdin=__import__('io').StringIO(json.dumps({'tool':'scan_url','arguments':{'url':'https://example.com/'}}))
import bridge_entry;raise SystemExit(bridge_entry.main())`;
const response = await runtime.run(python, ["-c", code], { env: { PYTHONIOENCODING: "utf-8", WEB_SECURITY_MCP_REPORT_DIR: reportDirectory } });
const report = JSON.parse(response.stdout);
assert.equal(report.saved_report.status, "saved");
assert.deepEqual(JSON.parse(fs.readFileSync(report.saved_report.path, "utf8")), report);
assert.equal(report.review.format_version, "3.0");
assert.ok(report.review.finding_rows.every(row => row.translation_status === "reviewed"));
assert.ok(!response.stdout.includes("SECRET_HUB_TEST"));
const text = report.review.finding_rows.find(row => row.finding === "sri_mutable_url").protected_markdown;
assert.ok(text.includes("侵害の有無は未確認です。"));
await assert.rejects(runtime.invoke("scan_url", { url: "http://127.0.0.1/" }, { python, reportDirectory }), /non-public|blocked|not allowed/i);
});
test("published manifest and source are standalone, not an Advanced fork", () => {
const root = path.resolve(__dirname, "..");
const manifest = JSON.parse(fs.readFileSync(path.join(root, "manifest.json")));
assert.equal(manifest.owner, "yuanjumay");
assert.equal(manifest.name, "web-security-mcp");
for (const name of ["server.py", "review.py", "protected_text.py", "requirements.txt", "bridge_entry.py"]) {
assert.ok(fs.existsSync(path.join(root, "python", name)));
}
});
test("LM Studio provider registers two tools and uses chat storage", { skip: !process.env.WEB_SECURITY_TEST_PYTHON }, async () => {
globalThis.__LMS_PLUGIN_CONTEXT = true;
const { toolsProvider } = require("../dist/toolsProvider.js");
const workingDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "web-security-chat-"));
const values = { pythonExecutable: process.env.WEB_SECURITY_TEST_PYTHON, reportDirectory: "" };
const controller = { getPluginConfig: () => ({ get: key => values[key] }), getWorkingDirectory: () => workingDirectory };
const tools = await toolsProvider(controller);
assert.deepEqual(tools.map(t => t.name), ["scan_url", "explain_capabilities"]);
const context = { signal: new AbortController().signal };
const result = JSON.parse(await tools[1].implementation({}, context));
assert.equal(result.report_storage.directory, path.join(workingDirectory, "web-security-reports"));
values.reportDirectory = "relative/path";
const error = await tools[1].implementation({}, context);
assert.ok(error.includes("絶対パス"));
});
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const runtime = require("../scripts/runtime.cjs");
test("Unicode output and paths with spaces survive process transport", async () => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "web security test "));
const file = path.join(directory, "child with spaces.cjs");
fs.writeFileSync(file, 'let data=""; process.stdin.on("data",s=>data+=s); process.stdin.on("end",()=>process.stdout.write(data));');
const text = JSON.stringify({ observation: "未確認です。日本語定型文", data: "x".repeat(70000) });
const result = await runtime.run(process.execPath, [file], { input: text });
assert.equal(result.stdout, text);
});
test("cancelled processes stop and don't return partial reports", async () => {
const ctl = new AbortController();
const result = runtime.run(process.execPath, ["-e", "setTimeout(()=>{},30000)"], { signal: ctl.signal });
setTimeout(() => ctl.abort(), 100);
await assert.rejects(result, /Cancelled/);
});
test("timeout and output limits fail instead of truncating a successful result", async () => {
await assert.rejects(runtime.run(process.execPath, ["-e", "setTimeout(()=>{},30000)"], { timeout: 100 }), /timed out/);
await assert.rejects(runtime.run(process.execPath, ["-e", "console.log('x'.repeat(10000))"], { maxBytes: 1000 }), /exceeded/);
});
test("Python bridge preserves scanner output and masks cookie values", { skip: !process.env.WEB_SECURITY_TEST_PYTHON }, async () => {
const python = process.env.WEB_SECURITY_TEST_PYTHON;
const root = path.resolve(__dirname, "..");
const reportDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "web-security-reports-test-"));
const result = await runtime.invoke("explain_capabilities", {}, { python, reportDirectory });
assert.equal(JSON.parse(result).version, "0.6.0");
assert.equal(JSON.parse(result).report_storage.directory, reportDirectory);
const code = `import sys,json,httpx;sys.path.insert(0,${JSON.stringify(path.join(root,"python"))});import server
r=httpx.Response(200,headers={'content-type':'text/html','set-cookie':'PHPSESSID=SECRET_HUB_TEST'},text='<script src="https://cdn.jsdelivr.net/npm/axios/a.js"></script>',request=httpx.Request('GET','https://example.com/'))
server._fetch=lambda url:(r,[url])
sys.stdin=__import__('io').StringIO(json.dumps({'tool':'scan_url','arguments':{'url':'https://example.com/'}}))
import bridge_entry;raise SystemExit(bridge_entry.main())`;
const response = await runtime.run(python, ["-c", code], { env: { PYTHONIOENCODING: "utf-8", WEB_SECURITY_MCP_REPORT_DIR: reportDirectory } });
const report = JSON.parse(response.stdout);
assert.equal(report.saved_report.status, "saved");
assert.deepEqual(JSON.parse(fs.readFileSync(report.saved_report.path, "utf8")), report);
assert.equal(report.review.format_version, "3.0");
assert.ok(report.review.finding_rows.every(row => row.translation_status === "reviewed"));
assert.ok(!response.stdout.includes("SECRET_HUB_TEST"));
const text = report.review.finding_rows.find(row => row.finding === "sri_mutable_url").protected_markdown;
assert.ok(text.includes("侵害の有無は未確認です。"));
await assert.rejects(runtime.invoke("scan_url", { url: "http://127.0.0.1/" }, { python, reportDirectory }), /non-public|blocked|not allowed/i);
});
test("published manifest and source are standalone, not an Advanced fork", () => {
const root = path.resolve(__dirname, "..");
const manifest = JSON.parse(fs.readFileSync(path.join(root, "manifest.json")));
assert.equal(manifest.owner, "yuanjumay");
assert.equal(manifest.name, "web-security-mcp");
for (const name of ["server.py", "review.py", "protected_text.py", "requirements.txt", "bridge_entry.py"]) {
assert.ok(fs.existsSync(path.join(root, "python", name)));
}
});
test("LM Studio provider registers two tools and uses chat storage", { skip: !process.env.WEB_SECURITY_TEST_PYTHON }, async () => {
globalThis.__LMS_PLUGIN_CONTEXT = true;
const { toolsProvider } = require("../dist/toolsProvider.js");
const workingDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "web-security-chat-"));
const values = { pythonExecutable: process.env.WEB_SECURITY_TEST_PYTHON, reportDirectory: "" };
const controller = { getPluginConfig: () => ({ get: key => values[key] }), getWorkingDirectory: () => workingDirectory };
const tools = await toolsProvider(controller);
assert.deepEqual(tools.map(t => t.name), ["scan_url", "explain_capabilities"]);
const context = { signal: new AbortController().signal };
const result = JSON.parse(await tools[1].implementation({}, context));
assert.equal(result.report_storage.directory, path.join(workingDirectory, "web-security-reports"));
values.reportDirectory = "relative/path";
const error = await tools[1].implementation({}, context);
assert.ok(error.includes("絶対パス"));
});