A calm place to write long-form, and publish it to the open social web.
skypress.blog/
1import { describe, expect, it } from 'vitest';
2import { sanitizeArticleHtml } from './sanitize';
3
4describe( 'sanitizeArticleHtml', () => {
5 it( 'strips <script> and inline event handlers (untrusted PDS content)', () => {
6 const dirty = '<p>Hi</p><script>alert(1)</script><img src="x" onerror="alert(2)">';
7 const clean = sanitizeArticleHtml( dirty );
8 expect( clean ).toContain( '<p>Hi</p>' );
9 expect( clean ).not.toContain( '<script' );
10 expect( clean ).not.toContain( 'onerror' );
11 expect( clean ).not.toContain( 'alert(2)' );
12 } );
13
14 it( 'drops javascript: URLs but keeps safe links + images', () => {
15 const dirty =
16 '<a href="javascript:alert(1)">x</a><a href="https://ok.example">ok</a><img src="https://cdn.example/a.png" alt="a">';
17 const clean = sanitizeArticleHtml( dirty );
18 expect( clean ).not.toContain( 'javascript:' );
19 expect( clean ).toContain( 'href="https://ok.example"' );
20 expect( clean ).toContain( '<img' );
21 expect( clean ).toContain( 'src="https://cdn.example/a.png"' );
22 } );
23
24 it( 'preserves the curated blocks’ tags + classes', () => {
25 const html =
26 '<h2 class="wp-block-heading">T</h2><pre class="wp-block-code"><code>x</code></pre><blockquote class="wp-block-quote"><p>q</p></blockquote>';
27 const clean = sanitizeArticleHtml( html );
28 expect( clean ).toContain( '<h2 class="wp-block-heading">' );
29 expect( clean ).toContain( '<pre class="wp-block-code">' );
30 expect( clean ).toContain( '<blockquote class="wp-block-quote">' );
31 } );
32
33 it( 'hardens external links with rel + target', () => {
34 const clean = sanitizeArticleHtml( '<a href="https://x.example">x</a>' );
35 expect( clean ).toContain( 'rel="noopener noreferrer nofollow ugc"' );
36 expect( clean ).toContain( 'target="_blank"' );
37 } );
38
39 it( 'keeps a mention link (class + href) but strips its data-did', () => {
40 const dirty =
41 '<p>Hi <a class="skypress-mention" href="https://bsky.app/profile/alice.bsky.social" data-did="did:plc:alice">@alice.bsky.social</a></p>';
42 const clean = sanitizeArticleHtml( dirty );
43 expect( clean ).toContain( 'class="skypress-mention"' );
44 expect( clean ).toContain( 'href="https://bsky.app/profile/alice.bsky.social"' );
45 expect( clean ).not.toContain( 'data-did' );
46 expect( clean ).toContain( '@alice.bsky.social' );
47 } );
48} );
49
50describe( 'embed cards', () => {
51 it( 'keeps the atproto card structure', () => {
52 const html = sanitizeArticleHtml(
53 '<figure class="wp-block-embed skypress-embed skypress-embed--atproto"><a class="skypress-embed__link" href="https://mu.social/profile/x/post/y"><span class="skypress-embed__text">hi</span></a></figure>'
54 );
55 expect( html ).toContain( 'skypress-embed--atproto' );
56 expect( html ).toContain( 'href="https://mu.social/profile/x/post/y"' );
57 } );
58
59 it( 'keeps the video facade button + scoped data attributes', () => {
60 const html = sanitizeArticleHtml(
61 '<figure class="wp-block-embed skypress-embed skypress-embed--video"><button type="button" class="skypress-embed__play" data-embed-provider="youtube" data-embed-id="dQw4w9WgXcQ"><img class="skypress-embed__thumb" src="https://i/x.jpg" alt=""/></button></figure>'
62 );
63 expect( html ).toContain( '<button' );
64 expect( html ).toContain( 'data-embed-provider="youtube"' );
65 expect( html ).toContain( 'data-embed-id="dQw4w9WgXcQ"' );
66 } );
67
68 it( 'still strips a document-injected iframe', () => {
69 const html = sanitizeArticleHtml( '<p>x</p><iframe src="https://evil.com"></iframe>' );
70 expect( html ).not.toContain( '<iframe' );
71 } );
72
73 it( 'strips data attributes on non-button elements and onclick handlers', () => {
74 const html = sanitizeArticleHtml(
75 '<span data-embed-id="x" onclick="alert(1)">t</span>'
76 );
77 expect( html ).not.toContain( 'data-embed-id' );
78 expect( html ).not.toContain( 'onclick' );
79 } );
80} );