Files
ArtPlayer/test/iframe-boundaries.test.js

191 lines
8.0 KiB
JavaScript

import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Real source compiled into an isolated window for ownership and protocol boundaries.
import test from 'node:test'
import { iframeCandidate, iframeEnvironment } from './helpers/iframe.js'
const implementation = await iframeCandidate()
const flush = () => new Promise(resolve => setImmediate(resolve))
function setup() {
const env = iframeEnvironment(implementation)
const frame = new env.Frame()
const instance = new env.Factory({ iframe: frame, url: '/first' })
return { ...env, frame, instance }
}
function observe(promise) {
const state = { status: 'pending' }
promise.then(value => Object.assign(state, { status: 'resolved', value }), error => Object.assign(state, { status: 'rejected', error: error.message }))
return state
}
test('Iframe source setter failure releases its bound listener and preserves the exact setup error', () => {
const env = iframeEnvironment(implementation)
const frame = new env.Frame()
const failure = new Error('src assignment rejected')
Object.defineProperty(frame, 'src', {
get() { return '/' },
set() {
throw failure
},
})
assert.throws(() => new env.Factory({ iframe: frame, url: '/' }), error => error === failure)
assert.equal(env.handlers.get('message').size, 0)
assert.equal(env.timers.size, 0)
})
test('Iframe partial listener acquisition failure releases ownership and keeps the first error', () => {
const env = iframeEnvironment(implementation)
const add = env.box.addEventListener
const failure = new Error('registration rejected after acquiring')
env.box.addEventListener = (...args) => {
add(...args)
throw failure
}
assert.throws(() => new env.Factory({ iframe: new env.Frame(), url: '/' }), error => error === failure)
assert.equal(env.handlers.get('message').size, 0)
assert.equal(env.order.includes('src'), false)
})
test('Iframe setup keeps the source error when releasing a partially constructed instance also throws', () => {
const env = iframeEnvironment(implementation)
const frame = new env.Frame()
const failure = new Error('original source error')
Object.defineProperty(frame, 'src', {
get() { return '/' },
set() {
throw failure
},
})
const remove = env.box.removeEventListener
let removals = 0
env.box.removeEventListener = (...args) => {
removals++
remove(...args)
throw new Error('secondary release error')
}
assert.throws(() => new env.Factory({ iframe: frame, url: '/' }), error => error === failure)
assert.equal(removals, 1)
assert.equal(env.handlers.get('message').size, 0)
})
test('Iframe destroy cancels requests even if listener release throws and does not repeat release', async () => {
const env = setup()
const state = observe(env.instance.postMessage({ type: 'waiting' }))
const failure = new Error('listener release failure')
const remove = env.box.removeEventListener
let removals = 0
env.box.removeEventListener = (...args) => {
removals++
remove(...args)
throw failure
}
assert.throws(() => env.instance.destroy(), error => error === failure)
await flush()
assert.deepEqual(state, { status: 'rejected', error: 'The instance has been destroyed' })
assert.equal(env.timers.size, 0)
assert.equal(env.handlers.get('message').size, 0)
assert.doesNotThrow(() => env.instance.destroy())
assert.equal(removals, 1)
})
for (const injected of [false, true]) {
test(`Iframe failed construction cancels a reentrant ${injected ? 'sent' : 'waiting'} request`, async () => {
const env = iframeEnvironment(implementation)
const frame = new env.Frame()
const failure = new Error('source setter failed after reentry')
const receive = env.Factory.prototype.onMessage
let state
let partial
env.Factory.prototype.onMessage = function (event) {
partial = this
receive.call(this, event)
state = observe(this.postMessage({ type: 'query' }))
}
Object.defineProperty(frame, 'src', {
get() { return '/' },
set() {
env.dispatch({ type: injected ? 'inject' : 'custom' }, frame.contentWindow)
throw failure
},
})
assert.throws(() => new env.Factory({ iframe: frame, url: '/' }), error => error === failure)
await flush()
assert.deepEqual(state, { status: 'rejected', error: 'The instance has been destroyed' })
assert.equal(partial.destroyed, true)
assert.equal(env.timers.size, 0)
assert.equal(Object.keys(partial.promises).length, 0)
assert.equal(env.handlers.get('message').size, 0)
})
}
test('Iframe parent rejects foreign handshakes, responses and callbacks but accepts its selected cross-origin peer', async () => {
const env = setup()
const seen = []
env.instance.message(packet => seen.push(packet.type))
env.dispatch({ type: 'inject' }, {})
assert.equal(env.instance.injected, false)
assert.deepEqual(seen, [])
env.dispatch({ type: 'inject' }, env.frame.contentWindow, 'https://redirected.example')
const state = observe(env.instance.postMessage({ type: 'query' }))
env.dispatch({ type: 'custom-result', data: 'forged', id: 1234 }, {})
await flush()
assert.deepEqual(state, { status: 'pending' })
assert.deepEqual(seen, ['inject'])
env.dispatch({ type: 'custom-result', data: 'real', id: 1234 }, env.frame.contentWindow, 'null')
await flush()
assert.deepEqual(state, { status: 'resolved', value: 'real' })
assert.deepEqual(seen, ['inject', 'custom-result'])
env.instance.destroy()
})
test('Iframe child rejects a foreign executable command and accepts only its actual parent peer', async () => {
const env = iframeEnvironment(implementation, true)
env.box.executions = 0
const data = { type: 'commit', data: 'window.executions++; return 7', id: 17 }
await env.Factory.onMessage({ data, source: {}, origin: 'https://sibling.example' })
assert.equal(env.box.executions, 0)
assert.equal(env.sent.length, 0)
await env.Factory.onMessage({ data, source: env.box.parent, origin: 'https://parent.example' })
assert.equal(env.box.executions, 1)
assert.equal(env.sent[0].packet.data, 7)
})
for (const data of [null, undefined, false, 3, 'message', {}, { type: null }, { type: 3 }]) {
test(`Iframe receivers ignore malformed payload ${JSON.stringify(data)} without callbacks or child rejection`, async () => {
const env = setup()
let calls = 0
env.instance.message(() => calls++)
assert.doesNotThrow(() => env.dispatch(data, env.frame.contentWindow))
assert.equal(calls, 0)
assert.equal(env.instance.injected, false)
const child = iframeEnvironment(implementation, true)
await assert.doesNotReject(child.Factory.onMessage({ data, source: child.box.parent }))
assert.equal(child.sent.length, 0)
env.instance.destroy()
})
}
test('Iframe public receivers keep source-less and null-source direct invocation as a local API', async () => {
const env = setup()
env.instance.onMessage({ data: { type: 'inject' } })
const promise = env.instance.postMessage({ type: 'query' })
env.instance.onMessage({ data: { type: 'custom', data: 9, id: 1234 }, source: null })
assert.equal(await promise, 9)
const child = iframeEnvironment(implementation, true)
await child.Factory.onMessage({ data: { type: 'commit', data: 'return 8', id: 9 }, source: null })
assert.equal(child.sent[0].packet.data, 8)
env.instance.destroy()
})
test('Iframe trusted protocol retains generic message types and child commit error serialization', async () => {
const env = setup()
const seen = []
env.instance.message(packet => seen.push({ ...packet }))
env.dispatch({ type: '', data: 'custom empty type' }, env.frame.contentWindow)
assert.deepEqual(seen, [{ type: '', data: 'custom empty type' }])
const child = iframeEnvironment(implementation, true)
await assert.rejects(child.Factory.onMessage({ data: { type: 'commit', data: 3, id: 7 }, source: child.box.parent }), error => error.name === 'TypeError')
assert.equal(child.sent[0].packet.type, 'error')
assert.equal(child.sent[0].packet.id, 7)
env.instance.destroy()
})