Files
ihasmail/web/src/lib/__tests__/markdown.test.ts
T
jcoffey-dev 15f2c3d357 Read a Markdown file as the document it is
A .md previewed as its own source, which is reading the punctuation
rather than the notes. It now opens rendered, with Rendered | Source in
the dialog footer for anyone who wants what the file actually says.
Markdown only; a .txt has nothing to toggle between.

Rendering is `marked`, sanitised by DOMPurify -- the one the app already
carries for mail. Markdown is not a safe subset of anything: raw HTML
passes through it by design, so a <script> in a file somebody uploaded or
shared into the account is a script tag unless something takes it out.

Images become links rather than pictures. An image in a Markdown file is
either a relative path, which has no base to resolve against here, or a
URL somewhere else, which fetches on open and tells that server the file
was read -- the tracking pixel this app blocks in mail. The link keeps
the alt text and the address, so nothing vanishes silently.

Fixes the PDF preview while here, which never worked: securityHeaders
put X-Frame-Options: DENY on every response including the blob route, so
the iframe showed Chrome's "refused to connect" where the file should
have been -- in Files today and in mail attachments long before that.
The middleware now leaves a header the route has set, and a PDF served
inline says SAMEORIGIN. Nothing else on the server is framable.
2026-09-01 20:32:59 -07:00

76 lines
3.1 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { isMarkdown, renderMarkdown } from "@/lib/markdown";
describe("isMarkdown", () => {
it("takes the type when there is one", () => {
expect(isMarkdown("text/markdown", "a")).toBe(true);
expect(isMarkdown("text/x-markdown; charset=utf-8", "a")).toBe(true);
expect(isMarkdown("text/plain", "notes.txt")).toBe(false);
});
it("falls back to the name, which is the usual case for an upload", () => {
expect(isMarkdown("application/octet-stream", "README.md")).toBe(true);
expect(isMarkdown("application/octet-stream", "NOTES.MARKDOWN")).toBe(true);
expect(isMarkdown(null, "changelog.mkd")).toBe(true);
expect(isMarkdown(null, "readme.txt")).toBe(false);
expect(isMarkdown(null, null)).toBe(false);
});
});
describe("renderMarkdown", () => {
it("renders the ordinary things", () => {
const html = renderMarkdown("# Title\n\nSome **bold** and `code`.\n\n- one\n- two\n");
expect(html).toContain("<h1");
expect(html).toContain("<strong>bold</strong>");
expect(html).toContain("<code>code</code>");
expect(html).toContain("<li>one</li>");
});
it("renders GitHub tables and fenced code", () => {
const html = renderMarkdown("| a | b |\n| - | - |\n| 1 | 2 |\n\n```js\nconst x = 1;\n```\n");
expect(html).toContain("<table>");
expect(html).toContain("<pre>");
});
/*
* Markdown passes raw HTML through by design, and the file came from
* somewhere else -- an upload, or a share from another account. Every one of
* these renders as a script tag without a sanitiser.
*/
it("takes out anything that would execute", () => {
const html = renderMarkdown("<script>alert(1)</script>\n\n<img src=x onerror=alert(1)>\n\n<iframe src='https://evil.example'></iframe>\n");
expect(html).not.toContain("<script");
expect(html).not.toContain("onerror");
expect(html).not.toContain("<iframe");
});
it("does not keep a javascript: link", () => {
const html = renderMarkdown("[click](javascript:alert(1))");
expect(html).not.toContain("javascript:");
});
it("shows an image as a link instead of fetching it", () => {
// A remote image in a file is a tracking pixel by another name; this app
// blocks those in mail and does not undo that here.
const html = renderMarkdown("![a diagram](https://tracker.example/px.png)");
expect(html).not.toContain("<img");
expect(html).toContain('class="md-img"');
expect(html).toContain("a diagram");
expect(html).toContain("https://tracker.example/px.png");
});
it("keeps a relative image visible even though it cannot resolve", () => {
const html = renderMarkdown("![local](./diagram.png)");
expect(html).not.toContain("<img");
expect(html).toContain("local");
// Nothing to link to, so it is text rather than a dead link.
expect(html).not.toContain('href="./diagram.png"');
});
it("sends links out of the app safely", () => {
const html = renderMarkdown("[docs](https://docs.ihasmail.org)");
expect(html).toContain('rel="noopener noreferrer"');
expect(html).toContain('target="_blank"');
});
});