mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-05-03 12:35:09 +00:00
113 lines
4.1 KiB
JavaScript
113 lines
4.1 KiB
JavaScript
// SPDX-License-Identifier: 0BSD
|
|
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import App from "../../meshchatx/src/frontend/components/App.vue";
|
|
import WebSocketConnection from "../../meshchatx/src/frontend/js/WebSocketConnection";
|
|
import ToastUtils from "../../meshchatx/src/frontend/js/ToastUtils";
|
|
|
|
vi.mock("../../meshchatx/src/frontend/js/ToastUtils", () => ({
|
|
default: {
|
|
success: vi.fn(),
|
|
error: vi.fn(),
|
|
info: vi.fn(),
|
|
warning: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
vi.mock("../../meshchatx/src/frontend/js/WebSocketConnection", () => ({
|
|
default: {
|
|
send: vi.fn(),
|
|
connect: vi.fn(),
|
|
on: vi.fn(),
|
|
off: vi.fn(),
|
|
destroy: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
describe("meshchatx://docs deep links (security / fuzz)", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("routes only hostname docs, not docs- prefix spoof", () => {
|
|
const push = vi.fn();
|
|
App.methods.handleProtocolLink.call({ $router: { push } }, "meshchatx://docs-foo?reticulum=evil");
|
|
expect(push).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("accepts meshchat alias and path-style manual path", () => {
|
|
const push = vi.fn();
|
|
App.methods.handleProtocolLink.call({ $router: { push } }, "meshchat://docs/manual/interfaces.html");
|
|
expect(push).toHaveBeenCalledWith({
|
|
name: "documentation",
|
|
query: { reticulum: encodeURIComponent("manual/interfaces.html") },
|
|
});
|
|
});
|
|
|
|
it("passes XSS-shaped reticulum through encodeURIComponent only (opaque to router)", () => {
|
|
const push = vi.fn();
|
|
const malicious = "<script>alert(1)</script>";
|
|
App.methods.handleProtocolLink.call(
|
|
{ $router: { push } },
|
|
`meshchatx://docs?reticulum=${encodeURIComponent(malicious)}`
|
|
);
|
|
expect(push).toHaveBeenCalledWith({
|
|
name: "documentation",
|
|
query: { reticulum: encodeURIComponent(malicious) },
|
|
});
|
|
});
|
|
|
|
it("does not treat javascript: prefix as docs link", () => {
|
|
const push = vi.fn();
|
|
App.methods.handleProtocolLink.call({ $router: { push } }, "javascript:meshchatx://docs?reticulum=x");
|
|
expect(push).not.toHaveBeenCalledWith(expect.objectContaining({ name: "documentation" }));
|
|
});
|
|
|
|
it("fuzz random query tails without throwing", () => {
|
|
const push = vi.fn();
|
|
for (let i = 0; i < 40; i++) {
|
|
const tail = `x=${encodeURIComponent(`${i}\u0000<script>`)}`;
|
|
expect(() =>
|
|
App.methods.handleProtocolLink.call({ $router: { push } }, `meshchatx://docs?${tail}`)
|
|
).not.toThrow();
|
|
}
|
|
});
|
|
|
|
it("onWebsocketMessage docs_view navigates like handleProtocolLink", async () => {
|
|
const push = vi.fn().mockResolvedValue(undefined);
|
|
await App.methods.onWebsocketMessage.call(
|
|
{ $router: { push } },
|
|
{
|
|
data: JSON.stringify({
|
|
type: "lxm.ingest_uri.result",
|
|
status: "success",
|
|
ingest_type: "docs_view",
|
|
message: "Opening documentation.",
|
|
docs_query: { reticulum: "manual/interfaces.html#x" },
|
|
}),
|
|
}
|
|
);
|
|
expect(push).toHaveBeenCalledWith({
|
|
name: "documentation",
|
|
query: { reticulum: encodeURIComponent("manual/interfaces.html#x") },
|
|
});
|
|
expect(ToastUtils.info).toHaveBeenCalled();
|
|
});
|
|
|
|
it("onWebsocketMessage docs_view without docs_query opens documentation index", async () => {
|
|
const push = vi.fn().mockResolvedValue(undefined);
|
|
await App.methods.onWebsocketMessage.call(
|
|
{ $router: { push } },
|
|
{
|
|
data: JSON.stringify({
|
|
type: "lxm.ingest_uri.result",
|
|
status: "success",
|
|
ingest_type: "docs_view",
|
|
message: "Opening documentation.",
|
|
}),
|
|
}
|
|
);
|
|
expect(push).toHaveBeenCalledWith({ name: "documentation" });
|
|
});
|
|
});
|