mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-04-04 09:45:51 +00:00
- Introduced a new test suite for App propagation sync metrics, validating toast notifications for sync status. - Enhanced ConfirmDialog tests to verify heading display and button attributes. - Added additional tests for FormLabel, FormSubLabel, LanguageSelector, and other components to improve test coverage and ensure proper functionality.
318 lines
13 KiB
JavaScript
318 lines
13 KiB
JavaScript
import { describe, it, expect } from "vitest";
|
||
import MarkdownRenderer from "@/js/MarkdownRenderer";
|
||
|
||
describe("MarkdownRenderer.js", () => {
|
||
describe("render", () => {
|
||
it("renders basic text correctly", () => {
|
||
expect(MarkdownRenderer.render("Hello")).toContain("Hello");
|
||
});
|
||
|
||
it("renders bold text correctly", () => {
|
||
const result = MarkdownRenderer.render("**Bold**");
|
||
expect(result).toContain("<strong>Bold</strong>");
|
||
});
|
||
|
||
it("renders italic text correctly", () => {
|
||
const result = MarkdownRenderer.render("*Italic*");
|
||
expect(result).toContain("<em>Italic</em>");
|
||
});
|
||
|
||
it("renders bold and italic text correctly", () => {
|
||
const result = MarkdownRenderer.render("***Bold and Italic***");
|
||
expect(result).toContain("<strong><em>Bold and Italic</em></strong>");
|
||
});
|
||
|
||
it("renders headers correctly", () => {
|
||
expect(MarkdownRenderer.render("# Header 1")).toContain("<h1");
|
||
expect(MarkdownRenderer.render("## Header 2")).toContain("<h2");
|
||
expect(MarkdownRenderer.render("### Header 3")).toContain("<h3");
|
||
});
|
||
|
||
it("renders inline code correctly", () => {
|
||
const result = MarkdownRenderer.render("`code`");
|
||
expect(result).toContain("<code");
|
||
expect(result).toContain("code");
|
||
});
|
||
|
||
it("renders fenced code blocks correctly", () => {
|
||
const result = MarkdownRenderer.render("```python\nprint('hello')\n```");
|
||
expect(result).toContain("<pre");
|
||
expect(result).toContain("<code");
|
||
expect(result).toContain("language-python");
|
||
expect(result).toContain("print('hello')");
|
||
});
|
||
|
||
it("handles paragraphs correctly", () => {
|
||
const result = MarkdownRenderer.render("Para 1\n\nPara 2");
|
||
expect(result).toContain("<p");
|
||
expect(result).toContain("Para 1");
|
||
expect(result).toContain("Para 2");
|
||
});
|
||
});
|
||
|
||
describe("security: XSS prevention", () => {
|
||
it("escapes script tags", () => {
|
||
const malformed = "<script>alert('xss')</script>";
|
||
const result = MarkdownRenderer.render(malformed);
|
||
expect(result).not.toContain("<script>");
|
||
expect(result).toContain("<script>");
|
||
});
|
||
|
||
it("escapes onerror attributes in images", () => {
|
||
const malformed = '<img src="x" onerror="alert(1)">';
|
||
const result = MarkdownRenderer.render(malformed);
|
||
expect(result).not.toContain("<img");
|
||
expect(result).toContain("<img");
|
||
expect(result).toContain("onerror="alert(1)"");
|
||
});
|
||
|
||
it("escapes html in code blocks", () => {
|
||
const malformed = "```\n<script>alert(1)</script>\n```";
|
||
const result = MarkdownRenderer.render(malformed);
|
||
expect(result).toContain("<script>");
|
||
});
|
||
|
||
it("escapes html in inline code", () => {
|
||
const malformed = "`<script>alert(1)</script>`";
|
||
const result = MarkdownRenderer.render(malformed);
|
||
expect(result).toContain("<script>");
|
||
});
|
||
});
|
||
|
||
describe("reticulum links", () => {
|
||
it("detects nomadnet:// links with hash and path", () => {
|
||
const text = "check this out: nomadnet://1dfeb0d794963579bd21ac8f153c77a4:/page/index.mu";
|
||
const result = MarkdownRenderer.render(text);
|
||
expect(result).toContain('data-nomadnet-url="1dfeb0d794963579bd21ac8f153c77a4:/page/index.mu"');
|
||
expect(result).toContain("nomadnet://1dfeb0d794963579bd21ac8f153c77a4:/page/index.mu");
|
||
});
|
||
|
||
it("detects bare hash and path links as nomadnet", () => {
|
||
const text = "node is at 1dfeb0d794963579bd21ac8f153c77a4:/page/index.mu";
|
||
const result = MarkdownRenderer.render(text);
|
||
expect(result).toContain('data-nomadnet-url="1dfeb0d794963579bd21ac8f153c77a4:/page/index.mu"');
|
||
expect(result).toContain("1dfeb0d794963579bd21ac8f153c77a4:/page/index.mu");
|
||
});
|
||
|
||
it("detects nomadnet:// links with just hash", () => {
|
||
const text = "nomadnet://1dfeb0d794963579bd21ac8f153c77a4";
|
||
const result = MarkdownRenderer.render(text);
|
||
expect(result).toContain('data-nomadnet-url="1dfeb0d794963579bd21ac8f153c77a4:/page/index.mu"');
|
||
});
|
||
|
||
it("detects bare hash as lxmf link", () => {
|
||
const text = "send to 1dfeb0d794963579bd21ac8f153c77a4";
|
||
const result = MarkdownRenderer.render(text);
|
||
expect(result).toContain('class="lxmf-link');
|
||
expect(result).toContain('data-lxmf-address="1dfeb0d794963579bd21ac8f153c77a4"');
|
||
});
|
||
|
||
it("does not detect invalid hashes", () => {
|
||
const text = "not-a-hash:/page/index.mu";
|
||
const result = MarkdownRenderer.render(text);
|
||
expect(result).not.toContain("nomadnet-link");
|
||
expect(result).not.toContain("lxmf-link");
|
||
});
|
||
});
|
||
|
||
describe("fuzzing: stability testing", () => {
|
||
const generateRandomString = (length) => {
|
||
const chars =
|
||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;':\",./<>?`~ \n\r\t";
|
||
let result = "";
|
||
for (let i = 0; i < length; i++) {
|
||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||
}
|
||
return result;
|
||
};
|
||
|
||
it("handles random inputs without crashing (100 iterations)", () => {
|
||
for (let i = 0; i < 100; i++) {
|
||
const randomText = generateRandomString(Math.floor(Math.random() * 1000));
|
||
expect(() => {
|
||
MarkdownRenderer.render(randomText);
|
||
}).not.toThrow();
|
||
}
|
||
});
|
||
|
||
it("handles deeply nested or complex markdown patterns without crashing", () => {
|
||
const complex = "# ".repeat(100) + "**".repeat(100) + "```".repeat(100) + "```\n".repeat(10);
|
||
expect(() => {
|
||
MarkdownRenderer.render(complex);
|
||
}).not.toThrow();
|
||
});
|
||
|
||
it("handles large inputs correctly (1MB of random text)", () => {
|
||
const largeText = generateRandomString(1024 * 1024);
|
||
const start = Date.now();
|
||
const result = MarkdownRenderer.render(largeText);
|
||
const end = Date.now();
|
||
|
||
expect(typeof result).toBe("string");
|
||
// performance check: should be relatively fast (less than 500ms for 1MB usually)
|
||
expect(end - start).toBeLessThan(1000);
|
||
});
|
||
|
||
it("handles potential ReDoS patterns (repeated separators)", () => {
|
||
// Test patterns that often cause ReDoS in poorly written markdown parsers (can never be too careful, especially on public testnets)
|
||
const redosPatterns = [
|
||
"*".repeat(10000), // Long string of bold markers
|
||
"#".repeat(10000), // Long string of header markers
|
||
"`".repeat(10000), // Long string of backticks
|
||
" ".repeat(10000) + "\n", // Long string of whitespace
|
||
"[](".repeat(5000), // Unclosed links (if we added them)
|
||
"** ".repeat(5000), // Bold marker followed by space repeated
|
||
];
|
||
|
||
redosPatterns.forEach((pattern) => {
|
||
const start = Date.now();
|
||
MarkdownRenderer.render(pattern);
|
||
const end = Date.now();
|
||
expect(end - start).toBeLessThan(100); // Should be very fast
|
||
});
|
||
});
|
||
|
||
it("handles unicode homoglyphs and special characters without interference", () => {
|
||
const homoglyphs = [
|
||
"**bold**",
|
||
"∗∗notbold∗∗", // unicode asterisks
|
||
"# header",
|
||
"# not header", // fullwidth hash
|
||
"`code`",
|
||
"`notcode`", // fullwidth backtick
|
||
];
|
||
homoglyphs.forEach((text) => {
|
||
const result = MarkdownRenderer.render(text);
|
||
expect(typeof result).toBe("string");
|
||
});
|
||
});
|
||
|
||
it("handles malformed or unclosed markdown tags gracefully", () => {
|
||
const malformed = [
|
||
"**bold",
|
||
"```python\nprint(1)",
|
||
"#header", // no space
|
||
"`code",
|
||
"___triple",
|
||
"**bold*italic**",
|
||
"***bolditalic**",
|
||
];
|
||
malformed.forEach((text) => {
|
||
expect(() => MarkdownRenderer.render(text)).not.toThrow();
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("strip", () => {
|
||
it("strips markdown correctly", () => {
|
||
const md = "# Header\n**Bold** *Italic* `code` ```\nblock\n```";
|
||
const stripped = MarkdownRenderer.strip(md);
|
||
expect(stripped).toContain("Header");
|
||
expect(stripped).toContain("Bold");
|
||
expect(stripped).toContain("Italic");
|
||
expect(stripped).toContain("code");
|
||
expect(stripped).toContain("[Code Block]");
|
||
expect(stripped).not.toContain("# ");
|
||
expect(stripped).not.toContain("**");
|
||
expect(stripped).not.toContain("` ");
|
||
});
|
||
|
||
it("strip handles null and undefined without throwing", () => {
|
||
expect(MarkdownRenderer.strip(null)).toBe("");
|
||
expect(MarkdownRenderer.strip(undefined)).toBe("");
|
||
});
|
||
|
||
it("strip handles ReDoS-prone patterns without hanging", () => {
|
||
const start = Date.now();
|
||
MarkdownRenderer.strip("*".repeat(5000));
|
||
MarkdownRenderer.strip("`".repeat(5000));
|
||
MarkdownRenderer.strip("# ".repeat(2000));
|
||
expect(Date.now() - start).toBeLessThan(200);
|
||
});
|
||
|
||
it("strip returns string for malformed and edge input", () => {
|
||
const edge = ["**no close", "```\ncode", "", " ", "\n\n", "[[CB0]] literal"];
|
||
edge.forEach((s) => {
|
||
const out = MarkdownRenderer.strip(s);
|
||
expect(typeof out).toBe("string");
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("edge: non-string and invalid input", () => {
|
||
it("render does not throw on null or undefined", () => {
|
||
expect(MarkdownRenderer.render(null)).toBe("");
|
||
expect(MarkdownRenderer.render(undefined)).toBe("");
|
||
});
|
||
|
||
it("render returns string for number input (coerced)", () => {
|
||
const r = MarkdownRenderer.render(12345);
|
||
expect(typeof r).toBe("string");
|
||
expect(r).toContain("12345");
|
||
});
|
||
|
||
it("render never returns executable script for any input", () => {
|
||
const risky = [
|
||
"<script>alert(1)</script>",
|
||
"javascript:alert(1)",
|
||
"data:text/html,<script>alert(1)</script>",
|
||
"'';alert(1);//",
|
||
];
|
||
risky.forEach((s) => {
|
||
const r = MarkdownRenderer.render(s);
|
||
expect(r).not.toMatch(/<script[\s>]/i);
|
||
expect(r).not.toMatch(/\bhref\s*=\s*["']?\s*javascript:/i);
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("edge: placeholder collision", () => {
|
||
it("literal [[CB0]] in text is not treated as code block placeholder", () => {
|
||
const text = "See [[CB0]] and [[CB1]] here.";
|
||
const result = MarkdownRenderer.render(text);
|
||
expect(result).toContain("[[CB0]]");
|
||
expect(result).toContain("[[CB1]]");
|
||
});
|
||
|
||
it("code block renders and restores; literal [[CB0]] may be replaced when code block exists", () => {
|
||
const text = "Before ```\na\n``` after.";
|
||
const result = MarkdownRenderer.render(text);
|
||
expect(result).toContain("Before");
|
||
expect(result).toContain("after");
|
||
expect(result).toContain("<pre");
|
||
});
|
||
});
|
||
|
||
describe("message-like content (decoded LXMF body)", () => {
|
||
it("content with null byte and control chars does not crash", () => {
|
||
const msg = "Hello\x00world\x07\n\t";
|
||
expect(() => MarkdownRenderer.render(msg)).not.toThrow();
|
||
const r = MarkdownRenderer.render(msg);
|
||
expect(typeof r).toBe("string");
|
||
expect(r).not.toContain("<script>");
|
||
});
|
||
|
||
it("content with mixed unicode and markdown does not inject script", () => {
|
||
const msg = "\u202eRTL **bold** <script>nope</script> \ufffd";
|
||
const r = MarkdownRenderer.render(msg);
|
||
expect(r).not.toContain("<script>");
|
||
expect(r).toContain("<script>");
|
||
});
|
||
|
||
it("very long single-line message (100k chars) completes in reasonable time", () => {
|
||
const msg = "x".repeat(100000);
|
||
const start = Date.now();
|
||
const r = MarkdownRenderer.render(msg);
|
||
expect(Date.now() - start).toBeLessThan(500);
|
||
expect(typeof r).toBe("string");
|
||
});
|
||
|
||
it("message with many newlines and markdown markers", () => {
|
||
const msg = "# ".repeat(500) + "**".repeat(500) + "\n\n\n";
|
||
expect(() => MarkdownRenderer.render(msg)).not.toThrow();
|
||
const r = MarkdownRenderer.render(msg);
|
||
expect(typeof r).toBe("string");
|
||
});
|
||
});
|
||
});
|