import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/svelte'; import TextBlock from './TextBlock.svelte'; import type { TextBlock as TextBlockType } from '$lib/types/content-blocks'; /** * Helper: create a link facet covering the byte range of `linkText` within `fullText`. */ function makeLinkFacet(fullText: string, linkText: string, uri: string) { const encoder = new TextEncoder(); const fullBytes = encoder.encode(fullText); const linkBytes = encoder.encode(linkText); // Find byte offset of linkText within fullText const fullStr = new TextDecoder().decode(fullBytes); const charStart = fullStr.indexOf(linkText); const byteStart = encoder.encode(fullStr.slice(0, charStart)).length; const byteEnd = byteStart + linkBytes.length; return { index: { byteStart, byteEnd }, features: [{ $type: 'app.bsky.richtext.facet#link', uri }] }; } describe('TextBlock XSS sanitisation', () => { describe('dangerous protocol URIs render as plain text', () => { it('does not create an anchor for javascript: URIs', () => { const text = 'click here now'; const block: TextBlockType = { $type: 'at.riven.block.text', text, facets: [makeLinkFacet(text, 'click here', 'javascript:alert(1)')] }; render(TextBlock, { props: { block } }); const el = screen.getByTestId('text-block'); // The text "click here" must appear but NOT inside an tag expect(el.innerHTML).toContain('click here'); expect(el.querySelector('a')).toBeNull(); }); it('does not create an anchor for data: URIs', () => { const text = 'click here now'; const block: TextBlockType = { $type: 'at.riven.block.text', text, facets: [ makeLinkFacet( text, 'click here', 'data:text/html,' ) ] }; render(TextBlock, { props: { block } }); const el = screen.getByTestId('text-block'); expect(el.innerHTML).toContain('click here'); expect(el.querySelector('a')).toBeNull(); }); it('does not create an anchor for vbscript: URIs', () => { const text = 'click here now'; const block: TextBlockType = { $type: 'at.riven.block.text', text, facets: [makeLinkFacet(text, 'click here', 'vbscript:msgbox')] }; render(TextBlock, { props: { block } }); const el = screen.getByTestId('text-block'); expect(el.innerHTML).toContain('click here'); expect(el.querySelector('a')).toBeNull(); }); it('does not create an anchor for mixed-case jAvAsCrIpT: URIs', () => { const text = 'click here now'; const block: TextBlockType = { $type: 'at.riven.block.text', text, facets: [makeLinkFacet(text, 'click here', 'JaVaScRiPt:alert(1)')] }; render(TextBlock, { props: { block } }); const el = screen.getByTestId('text-block'); expect(el.innerHTML).toContain('click here'); expect(el.querySelector('a')).toBeNull(); }); it('does not create an anchor for whitespace-prefixed javascript: URIs', () => { const text = 'click here now'; const block: TextBlockType = { $type: 'at.riven.block.text', text, facets: [makeLinkFacet(text, 'click here', ' javascript:alert(1)')] }; render(TextBlock, { props: { block } }); const el = screen.getByTestId('text-block'); expect(el.innerHTML).toContain('click here'); expect(el.querySelector('a')).toBeNull(); }); }); describe('valid http(s) URIs still render as links', () => { it('renders an anchor for https: URIs', () => { const text = 'click here now'; const block: TextBlockType = { $type: 'at.riven.block.text', text, facets: [makeLinkFacet(text, 'click here', 'https://example.com')] }; render(TextBlock, { props: { block } }); const el = screen.getByTestId('text-block'); const anchor = el.querySelector('a'); expect(anchor).not.toBeNull(); expect(anchor!.getAttribute('href')).toBe('https://example.com'); }); }); describe('escapeAttr escapes & in URIs', () => { it('escapes & characters in href attributes', () => { const text = 'click here now'; const uri = 'https://example.com?a=1&b=2'; const block: TextBlockType = { $type: 'at.riven.block.text', text, facets: [makeLinkFacet(text, 'click here', uri)] }; render(TextBlock, { props: { block } }); const el = screen.getByTestId('text-block'); // The raw innerHTML should have & not bare & // (bare & in an attribute is technically invalid HTML) expect(el.innerHTML).toContain('href="https://example.com?a=1&b=2"'); }); }); describe('escapeAttr escapes quotes in URIs', () => { it('escapes double-quote characters in href attributes', () => { const text = 'click here now'; const uri = 'https://example.com?a="bad"'; const block: TextBlockType = { $type: 'at.riven.block.text', text, facets: [makeLinkFacet(text, 'click here', uri)] }; render(TextBlock, { props: { block } }); const el = screen.getByTestId('text-block'); // The " in the URI must be escaped as " so it doesn't break out expect(el.innerHTML).toContain('"'); // The anchor should still be present and functional const anchor = el.querySelector('a'); expect(anchor).not.toBeNull(); }); }); });