diff --git a/sql/blog-menu.sql b/sql/blog-menu.sql index b8f652d..dae17d4 100644 --- a/sql/blog-menu.sql +++ b/sql/blog-menu.sql @@ -11,6 +11,8 @@ VALUES (2041700000000120302, 2041700000000100301, 'BlogArticleEdit', NULL, NULL, NULL, 'Blog:Article:Edit', 'button', '{"title":"common.edit"}', 1, 0), (2041700000000120303, 2041700000000100301, 'BlogArticleDelete', NULL, NULL, NULL, 'Blog:Article:Delete', 'button', '{"title":"common.delete"}', 1, 0), (2041700000000120304, 2041700000000100301, 'BlogArticleImport', NULL, NULL, NULL, 'Blog:Article:Import', 'button', '{"title":"导入 WordPress"}', 1, 1), + (2041700000000120305, 2041700000000100301, 'BlogArticlePreviewButton', NULL, NULL, NULL, 'Blog:Article:Preview', 'button', '{"title":"预览"}', 1, 2), + (2041700000000100305, 2041700000000100300, 'BlogArticlePreview', '/blog/article/:articleId/preview', '/blog/article/preview/index', NULL, 'Blog:Article:Preview', 'menu', '{"activePath":"/blog/article","hideInMenu":true,"title":"文章预览"}', 1, 4), (2041700000000100302, 2041700000000100300, 'BlogCategory', '/blog/category', '/blog/category/list', NULL, 'Blog:Category:List', 'menu', '{"icon":"lucide:folder-tree","title":"分类管理"}', 1, 1), (2041700000000120311, 2041700000000100302, 'BlogCategoryCreate', NULL, NULL, NULL, 'Blog:Category:Create', 'button', '{"title":"common.create"}', 1, 0), (2041700000000120312, 2041700000000100302, 'BlogCategoryEdit', NULL, NULL, NULL, 'Blog:Category:Edit', 'button', '{"title":"common.edit"}', 1, 0), diff --git a/sql/vben-admin-init.sql b/sql/vben-admin-init.sql index 07b1693..7b6c7d9 100644 --- a/sql/vben-admin-init.sql +++ b/sql/vben-admin-init.sql @@ -245,6 +245,8 @@ VALUES (2041700000000120302, 2041700000000100301, 'BlogArticleEdit', NULL, NULL, NULL, 'Blog:Article:Edit', 'button', '{"title":"common.edit"}', 1, 0), (2041700000000120303, 2041700000000100301, 'BlogArticleDelete', NULL, NULL, NULL, 'Blog:Article:Delete', 'button', '{"title":"common.delete"}', 1, 0), (2041700000000120304, 2041700000000100301, 'BlogArticleImport', NULL, NULL, NULL, 'Blog:Article:Import', 'button', '{"title":"导入 WordPress"}', 1, 1), + (2041700000000120305, 2041700000000100301, 'BlogArticlePreviewButton', NULL, NULL, NULL, 'Blog:Article:Preview', 'button', '{"title":"预览"}', 1, 2), + (2041700000000100305, 2041700000000100300, 'BlogArticlePreview', '/blog/article/:articleId/preview', '/blog/article/preview/index', NULL, 'Blog:Article:Preview', 'menu', '{"activePath":"/blog/article","hideInMenu":true,"title":"文章预览"}', 1, 4), (2041700000000100302, 2041700000000100300, 'BlogCategory', '/blog/category', '/blog/category/list', NULL, 'Blog:Category:List', 'menu', '{"icon":"lucide:folder-tree","title":"分类管理"}', 1, 1), (2041700000000120311, 2041700000000100302, 'BlogCategoryCreate', NULL, NULL, NULL, 'Blog:Category:Create', 'button', '{"title":"common.create"}', 1, 0), (2041700000000120312, 2041700000000100302, 'BlogCategoryEdit', NULL, NULL, NULL, 'Blog:Category:Edit', 'button', '{"title":"common.edit"}', 1, 0), diff --git a/src/modules/blog/application/blog-article.service.ts b/src/modules/blog/application/blog-article.service.ts index b8db4ef..cc1fc6e 100644 --- a/src/modules/blog/application/blog-article.service.ts +++ b/src/modules/blog/application/blog-article.service.ts @@ -362,7 +362,7 @@ export class BlogArticleService { ); } else { nextArticle.contentMarkdown = current?.contentMarkdown || ''; - nextArticle.contentHtml = await this.markdownService.renderToHtml( + nextArticle.contentHtml = await this.markdownService.sanitizeHtml( body.content, ); } diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts index 37f35fb..4206d84 100644 --- a/test/app.e2e-spec.ts +++ b/test/app.e2e-spec.ts @@ -643,12 +643,16 @@ const routeTestCases: Record = { const response = await request(server) .post('/blog/article/update') .send({ + content: '
',
+        contentFormat: 'html',
         id: blogArticle.id,
         title: '本地文章',
       })
       .expect(200);
 
     expect(blogArticleServiceMock.update).toHaveBeenCalledWith({
+      content: '
',
+      contentFormat: 'html',
       id: blogArticle.id,
       title: '本地文章',
     });
diff --git a/test/blog/blog-article.service.spec.ts b/test/blog/blog-article.service.spec.ts
index f457a19..8e7901d 100644
--- a/test/blog/blog-article.service.spec.ts
+++ b/test/blog/blog-article.service.spec.ts
@@ -119,6 +119,38 @@ describe('BlogArticleService', () => {
     });
   });
 
