Files
MeshChatX/tests/frontend/MarkdownRenderer.test.js
Sudo-Ivan 21450c69eb Add AppPropagationSync tests and enhance ConfirmDialog tests
- 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.
2026-02-14 19:18:37 -06:00

318 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(&#039;hello&#039;)");
});
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("&lt;script&gt;");
});
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("&lt;img");
expect(result).toContain("onerror=&quot;alert(1)&quot;");
});
it("escapes html in code blocks", () => {
const malformed = "```\n<script>alert(1)</script>\n```";
const result = MarkdownRenderer.render(malformed);
expect(result).toContain("&lt;script&gt;");
});
it("escapes html in inline code", () => {
const malformed = "`<script>alert(1)</script>`";
const result = MarkdownRenderer.render(malformed);
expect(result).toContain("&lt;script&gt;");
});
});
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("&lt;script&gt;");
});
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");
});
});
});