import { CardSeoService } from '../../../src/modules/card/service/seo'; import { CardPortalService } from '../../../src/modules/card/service/portal'; import { OpenCardPortalController, PortalRequestContext, } from '../../../src/modules/card/controller/open/portal'; import { CardPortalDetail } from '../../../src/comm/types/omni'; describe('Point 29-B: 社交爬虫探测与微官网短链服务端渲染集成 (Social Crawler & SEO SSR)', () => { let cardSeoService: CardSeoService; let cardPortalService: CardPortalService; let portalController: OpenCardPortalController; // 模拟真实名片聚合资产 const mockAlexCardDetail: CardPortalDetail = { id: 101, slug: 'alex-zheng', name: 'Alex Zheng', title: 'VP of Global Expansion', company: 'OmniCard Global Inc.', slogan: 'Empowering Global Business Connections', avatarUrl: 'https://cdn.omnicard.ai/avatars/alex.png', phone: '+86-13800138000', email: 'alex@omnicard.ai', whatsapp: '+8613800138000', linkedin: 'https://linkedin.com/in/alex-zheng', timezone: 'Asia/Shanghai', audioPronunciationUrl: 'https://cdn.omnicard.ai/audio/alex.mp3', themeCode: 'corporate_navy', status: 'published', bentoBlocks: [ { id: 1, cardId: 101, type: 'hero', colSpan: 2, rowSpan: 1, sortOrder: 1, title: 'About Alex', content: '10+ years in international business expansion.', }, ], cachedAt: Date.now(), }; // 常用社交爬虫与普通用户 UA 常量 const USER_AGENTS = { whatsapp: 'WhatsApp/2.21.12.21 A', whatsappIos: 'WhatsApp/2.23.20.0 i', linkedin: 'LinkedInBot/1.0 (compatible; Mozilla/5.0; Apache-HttpClient +http://www.linkedin.com)', twitter: 'Twitterbot/1.0', facebook: 'facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)', facebot: 'Facebot', telegram: 'TelegramBot (like TwitterBot)', slack: 'Slackbot-LinkExpanding 1.0 (+https://api.slack.com/robots)', discord: 'Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)', googlebot: 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)', safariMobile: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1', chromeDesktop: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', }; beforeEach(() => { cardSeoService = new CardSeoService(); cardPortalService = new CardPortalService(); interface CustomHttpError extends Error { status?: number; } // Mock cardPortalService.getPortalDetail jest .spyOn(cardPortalService, 'getPortalDetail') .mockImplementation(async (slugOrId: string | number) => { if (slugOrId === 'alex-zheng' || slugOrId === 101 || slugOrId === '101') { return { ...mockAlexCardDetail }; } const error = new Error('名片不存在或已下线') as CustomHttpError; error.status = 404; throw error; }); portalController = new OpenCardPortalController(); portalController.cardPortalService = cardPortalService; portalController.cardSeoService = cardSeoService; }); describe('1. 社交爬虫与机器人 User-Agent 精准探测识别', () => { it('应准确识别 WhatsApp 爬虫 User-Agent', () => { expect(cardSeoService.isCrawler(USER_AGENTS.whatsapp)).toBe(true); expect(cardSeoService.isCrawler(USER_AGENTS.whatsappIos)).toBe(true); expect(cardSeoService.detectCrawler(USER_AGENTS.whatsapp)).toBe('whatsapp'); }); it('应准确识别 LinkedInBot 爬虫 User-Agent', () => { expect(cardSeoService.isCrawler(USER_AGENTS.linkedin)).toBe(true); expect(cardSeoService.detectCrawler(USER_AGENTS.linkedin)).toBe('linkedin'); }); it('应准确识别 Twitterbot 爬虫 User-Agent', () => { expect(cardSeoService.isCrawler(USER_AGENTS.twitter)).toBe(true); expect(cardSeoService.detectCrawler(USER_AGENTS.twitter)).toBe('twitter'); }); it('应准确识别 facebookexternalhit 与 Facebot 爬虫 User-Agent', () => { expect(cardSeoService.isCrawler(USER_AGENTS.facebook)).toBe(true); expect(cardSeoService.isCrawler(USER_AGENTS.facebot)).toBe(true); expect(cardSeoService.detectCrawler(USER_AGENTS.facebook)).toBe('facebook'); }); it('应准确识别 TelegramBot 爬虫 User-Agent', () => { expect(cardSeoService.isCrawler(USER_AGENTS.telegram)).toBe(true); expect(cardSeoService.detectCrawler(USER_AGENTS.telegram)).toBe('telegram'); }); it('应支持识别 Slackbot、Discordbot 及主流搜索引擎机器人', () => { expect(cardSeoService.isCrawler(USER_AGENTS.slack)).toBe(true); expect(cardSeoService.isCrawler(USER_AGENTS.discord)).toBe(true); expect(cardSeoService.isCrawler(USER_AGENTS.googlebot)).toBe(true); }); it('常规移动端与桌面端浏览器不应被判定为爬虫', () => { expect(cardSeoService.isCrawler(USER_AGENTS.safariMobile)).toBe(false); expect(cardSeoService.isCrawler(USER_AGENTS.chromeDesktop)).toBe(false); expect(cardSeoService.detectCrawler(USER_AGENTS.chromeDesktop)).toBeNull(); }); it('对于空字符串或 undefined User-Agent 应安全降级返回 false', () => { expect(cardSeoService.isCrawler('')).toBe(false); expect(cardSeoService.isCrawler(undefined)).toBe(false); expect(cardSeoService.detectCrawler('')).toBeNull(); expect(cardSeoService.detectCrawler(undefined)).toBeNull(); }); }); describe('2. OpenGraph 元数据提炼与微官网 HTML 模板服务端渲染', () => { it('应正确从名片提取 og:title、og:image、og:description 等元数据', () => { const ogMeta = cardSeoService.generateOgMeta(mockAlexCardDetail); expect(ogMeta.title).toBe('Alex Zheng - VP of Global Expansion | OmniCard Global Inc.'); expect(ogMeta.image).toBe('https://cdn.omnicard.ai/avatars/alex.png'); expect(ogMeta.description).toBe('Empowering Global Business Connections'); expect(ogMeta.type).toBe('profile'); }); it('渲染输出的 HTML 必须包含完整的 OpenGraph、Twitter Cards 与 JSON-LD 标签', () => { const html = cardSeoService.renderCardHtml(mockAlexCardDetail); // HTML5 规范结构 expect(html).toContain(''); expect(html).toContain(''); expect(html).toContain(''); expect(html).toContain(''); expect(html).toContain(''); // OpenGraph 协议标准断言 expect(html).toContain(''); expect(html).toContain('"@type":"Person"'); expect(html).toContain('"name":"Alex Zheng"'); expect(html).toContain('"jobTitle":"VP of Global Expansion"'); // 包含名片主要信息 expect(html).toContain('Alex Zheng'); expect(html).toContain('VP of Global Expansion'); expect(html).toContain('OmniCard Global Inc.'); }); it('处理特殊字符时必须严格 HTML 实体转义以防范 XSS 注入', () => { const maliciousCard: CardPortalDetail = { ...mockAlexCardDetail, name: 'Evil ', company: 'Hack & Co "Global"', slogan: 'Test & more', }; const html = cardSeoService.renderCardHtml(maliciousCard); expect(html).not.toContain(''); expect(html).toContain('Evil <script>alert("xss")</script>'); expect(html).toContain('Hack & Co "Global"'); }); it('当名片推荐语言为阿拉伯语等 RTL 语言时,服务端首屏 HTML 必须注入 dir="rtl" 属性', () => { const arabicCard: CardPortalDetail = { ...mockAlexCardDetail, suggestedLocale: 'ar', }; const html = cardSeoService.renderCardHtml(arabicCard); expect(html).toContain(''); const uaeCard: CardPortalDetail = { ...mockAlexCardDetail, suggestedLocale: 'ar-AE', }; const uaeHtml = cardSeoService.renderCardHtml(uaeCard); expect(uaeHtml).toContain(''); }); it('生成 404 兜底 HTML 时应提示名片已下线或不存在', () => { const notFoundHtml = cardSeoService.renderNotFoundHtml('名片不存在或已下线'); expect(notFoundHtml).toContain(''); expect(notFoundHtml).toContain('名片不存在或已下线'); expect(notFoundHtml).toContain(' { const createMockCtx = (userAgent?: string): PortalRequestContext => { const headerMap: Record = {}; if (userAgent) { headerMap['user-agent'] = userAgent; } return { headers: headerMap, set: jest.fn((k: string, v: string) => { headerMap[k.toLowerCase()] = v; }), get: jest.fn((field: string) => headerMap[field.toLowerCase()]), status: 200, body: undefined, }; }; it('模拟 WhatsApp 爬虫访问 preview-html 路由,返回 200、text/html 及包含 og:title 和 og:image', async () => { const mockCtx = createMockCtx(USER_AGENTS.whatsapp); portalController.ctx = mockCtx; const html = await portalController.previewHtml('alex-zheng'); expect(typeof html).toBe('string'); expect(mockCtx.status).toBe(200); expect(mockCtx.set).toHaveBeenCalledWith('Content-Type', 'text/html; charset=utf-8'); expect(html).toContain('og:title'); expect(html).toContain('Alex Zheng - VP of Global Expansion | OmniCard Global Inc.'); expect(html).toContain('og:image'); expect(html).toContain('https://cdn.omnicard.ai/avatars/alex.png'); }); it('模拟 LinkedInBot 爬虫访问,断言返回 200 与 OpenGraph HTML', async () => { const mockCtx = createMockCtx(USER_AGENTS.linkedin); portalController.ctx = mockCtx; const html = await portalController.previewHtml('alex-zheng'); expect(mockCtx.status).toBe(200); expect(mockCtx.set).toHaveBeenCalledWith('Content-Type', 'text/html; charset=utf-8'); expect(html).toContain('og:title'); expect(html).toContain('og:image'); expect(html).toContain('Alex Zheng'); }); it('模拟 Twitterbot 爬虫访问,断言返回 200 与 twitter:card 元数据', async () => { const mockCtx = createMockCtx(USER_AGENTS.twitter); portalController.ctx = mockCtx; const html = await portalController.previewHtml('alex-zheng'); expect(mockCtx.status).toBe(200); expect(mockCtx.set).toHaveBeenCalledWith('Content-Type', 'text/html; charset=utf-8'); expect(html).toContain('twitter:card'); expect(html).toContain('twitter:image'); expect(html).toContain('https://cdn.omnicard.ai/avatars/alex.png'); }); it('模拟 facebookexternalhit 爬虫访问,断言返回 200', async () => { const mockCtx = createMockCtx(USER_AGENTS.facebook); portalController.ctx = mockCtx; const html = await portalController.previewHtml('alex-zheng'); expect(mockCtx.status).toBe(200); expect(mockCtx.set).toHaveBeenCalledWith('Content-Type', 'text/html; charset=utf-8'); expect(html).toContain('og:title'); }); it('模拟 TelegramBot 爬虫访问,断言返回 200', async () => { const mockCtx = createMockCtx(USER_AGENTS.telegram); portalController.ctx = mockCtx; const html = await portalController.previewHtml('alex-zheng'); expect(mockCtx.status).toBe(200); expect(mockCtx.set).toHaveBeenCalledWith('Content-Type', 'text/html; charset=utf-8'); expect(html).toContain('og:title'); expect(html).toContain('og:image'); }); it('普通桌面浏览器在 preview-html 路由下也能正常获取真实名片渲染预览', async () => { const mockCtx = createMockCtx(USER_AGENTS.chromeDesktop); portalController.ctx = mockCtx; const html = await portalController.previewHtml('alex-zheng'); expect(mockCtx.status).toBe(200); expect(mockCtx.set).toHaveBeenCalledWith('Content-Type', 'text/html; charset=utf-8'); expect(html).toContain('Alex Zheng'); expect(html).toContain('OmniCard Global Inc.'); }); it('访问不存在的名片 slug 时应返回 404 状态码及兜底 HTML', async () => { const mockCtx = createMockCtx(USER_AGENTS.whatsapp); portalController.ctx = mockCtx; const html = await portalController.previewHtml('non-existent-slug'); expect(mockCtx.status).toBe(404); expect(mockCtx.set).toHaveBeenCalledWith('Content-Type', 'text/html; charset=utf-8'); expect(html).toContain('名片不存在或已下线'); }); }); describe('4. 短链路由 GET /open/card/portal/:slugOrId 智能爬虫探测集成', () => { it('当社交爬虫请求普通短链路由时,自动响应 SEO 预览 HTML 而非 JSON', async () => { const headerMap: Record = { 'user-agent': USER_AGENTS.whatsapp, }; const mockCtx: PortalRequestContext = { headers: headerMap, set: jest.fn((k: string, v: string) => { headerMap[k.toLowerCase()] = v; }), get: jest.fn((field: string) => headerMap[field.toLowerCase()]), status: 200, }; portalController.ctx = mockCtx; const res = await portalController.getBySlugOrId('alex-zheng'); // 确认为 HTML 字符串响应,并设置了 Content-Type: text/html expect(typeof res).toBe('string'); expect(res).toContain('og:title'); expect(res).toContain('Alex Zheng'); expect(mockCtx.set).toHaveBeenCalledWith('Content-Type', 'text/html; charset=utf-8'); expect(mockCtx.status).toBe(200); }); it('普通移动端用户或 API 客户端访问短链路由时,保持标准 JSON API 响应不变', async () => { const headerMap: Record = { 'user-agent': USER_AGENTS.safariMobile, }; const mockCtx: PortalRequestContext = { headers: headerMap, set: jest.fn(), get: jest.fn((field: string) => headerMap[field.toLowerCase()]), status: 200, }; portalController.ctx = mockCtx; const res = await portalController.getBySlugOrId('alex-zheng'); interface ApiOkResponse { code: number; message: string; data: CardPortalDetail; } expect(typeof res).toBe('object'); const okRes = res as ApiOkResponse; expect(okRes.code).toBe(1000); expect(okRes.data.slug).toBe('alex-zheng'); }); }); });