+  it('sanitizes html content without markdown rerendering when updating imported articles', async () => {
+    const rawArgonCodeblock =
+      '
select 1;
'; + const currentArticle = { + contentHtml: '

旧正文

', + contentMarkdown: '旧正文', + id: '50', + isDeleted: false, + slug: 'wordpress-post', + status: 'publish', + title: 'WordPress 文章', + }; + repository.findOne.mockResolvedValue(currentArticle); + + await service.update({ + content: rawArgonCodeblock, + contentFormat: 'html', + id: '50', + title: 'WordPress 文章', + }); + + expect(markdownService.renderToHtml).not.toHaveBeenCalled(); + expect(markdownService.sanitizeHtml).toHaveBeenCalledWith(rawArgonCodeblock); + expect(repository.save).toHaveBeenCalledWith( + expect.objectContaining({ + contentHtml: + '
select 1;
', + contentMarkdown: '旧正文', + }), + ); + }); + it('queries public list with publish status and maps response fields', async () => { const builder = createQueryBuilderMock([ { diff --git a/test/blog/blog-menu-sql.spec.ts b/test/blog/blog-menu-sql.spec.ts index bfc3f62..2e82219 100644 --- a/test/blog/blog-menu-sql.spec.ts +++ b/test/blog/blog-menu-sql.spec.ts @@ -2,7 +2,20 @@ import { readFileSync } from 'node:fs'; import { join } from 'node:path'; describe('blog-menu.sql', () => { - const sql = readFileSync(join(process.cwd(), 'sql/blog-menu.sql'), 'utf8'); + const sqlFiles = [ + readFileSync(join(process.cwd(), 'sql/blog-menu.sql'), 'utf8'), + readFileSync(join(process.cwd(), 'sql/vben-admin-init.sql'), 'utf8'), + ]; + + /** + * 断言博客菜单种子和全量初始化 SQL 同步包含同一段菜单配置。 + * @param snippet - SQL 片段;同时约束增量菜单脚本和全量初始化脚本。 + */ + function expectAllSqlToContain(snippet: string) { + sqlFiles.forEach((sql) => { + expect(sql).toContain(snippet); + }); + } /** * 执行 博客内容流程。 @@ -17,7 +30,7 @@ describe('blog-menu.sql', () => { component: string, authCode: string, ) { - expect(sql).toContain( + expectAllSqlToContain( `'${name}', '${path}', '${component}', NULL, '${authCode}', 'menu'`, ); } @@ -30,18 +43,20 @@ describe('blog-menu.sql', () => { 'BlogArticleEdit', 'BlogArticleDelete', 'BlogArticleImport', + 'BlogArticlePreview', + 'BlogArticlePreviewButton', 'BlogCategory', 'BlogTag', 'BlogTheme', 'BlogThemeSave', 'BlogThemeImport', ].forEach((name) => { - expect(sql).toContain(`'${name}'`); + expectAllSqlToContain(`'${name}'`); }); }); it('keeps database menu routes aligned with admin route components', () => { - expect(sql).toContain( + expectAllSqlToContain( "'Blog', '/blog', NULL, '/blog/article', NULL, 'catalog'", ); expectMenuRoute( @@ -50,6 +65,14 @@ describe('blog-menu.sql', () => { '/blog/article/list', 'Blog:Article:List', ); + expectAllSqlToContain( + "'BlogArticlePreview', '/blog/article/:articleId/preview', '/blog/article/preview/index', NULL, 'Blog:Article:Preview', 'menu'", + ); + expectAllSqlToContain( + `'BlogArticlePreviewButton', NULL, NULL, NULL, 'Blog:Article:Preview', 'button'`, + ); + expectAllSqlToContain('"hideInMenu":true'); + expectAllSqlToContain('"activePath":"/blog/article"'); expectMenuRoute( 'BlogCategory', '/blog/category', @@ -66,9 +89,10 @@ describe('blog-menu.sql', () => { }); it('grants blog menus to admin roles without deleting existing role menus', () => { - expect(sql).toContain('INSERT IGNORE INTO `admin_role_menu`'); - expect(sql).toContain("menu.`name` LIKE 'Blog%'"); - expect(sql).toContain("role.`role_code` IN ('super', 'admin')"); - expect(sql).not.toMatch(/DELETE\s+FROM\s+`admin_role_menu`/i); + const blogMenuSql = sqlFiles[0] || ''; + expect(blogMenuSql).toContain('INSERT IGNORE INTO `admin_role_menu`'); + expect(blogMenuSql).toContain("menu.`name` LIKE 'Blog%'"); + expect(blogMenuSql).toContain("role.`role_code` IN ('super', 'admin')"); + expect(blogMenuSql).not.toMatch(/DELETE\s+FROM\s+`admin_role_menu`/i); }); });