Files
vue-journal-layer/tests/embeds.test.ts
Jakub Zych 782aa4da7d test: pin embed expansion order and the editor policy mirror
- expansion runs last at the sinks, never in payload transforms
- only the exact placeholder serialization expands; stable across calls
- the Go policy negatives are stripped the same way in the mirror
2026-10-10 16:03:08 +02:00

176 lines
8.4 KiB
TypeScript

// Embed expansion (Phase 16.2, D-17). The Blog Editor stores the placeholder
// <figure data-embed="youtube" data-id="ID"></figure>; the sinks render
// expandEmbeds(sanitizeJournalHtml(html)), so the sanitizer runs first and the
// only iframe in the output is the one built from a validated placeholder.
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { EMBED_PLACEHOLDER, EMBED_PROVIDERS, expandEmbeds } from '../app/utils/embeds.ts'
import { sanitizeJournalHtml } from '../app/utils/htmlGate.ts'
const ID = 'dQw4w9WgXcQ'
const PLACEHOLDER = `<figure data-embed="youtube" data-id="${ID}"></figure>`
const IFRAME =
'<figure class="journal-embed journal-embed--youtube">' +
`<iframe src="https://www.youtube-nocookie.com/embed/${ID}" title="YouTube video" loading="lazy" ` +
'referrerpolicy="strict-origin-when-cross-origin" allow="encrypted-media; picture-in-picture; fullscreen" allowfullscreen></iframe>' +
'</figure>'
// The sink: sanitize, then expand last.
const sink = (html: string) => expandEmbeds(sanitizeJournalHtml(html))
// The payload transform sanitizes, and the sink sanitizes again before expanding.
const payloadThenSink = (html: string) => expandEmbeds(sanitizeJournalHtml(sanitizeJournalHtml(html)))
const iframeCount = (html: string) => (html.match(/<iframe/g) ?? []).length
test('the placeholder expands to the privacy-enhanced YouTube iframe', () => {
assert.equal(expandEmbeds(PLACEHOLDER), IFRAME)
assert.equal(expandEmbeds(`<p>a</p>${PLACEHOLDER}<p>b</p>`), `<p>a</p>${IFRAME}<p>b</p>`)
})
test('the provider table holds YouTube only, on youtube-nocookie.com', () => {
assert.deepEqual(Object.keys(EMBED_PROVIDERS), ['youtube'])
assert.equal(EMBED_PROVIDERS.youtube!.src(ID), `https://www.youtube-nocookie.com/embed/${ID}`)
assert.ok(EMBED_PLACEHOLDER.global)
})
test('malformed or unknown placeholders stay unchanged and inert', () => {
const inert = [
`<figure data-embed="youtube" data-id="${ID.slice(0, 10)}"></figure>`,
`<figure data-embed="youtube" data-id="${ID}Q"></figure>`,
`<figure data-embed="youtube" data-id="dQw4w9WgX/Q"></figure>`,
`<figure data-embed="vimeo" data-id="${ID}"></figure>`,
`<figure data-embed="constructor" data-id="${ID}"></figure>`,
`<figure data-embed="__proto__" data-id="${ID}"></figure>`,
`<figure data-embed="youtube" data-id="${ID}" class="x"></figure>`,
`<figure class="x" data-embed="youtube" data-id="${ID}"></figure>`,
`<figure data-id="${ID}" data-embed="youtube"></figure>`,
`<figure data-embed="youtube" data-id="${ID}"><figcaption>c</figcaption></figure>`,
`<figure data-embed='youtube' data-id='${ID}'></figure>`,
]
for (const html of inert) assert.equal(expandEmbeds(html), html, html)
})
test('empty input gives ""', () => {
for (const html of [null, undefined, '']) assert.equal(expandEmbeds(html), '')
})
test('the sink turns a stored placeholder into the iframe', () => {
assert.equal(sink(PLACEHOLDER), IFRAME)
assert.equal(sink(`<h2>Title</h2>\n${PLACEHOLDER}\n<p>After</p>`), `<h2>Title</h2>\n${IFRAME}\n<p>After</p>`)
})
test('a raw iframe in stored HTML is stripped; only the placeholder becomes an iframe', () => {
const stored =
'<p>Before</p>' +
'<iframe src="https://evil.test/"></iframe>' +
`<iframe src="https://www.youtube-nocookie.com/embed/${ID}"></iframe>` +
PLACEHOLDER +
'<p>After</p>'
for (const run of [sink, payloadThenSink]) {
const out = run(stored)
assert.equal(out, `<p>Before</p>${IFRAME}<p>After</p>`)
assert.equal(iframeCount(out), 1)
assert.doesNotMatch(out, /evil\.test/)
}
})
test('a placeholder smuggling an iframe or extra attributes expands only its validated parts', () => {
const stored = `<figure data-embed="youtube" data-id="${ID}" data-src="https://evil.test/" onload="alert(1)"><iframe src="https://evil.test/"></iframe></figure>`
assert.equal(sink(stored), IFRAME)
assert.equal(payloadThenSink(stored), IFRAME)
})
test('a malformed placeholder stays an empty figure after the sink', () => {
assert.equal(sink(`<figure data-embed="youtube" data-id="short"></figure>`), '<figure data-embed="youtube"></figure>')
assert.equal(sink(`<figure data-embed="vimeo" data-id="${ID}"></figure>`), `<figure data-id="${ID}"></figure>`)
assert.equal(iframeCount(sink(`<figure data-embed="vimeo" data-id="${ID}"></figure>`)), 0)
})
test('the payload-then-sink pipeline gives the same result as the sink', () => {
const stored = `<p>x</p>${PLACEHOLDER}<script>alert(1)</script><p>y</p>`
assert.equal(payloadThenSink(stored), sink(stored))
assert.equal(sink(stored), `<p>x</p>${IFRAME}<p>y</p>`)
})
test('two placeholders in one body both expand', () => {
const other = 'abcdefghijk'
const out = sink(`${PLACEHOLDER}<p>between</p><figure data-embed="youtube" data-id="${other}"></figure>`)
assert.equal(iframeCount(out), 2)
assert.equal(out, `${IFRAME}<p>between</p>${IFRAME.replace(ID, other)}`)
})
test('the editor corpus embed entries expand after sanitizing', () => {
const corpus = JSON.parse(
readFileSync(new URL('./fixtures/editor-corpus.json', import.meta.url), 'utf8'),
) as Array<{ name: string; html: string }>
const embed = corpus.find((e) => e.name === 'embed-youtube')
assert.ok(embed)
assert.equal(payloadThenSink(embed.html), IFRAME)
const combined = corpus.find((e) => e.name === 'combined')
assert.ok(combined)
const out = payloadThenSink(combined.html)
assert.ok(out.startsWith(IFRAME), out)
assert.equal(iframeCount(out), 1)
// Entries without a placeholder pass through the expansion unchanged.
for (const entry of corpus.filter((e) => !e.html.includes('data-embed'))) {
const clean = sanitizeJournalHtml(entry.html)
assert.equal(expandEmbeds(clean), clean, entry.name)
}
})
test('expansion is last: sanitizing the expanded output would strip the iframe', () => {
// Why the order matters: the sanitizer never keeps an iframe, so a sink that
// sanitized after expanding would lose every video.
const again = sanitizeJournalHtml(IFRAME)
assert.equal(iframeCount(again), 0)
assert.doesNotMatch(again, /youtube-nocookie/)
})
test('the sinks expand after sanitizing and the payload transforms never expand', () => {
const read = (path: string) => readFileSync(new URL(path, import.meta.url), 'utf8')
for (const sink of ['../app/components/Journal/PostBody.vue', '../app/components/Journal/CategoryHeader.vue']) {
const source = read(sink)
assert.match(source, /expandEmbeds\(sanitizeJournalHtml\(/, `${sink} must render expandEmbeds(sanitizeJournalHtml(...))`)
assert.equal((source.match(/expandEmbeds\(/g) ?? []).length, 1, `${sink} expands once`)
}
for (const transform of ['../app/utils/postView.ts', '../app/journal-pages/CategoryPage.vue']) {
assert.doesNotMatch(read(transform), /expandEmbeds/, `${transform} must not expand`)
}
})
test('only the exact serialization expands: case, spacing and entities stay inert', () => {
const inert = [
`<figure data-embed="YouTube" data-id="${ID}"></figure>`,
`<figure data-embed="youtube " data-id="${ID}"></figure>`,
`<figure data-embed="youtube" data-id="${ID}"></figure>`,
`<figure data-embed="youtube" data-id="${ID}" ></figure>`,
`<FIGURE data-embed="youtube" data-id="${ID}"></FIGURE>`,
`<figure data-embed="youtube" data-id="javascript:1"></figure>`,
`<figure data-embed="youtube" data-id="dQw4w9WgXc&#81;"></figure>`,
`<figure data-embed="youtube" data-id="${ID}">\n</figure>`,
`<figure data-embed="youtube" data-id=""></figure>`,
]
for (const html of inert) {
assert.equal(expandEmbeds(html), html, html)
assert.equal(iframeCount(expandEmbeds(html)), 0, html)
}
})
test('expansion is stable across calls (the global pattern keeps no state)', () => {
const body = `<p>a</p>${PLACEHOLDER}<p>b</p>${PLACEHOLDER}`
const first = expandEmbeds(body)
assert.equal(expandEmbeds(body), first)
assert.equal(EMBED_PLACEHOLDER.lastIndex, 0)
assert.equal(iframeCount(first), 2)
})
test('the iframe src is a fixed host plus the validated id, never stored text', () => {
const out = sink(`<figure data-embed="youtube" data-id="${ID}"></figure>`)
const src = /<iframe src="([^"]+)"/.exec(out)?.[1]
assert.equal(src, `https://www.youtube-nocookie.com/embed/${ID}`)
assert.match(out, /referrerpolicy="strict-origin-when-cross-origin"/)
assert.match(out, /loading="lazy"/)
assert.doesNotMatch(out, /allow="[^"]*(autoplay|camera|microphone|geolocation)/)
})