A calm place to write long-form, and publish it to the open social web.
skypress.blog/
1// src/lib/embeds/card.test.ts
2import { describe, expect, it } from 'vitest';
3import { escapeHtml, renderEmbedCard } from './card';
4
5describe( 'escapeHtml', () => {
6 it( 'escapes the HTML metacharacters', () => {
7 expect( escapeHtml( `<img src=x onerror="alert(1)"> & "q" 'a'` ) ).toBe(
8 '<img src=x onerror="alert(1)"> & "q" 'a''
9 );
10 } );
11} );
12
13describe( 'renderEmbedCard — atproto', () => {
14 const data = {
15 kind: 'atproto' as const,
16 authorName: 'Jeremy',
17 handle: 'jeremy.herve.bzh',
18 avatar: 'https://cdn.example/av.jpg',
19 text: 'Hello <script>alert(1)</script> world',
20 images: [ { src: 'https://cdn.example/i.jpg', alt: 'a "nice" pic' } ],
21 createdAt: '2026-06-19T10:00:00.000Z',
22 viewUrl: 'https://mu.social/profile/did:plc:x/post/abc',
23 };
24
25 it( 'renders a static card with escaped post text', () => {
26 const html = renderEmbedCard( data );
27 expect( html ).toContain( 'class="wp-block-embed skypress-embed skypress-embed--atproto"' );
28 expect( html ).toContain( 'Hello <script>alert(1)</script> world' );
29 expect( html ).not.toContain( '<script>' );
30 expect( html ).toContain( 'href="https://mu.social/profile/did:plc:x/post/abc"' );
31 expect( html ).toContain( '@jeremy.herve.bzh' );
32 } );
33
34 it( 'escapes hostile image alt text', () => {
35 expect( renderEmbedCard( data ) ).toContain( 'alt="a "nice" pic"' );
36 } );
37
38 it( 'contains no iframe', () => {
39 expect( renderEmbedCard( data ) ).not.toContain( '<iframe' );
40 } );
41} );
42
43describe( 'renderEmbedCard — video facade', () => {
44 const data = {
45 kind: 'youtube' as const,
46 id: 'dQw4w9WgXcQ',
47 title: 'Cool <b>video</b>',
48 thumbnail: 'https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg',
49 url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
50 };
51
52 it( 'renders a facade with a play button and data attributes, no iframe', () => {
53 const html = renderEmbedCard( data );
54 expect( html ).toContain( 'skypress-embed--video' );
55 expect( html ).toContain( 'data-embed-provider="youtube"' );
56 expect( html ).toContain( 'data-embed-id="dQw4w9WgXcQ"' );
57 expect( html ).toContain( '<button' );
58 expect( html ).toContain( 'Cool <b>video</b>' );
59 expect( html ).not.toContain( '<iframe' );
60 } );
61} );
62
63describe( 'renderEmbedCard — URL scheme guard', () => {
64 it( 'neutralizes a javascript: viewUrl on an atproto card', () => {
65 const html = renderEmbedCard( {
66 kind: 'atproto',
67 authorName: 'Jeremy',
68 handle: 'jeremy.herve.bzh',
69 text: 'hi',
70 images: [],
71 viewUrl: 'javascript:alert(1)',
72 } );
73 expect( html ).not.toContain( 'href="javascript:alert(1)"' );
74 expect( html ).not.toContain( 'javascript:' );
75 expect( html ).toContain( 'href=""' );
76 } );
77
78 it( 'neutralizes a javascript: url in the video fallback href', () => {
79 const html = renderEmbedCard( {
80 kind: 'youtube',
81 id: 'dQw4w9WgXcQ',
82 title: 'x',
83 thumbnail: 'https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg',
84 url: 'javascript:alert(1)',
85 } );
86 expect( html ).not.toContain( 'href="javascript:alert(1)"' );
87 expect( html ).not.toContain( 'javascript:' );
88 expect( html ).toContain( 'href=""' );
89 } );
90
91 it( 'leaves a valid https url untouched in the href', () => {
92 const html = renderEmbedCard( {
93 kind: 'atproto',
94 authorName: 'Jeremy',
95 handle: 'jeremy.herve.bzh',
96 text: 'hi',
97 images: [],
98 viewUrl: 'https://mu.social/profile/did:plc:x/post/abc',
99 } );
100 expect( html ).toContain( 'href="https://mu.social/profile/did:plc:x/post/abc"' );
101 } );
102} );