补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
299 行
11 KiB
TypeScript
299 行
11 KiB
TypeScript
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('"');
|
||
expect(renderedHtml).toContain('<');
|
||
expect(renderedHtml).toContain('>');
|
||
expect(renderedHtml).toContain('&');
|
||
});
|
||
});
|
||
|
||
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);
|
||
});
|
||
});
|
||
});
|