文件
OmniCard/backend/test/modules/card/seo.test.ts
wikiboss 62e0ae4435 feat: 同步管理端、用户端与后端业务,并接通线上 Web 接口
补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 18:01:47 +08:00

299 行
11 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

import { CardSeoService, SeoOptions } from '../../../src/modules/card/service/seo';
import { CardProfile } from '../../../src/comm/types/omni';
describe('Point 29-A: SEO & OpenGraph 社交预览元标签注入引擎', () => {
let seoService: CardSeoService;
beforeEach(() => {
seoService = new CardSeoService();
});
const mockCard: Partial<CardProfile> = {
id: 101,
name: 'Sarah Connor',
title: 'Chief Technology Officer',
company: 'Omni Global Tech',
slogan: 'Building the next generation of AI-driven digital identities.',
avatarUrl: 'https://cdn.omnicard.ai/avatars/sarah.jpg',
slug: 'sarah-connor',
};
describe('1. generateOpenGraphMeta 标签生成规范', () => {
it('应为完整名片生成标准化的 OpenGraph 与 Twitter Card 标签集合', () => {
const options: SeoOptions = {
baseUrl: 'https://omnicard.ai',
locale: 'en_US',
siteName: 'OmniCard AI',
};
const meta = seoService.generateOpenGraphMeta(mockCard, options);
// OpenGraph 基础标准规范
expect(meta['og:title']).toBe('Sarah Connor - Chief Technology Officer | Omni Global Tech');
expect(meta['og:description']).toBe(
'Building the next generation of AI-driven digital identities.'
);
expect(meta['og:image']).toBe('https://cdn.omnicard.ai/avatars/sarah.jpg');
expect(meta['og:url']).toBe('https://omnicard.ai/c/sarah-connor');
expect(meta['og:type']).toBe('profile');
expect(meta['og:locale']).toBe('en_US');
expect(meta['og:site_name']).toBe('OmniCard AI');
// Twitter Card 协议
expect(meta['twitter:card']).toBe('summary_large_image');
expect(meta['twitter:title']).toBe('Sarah Connor - Chief Technology Officer | Omni Global Tech');
expect(meta['twitter:description']).toBe(
'Building the next generation of AI-driven digital identities.'
);
expect(meta['twitter:image']).toBe('https://cdn.omnicard.ai/avatars/sarah.jpg');
});
it('当提供 currentUrl 时,og:url 应优先采用 currentUrl', () => {
const meta = seoService.generateOpenGraphMeta(mockCard, {
currentUrl: 'https://custom.domain.com/profile/sarah',
});
expect(meta['og:url']).toBe('https://custom.domain.com/profile/sarah');
});
it('当缺少 slogan 时,og:description 应优雅拼接 title 与 company', () => {
const cardWithoutSlogan: Partial<CardProfile> = {
name: 'Alex Rivera',
title: 'Global Sales Director',
company: 'Nexa Global',
};
const meta = seoService.generateOpenGraphMeta(cardWithoutSlogan);
expect(meta['og:description']).toBe('Global Sales Director @ Nexa Global');
expect(meta['og:title']).toBe('Alex Rivera - Global Sales Director | Nexa Global');
});
});
describe('2. 多语言规范与 OpenGraph Locale 格式化', () => {
it('应将各类格式的多语言编码格式化为 RFC/OpenGraph 标准 ll_CC 格式', () => {
expect(seoService.formatOgLocale('zh-CN')).toBe('zh_CN');
expect(seoService.formatOgLocale('en-US')).toBe('en_US');
expect(seoService.formatOgLocale('ar-SA')).toBe('ar_SA');
expect(seoService.formatOgLocale('ja')).toBe('ja_JP');
expect(seoService.formatOgLocale('en')).toBe('en_US');
expect(seoService.formatOgLocale('zh')).toBe('zh_CN');
expect(seoService.formatOgLocale('ar')).toBe('ar_SA');
expect(seoService.formatOgLocale(undefined)).toBe('en_US');
});
});
describe('3. renderHtmlWithOgMeta 模板注入与 RTL 支持', () => {
const template = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>OmniCard</title>
</head>
<body>
<div id="app"></div>
</body>
</html>`;
it('应将 meta 标签与 title 安全注入到 <head> 中', () => {
const html = seoService.renderHtmlWithOgMeta(template, mockCard, {
baseUrl: 'https://omnicard.ai',
locale: 'en_US',
});
expect(html).toContain('<meta property="og:title" content="Sarah Connor - Chief Technology Officer | Omni Global Tech" />');
expect(html).toContain('<meta property="og:type" content="profile" />');
expect(html).toContain('<meta property="og:image" content="https://cdn.omnicard.ai/avatars/sarah.jpg" />');
expect(html).toContain('<meta name="twitter:card" content="summary_large_image" />');
expect(html).toContain('<title>Sarah Connor - Chief Technology Officer | Omni Global Tech</title>');
expect(html).toContain('<html lang="en">');
});
it('当语言为阿拉伯语时,应自动为 <html> 标签注入 dir="rtl" 与 lang="ar"', () => {
const arCard: Partial<CardProfile> = {
name: 'طارق المنصور',
title: 'مدير تطوير الأعمال',
company: 'التقنية العالمية',
};
const html = seoService.renderHtmlWithOgMeta(template, arCard, {
locale: 'ar-SA',
});
expect(html).toContain('dir="rtl"');
expect(html).toContain('lang="ar"');
expect(html).toContain('<meta property="og:locale" content="ar_SA" />');
expect(html).toContain('طارق المنصور');
});
it('当显式传入 options.isRtl = true 时,也应注入 dir="rtl"', () => {
const html = seoService.renderHtmlWithOgMeta(template, mockCard, {
isRtl: true,
});
expect(html).toContain('dir="rtl"');
});
});
describe('4. 防 XSS 注入与特殊字符安全转义', () => {
it('当姓名、职位或描述包含恶意 HTML/JS 脚本或注入符号时,应进行严格转义', () => {
const xssCard: Partial<CardProfile> = {
name: '<script>alert("xss")</script>',
title: '"><img src=x onerror=alert(1)>',
company: 'Tech & "Solutions" <LLC>',
slogan: '\'OR 1=1 -- <iframe src="evil.com">',
};
const meta = seoService.generateOpenGraphMeta(xssCard);
expect(meta['og:title']).not.toContain('<script>');
expect(meta['og:title']).not.toContain('onerror=');
const template = '<html><head></head><body></body></html>';
const renderedHtml = seoService.renderHtmlWithOgMeta(template, xssCard);
// 验证特殊字符均被 escape,属性双引号不可跳脱
expect(renderedHtml).not.toContain('<script>alert');
expect(renderedHtml).not.toContain('<img src=x');
expect(renderedHtml).not.toContain('<iframe');
expect(renderedHtml).toContain('&quot;');
expect(renderedHtml).toContain('&lt;');
expect(renderedHtml).toContain('&gt;');
expect(renderedHtml).toContain('&amp;');
});
});
describe('5. 空值与边界安全兜底', () => {
it('当 card 为 null 或 undefined 时,应使用默认值兜底且绝不报错', () => {
const metaNull = seoService.generateOpenGraphMeta(null);
expect(metaNull['og:title']).toBe('OmniCard AI');
expect(metaNull['og:description']).toBe(
'OmniCard AI - Global Digital Business Card & Micro Portal'
);
// 无真实头像/封面时不得填假 CDN / 占位图
expect(metaNull['og:image']).toBe('');
expect(metaNull['twitter:image']).toBe('');
expect(metaNull['og:type']).toBe('profile');
expect(metaNull['twitter:card']).toBe('summary_large_image');
const metaUndefined = seoService.generateOpenGraphMeta(undefined, {
siteName: 'Custom Brand',
});
expect(metaUndefined['og:title']).toBe('Custom Brand');
expect(metaUndefined['og:image']).toBe('');
expect(metaUndefined['twitter:image']).toBe('');
});
it('当 template 不含标准 <head> 标签时,renderHtmlWithOgMeta 能够安全兜底注入', () => {
const rawHtml = '<div>Raw Body Content</div>';
const result = seoService.renderHtmlWithOgMeta(rawHtml, mockCard);
expect(result).toContain('<meta property="og:title"');
expect(result).toContain('<div>Raw Body Content</div>');
});
});
describe('6. 无真实图片时 og:image / twitter:image 必须为空(禁止假图)', () => {
it('generateOgMeta:无 avatarUrl 时 image 为空,不得回退到 default-card-og / og-default', () => {
const og = seoService.generateOgMeta(
{
id: 1,
name: 'No Avatar',
title: 'Engineer',
company: 'Omni',
status: 'active' as any,
bentoBlocks: [],
},
'https://omnicard.ai'
);
expect(og.image).toBe('');
expect(og.image).not.toMatch(/og-default|default-card-og|placeholder|example\.com|unsplash|picsum/i);
});
it('generateOgMeta:仅接受真实 http(s) 头像,相对路径与非 http 协议视为无图', () => {
const relative = seoService.generateOgMeta({
id: 2,
name: 'Rel',
title: 'T',
company: 'C',
avatarUrl: '/static/avatar.png',
status: 'active' as any,
bentoBlocks: [],
});
expect(relative.image).toBe('');
const blank = seoService.generateOgMeta({
id: 2,
name: 'Blank',
title: 'T',
company: 'C',
avatarUrl: ' ',
status: 'active' as any,
bentoBlocks: [],
});
expect(blank.image).toBe('');
const dataUri = seoService.generateOgMeta({
id: 2,
name: 'Data',
title: 'T',
company: 'C',
avatarUrl: 'data:image/png;base64,AAAA',
status: 'active' as any,
bentoBlocks: [],
});
expect(dataUri.image).toBe('');
const real = seoService.generateOgMeta({
id: 3,
name: 'Https',
title: 'T',
company: 'C',
avatarUrl: 'https://cdn.omnicard.ai/avatars/real.jpg',
status: 'active' as any,
bentoBlocks: [],
});
expect(real.image).toBe('https://cdn.omnicard.ai/avatars/real.jpg');
});
it('generateOpenGraphMeta:无头像时 og:image / twitter:image 为空,忽略假 defaultImage 占位', () => {
const meta = seoService.generateOpenGraphMeta(
{ name: 'Empty Pic', title: 'CTO', company: 'X' },
{
baseUrl: 'https://omnicard.ai',
defaultImage: 'https://omnicard.ai/assets/og-default.png',
}
);
expect(meta['og:image']).toBe('');
expect(meta['twitter:image']).toBe('');
expect(JSON.stringify(meta)).not.toMatch(
/og-default|default-card-og|placeholder|picsum|unsplash/i
);
});
it('renderHtmlWithOgMeta:无真实图片时不输出 og:image / twitter:image 标签', () => {
const template = `<!DOCTYPE html><html><head><title>t</title></head><body></body></html>`;
const html = seoService.renderHtmlWithOgMeta(
template,
{ name: 'No Image Card' },
{ baseUrl: 'https://omnicard.ai' }
);
expect(html).not.toContain('property="og:image"');
expect(html).not.toContain('name="twitter:image"');
expect(html).not.toMatch(/og-default|default-card-og/i);
});
it('renderCardHtml:无头像时 og:image / twitter:image content 为空', () => {
const html = seoService.renderCardHtml(
{
id: 9,
name: 'Blank',
title: 'Dev',
company: 'ACME',
status: 'active' as any,
bentoBlocks: [],
},
{ baseUrl: 'https://omnicard.ai' }
);
expect(html).toContain('<meta property="og:image" content="" />');
expect(html).toContain('<meta name="twitter:image" content="" />');
expect(html).not.toMatch(/og-default|default-card-og|placeholder\.com|example\.com\/.*\.(png|jpe?g)/i);
});
});
});