feat: 发布 v0.4.0 桌面端
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
import {
|
||||
afterEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from "vitest";
|
||||
import {
|
||||
mkdtemp,
|
||||
mkdir,
|
||||
rm,
|
||||
writeFile
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
ApplicationRequestError,
|
||||
createApplicationService
|
||||
} from "../src/index.js";
|
||||
|
||||
let temporaryDirectory: string | undefined;
|
||||
|
||||
afterEach(async () => {
|
||||
if (temporaryDirectory) {
|
||||
await rm(temporaryDirectory, {
|
||||
recursive: true,
|
||||
force: true
|
||||
});
|
||||
}
|
||||
temporaryDirectory = undefined;
|
||||
});
|
||||
|
||||
async function createThemeFixture() {
|
||||
temporaryDirectory = await mkdtemp(
|
||||
join(tmpdir(), "md-to-pdf-application-")
|
||||
);
|
||||
const bundledRoot = join(temporaryDirectory, "bundled");
|
||||
const localRoot = join(temporaryDirectory, "local");
|
||||
const themeRoot = join(bundledRoot, "test-theme");
|
||||
await mkdir(join(themeRoot, "fonts"), { recursive: true });
|
||||
await mkdir(localRoot, { recursive: true });
|
||||
await writeFile(
|
||||
join(themeRoot, "theme.json"),
|
||||
JSON.stringify({
|
||||
manifestVersion: 1,
|
||||
id: "test-theme",
|
||||
name: "测试主题",
|
||||
version: "1.0.0",
|
||||
description: "共享应用服务测试主题",
|
||||
author: "test",
|
||||
license: "MIT",
|
||||
entry: "theme.css",
|
||||
domPreset: "typora",
|
||||
defaultFontSize: "16px",
|
||||
supportedFeatures: ["code", "table"],
|
||||
bundled: true
|
||||
}),
|
||||
"utf8"
|
||||
);
|
||||
await writeFile(
|
||||
join(themeRoot, "theme.css"),
|
||||
[
|
||||
"@font-face {",
|
||||
" font-family: Test;",
|
||||
" src: url('./fonts/test.woff2') format('woff2');",
|
||||
"}",
|
||||
"#write { font-family: Test; }"
|
||||
].join("\n"),
|
||||
"utf8"
|
||||
);
|
||||
await writeFile(
|
||||
join(themeRoot, "fonts", "test.woff2"),
|
||||
Buffer.from([0, 1, 2, 3])
|
||||
);
|
||||
return { bundledRoot, localRoot };
|
||||
}
|
||||
|
||||
describe("共享应用服务", () => {
|
||||
it("渲染安全 Markdown 文档", async () => {
|
||||
const roots = await createThemeFixture();
|
||||
const service = createApplicationService(roots);
|
||||
|
||||
const document = service.render({
|
||||
markdown: "# 文档\n\n<script>alert('xss')</script>",
|
||||
language: "zh-CN"
|
||||
});
|
||||
|
||||
expect(document.articleHtml).toContain('id="write"');
|
||||
expect(document.articleHtml).not.toContain("<script");
|
||||
expect(document.metadata.title).toBe("文档");
|
||||
});
|
||||
|
||||
it("以稳定错误协议拒绝非法请求", async () => {
|
||||
const roots = await createThemeFixture();
|
||||
const service = createApplicationService(roots);
|
||||
|
||||
expect(() => service.render({ markdown: 42 })).toThrow(
|
||||
expect.objectContaining<ApplicationRequestError>({
|
||||
statusCode: 400,
|
||||
code: "INVALID_MARKDOWN"
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("列出主题并使用调用方提供的资源 URL", async () => {
|
||||
const roots = await createThemeFixture();
|
||||
const service = createApplicationService({
|
||||
...roots,
|
||||
createAssetUrl: (themeId, assetPath) =>
|
||||
`mdpdf://theme/${themeId}/${assetPath}`
|
||||
});
|
||||
|
||||
await expect(service.listThemes()).resolves.toEqual({
|
||||
themes: [
|
||||
expect.objectContaining({
|
||||
id: "test-theme",
|
||||
bundled: true,
|
||||
source: "bundled"
|
||||
})
|
||||
]
|
||||
});
|
||||
await expect(
|
||||
service.getThemeCss("test-theme")
|
||||
).resolves.toContain(
|
||||
"mdpdf://theme/test-theme/fonts/test.woff2"
|
||||
);
|
||||
});
|
||||
|
||||
it("安全读取主题二进制资源", async () => {
|
||||
const roots = await createThemeFixture();
|
||||
const service = createApplicationService(roots);
|
||||
|
||||
await expect(
|
||||
service.getThemeAsset("test-theme", "fonts/test.woff2")
|
||||
).resolves.toEqual({
|
||||
contentType: "font/woff2",
|
||||
content: Buffer.from([0, 1, 2, 3])
|
||||
});
|
||||
await expect(
|
||||
service.getThemeAsset("test-theme", "../test.woff2")
|
||||
).rejects.toThrow("主题资源路径不安全");
|
||||
});
|
||||
|
||||
it("内置主题优先于同 ID 的旧本地副本", async () => {
|
||||
const roots = await createThemeFixture();
|
||||
const localThemeRoot = join(roots.localRoot, "test-theme");
|
||||
await mkdir(localThemeRoot, { recursive: true });
|
||||
await writeFile(
|
||||
join(localThemeRoot, "theme.json"),
|
||||
JSON.stringify({
|
||||
manifestVersion: 1,
|
||||
id: "test-theme",
|
||||
name: "旧本地主题",
|
||||
version: "local",
|
||||
description: "迁移前的本地副本",
|
||||
author: "test",
|
||||
license: "local-only",
|
||||
entry: "theme.css",
|
||||
domPreset: "typora",
|
||||
defaultFontSize: "16px",
|
||||
supportedFeatures: ["code", "table"],
|
||||
bundled: false
|
||||
}),
|
||||
"utf8"
|
||||
);
|
||||
await writeFile(
|
||||
join(localThemeRoot, "theme.css"),
|
||||
"#write { color: red; }",
|
||||
"utf8"
|
||||
);
|
||||
const onWarning = vi.fn();
|
||||
const service = createApplicationService({
|
||||
...roots,
|
||||
onWarning
|
||||
});
|
||||
|
||||
await expect(service.listThemes()).resolves.toEqual({
|
||||
themes: [
|
||||
expect.objectContaining({
|
||||
id: "test-theme",
|
||||
name: "测试主题",
|
||||
bundled: true
|
||||
})
|
||||
]
|
||||
});
|
||||
expect(onWarning).toHaveBeenCalledWith(
|
||||
"已忽略与内置主题同 ID 的本地主题 test-theme"
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user