304 lines
13 KiB
JavaScript
304 lines
13 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { spawn } from 'node:child_process'
|
|
import { createServer } from 'node:http'
|
|
import { mkdtemp, symlink } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import test from 'node:test'
|
|
import { verifyDshWebReady } from '../../control-plane/hooks/dsh-web-ready.mjs'
|
|
|
|
async function fixture(routes) {
|
|
const requests = new Map()
|
|
const server = createServer((request, response) => {
|
|
requests.set(request.url, (requests.get(request.url) ?? 0) + 1)
|
|
const configured = routes.get(`${request.method} ${request.url}`) ?? routes.get(request.url)
|
|
const route = typeof configured === 'function' ? configured(requests.get(request.url)) : configured
|
|
response.writeHead(route?.status ?? 404, {
|
|
'content-type': route?.type ?? 'text/plain',
|
|
...route?.headers,
|
|
})
|
|
response.end(route?.body ?? '')
|
|
})
|
|
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
|
|
return { server, port: server.address().port, requests }
|
|
}
|
|
|
|
test('DSH web readiness requires every boot manifest Plugin bundle', async () => {
|
|
const manifest = JSON.stringify({ rev: 'one', entries: [
|
|
{ id: 'one', url: '/plugins/one/client.js?rev=one', rev: 'one' },
|
|
{ id: 'two', url: '/plugins/two/client.js?rev=two', rev: 'two' },
|
|
] })
|
|
const routes = new Map([
|
|
['/', { status: 200, type: 'text/html', body: `<script>globalThis["__DSH_BOOT__"] = ${manifest}</script>` }],
|
|
['/plugins/one/client.js?rev=one', { status: 200, type: 'text/javascript', body: 'one' }],
|
|
['/plugins/two/client.js?rev=two', { status: 404 }],
|
|
['POST /api/pluginInventory/list', { status: 200, type: 'application/json', body: JSON.stringify({
|
|
type: 'server-response',
|
|
rpcId: 'dsh-platform-readiness',
|
|
result: { ok: true, value: { entries: [{ moduleName: 'ready', enabled: true, fiberPhase: 'active' }] } },
|
|
}) }],
|
|
])
|
|
const context = await fixture(routes)
|
|
try {
|
|
await assert.rejects(verifyDshWebReady({ port: context.port, managedReady: async () => {} }), /returned HTTP 404/)
|
|
routes.set('/plugins/two/client.js?rev=two', { status: 200, type: 'text/javascript', body: 'two' })
|
|
await verifyDshWebReady({ port: context.port, managedReady: async () => {} })
|
|
assert.equal(context.requests.get('/'), 3)
|
|
assert.equal(context.requests.get('/plugins/one/client.js?rev=one'), 3)
|
|
assert.equal(context.requests.get('/plugins/two/client.js?rev=two'), 3)
|
|
assert.equal(context.requests.get('/api/pluginInventory/list'), 2)
|
|
} finally {
|
|
await new Promise(resolve => context.server.close(resolve))
|
|
}
|
|
})
|
|
|
|
test('DSH web readiness verifies 0.1.5 initial combo batches', async () => {
|
|
const entryUrl = '/plugins/??one/client.js&rev=entry-one'
|
|
const batchUrl = '/plugins/??one/client.js,two/client.js&rev=batch-one'
|
|
const manifest = JSON.stringify({
|
|
rev: 'graph-one',
|
|
entries: [{ id: 'one', url: entryUrl, rev: 'entry-one' }],
|
|
batches: [{ phase: 'application', url: batchUrl, rev: 'batch-one', entries: ['one', 'two'] }],
|
|
})
|
|
const routes = new Map([
|
|
['/', { status: 200, type: 'text/html', body: `<script>window.__DSH_BOOT__ = ${manifest}</script>` }],
|
|
[entryUrl, { status: 200, type: 'text/javascript', body: 'one' }],
|
|
[batchUrl, { status: 404 }],
|
|
['POST /api/pluginInventory/list', { status: 200, type: 'application/json', body: JSON.stringify({
|
|
type: 'server-response',
|
|
rpcId: 'dsh-platform-readiness',
|
|
result: { ok: true, value: { entries: [{ moduleName: 'ready', enabled: true, fiberPhase: 'active' }] } },
|
|
}) }],
|
|
])
|
|
const context = await fixture(routes)
|
|
try {
|
|
await assert.rejects(
|
|
verifyDshWebReady({ port: context.port, managedReady: async () => ({}) }),
|
|
/returned HTTP 404/,
|
|
)
|
|
routes.set(batchUrl, { status: 200, type: 'text/javascript', body: 'one and two' })
|
|
await verifyDshWebReady({ port: context.port, managedReady: async () => ({}) })
|
|
assert.equal(context.requests.get(entryUrl), 3)
|
|
assert.equal(context.requests.get(batchUrl), 3)
|
|
} finally {
|
|
await new Promise(resolve => context.server.close(resolve))
|
|
}
|
|
})
|
|
|
|
test('DSH web readiness rejects a malformed combo batch contract', async () => {
|
|
const manifest = JSON.stringify({
|
|
rev: 'graph-one',
|
|
entries: [{ id: 'one', url: '/plugins/one/client.js?rev=one', rev: 'one' }],
|
|
batches: { phase: 'application', url: '/plugins/??one/client.js&rev=one' },
|
|
})
|
|
const context = await fixture(new Map([
|
|
['/', { status: 200, type: 'text/html', body: `<script>window.__DSH_BOOT__ = ${manifest}</script>` }],
|
|
]))
|
|
try {
|
|
await assert.rejects(
|
|
verifyDshWebReady({ port: context.port, managedReady: async () => ({}) }),
|
|
/boot manifest batches are invalid/,
|
|
)
|
|
} finally {
|
|
await new Promise(resolve => context.server.close(resolve))
|
|
}
|
|
})
|
|
|
|
test('DSH web readiness exchanges the private launch token for an authenticated cookie', async () => {
|
|
const manifest = JSON.stringify({ rev: 'one', entries: [
|
|
{ id: 'one', url: '/plugins/one/client.js?rev=one', rev: 'one' },
|
|
] })
|
|
let expectedCookie = ''
|
|
const routes = new Map([
|
|
['/?token=fixture-token', {
|
|
status: 303,
|
|
headers: { location: '/', 'set-cookie': 'dsh-auth-fixture=session; Path=/; HttpOnly; SameSite=Strict' },
|
|
}],
|
|
])
|
|
const context = await fixture(routes)
|
|
context.server.removeAllListeners('request')
|
|
context.server.on('request', (request, response) => {
|
|
context.requests.set(request.url, (context.requests.get(request.url) ?? 0) + 1)
|
|
if (request.url === '/?token=fixture-token') {
|
|
response.writeHead(303, {
|
|
location: '/',
|
|
'set-cookie': 'dsh-auth-fixture=session; Path=/; HttpOnly; SameSite=Strict',
|
|
})
|
|
response.end()
|
|
return
|
|
}
|
|
expectedCookie = request.headers.cookie ?? ''
|
|
if (expectedCookie !== 'dsh-auth-fixture=session') {
|
|
response.writeHead(401)
|
|
response.end('unauthorized')
|
|
return
|
|
}
|
|
if (request.url === '/') {
|
|
response.writeHead(200, { 'content-type': 'text/html' })
|
|
response.end(`<script>window.__DSH_BOOT__ = ${manifest}</script>`)
|
|
} else if (request.url === '/plugins/one/client.js?rev=one') {
|
|
response.writeHead(200, { 'content-type': 'text/javascript' })
|
|
response.end('one')
|
|
} else if (request.url === '/api/pluginInventory/list') {
|
|
response.writeHead(200, { 'content-type': 'application/json' })
|
|
response.end(JSON.stringify({
|
|
type: 'server-response',
|
|
rpcId: 'dsh-platform-readiness',
|
|
result: { ok: true, value: { entries: [{ moduleName: 'ready', enabled: true, fiberPhase: 'active' }] } },
|
|
}))
|
|
} else {
|
|
response.writeHead(404)
|
|
response.end()
|
|
}
|
|
})
|
|
try {
|
|
await verifyDshWebReady({
|
|
port: context.port,
|
|
managedReady: async () => ({ ready: true, readyUrl: `http://127.0.0.1:${String(context.port)}/?token=fixture-token` }),
|
|
})
|
|
assert.equal(expectedCookie, 'dsh-auth-fixture=session')
|
|
assert.equal(context.requests.get('/?token=fixture-token'), 1)
|
|
} finally {
|
|
await new Promise(resolve => context.server.close(resolve))
|
|
}
|
|
})
|
|
|
|
test('DSH web readiness rejects enabled Plugins that are not active', async () => {
|
|
const manifest = JSON.stringify({ rev: 'one', entries: [] })
|
|
const routes = new Map([
|
|
['/', { status: 200, type: 'text/html', body: `<script>window.__DSH_BOOT__ = ${manifest}</script>` }],
|
|
['POST /api/pluginInventory/list', { status: 200, type: 'application/json', body: JSON.stringify({
|
|
type: 'server-response',
|
|
rpcId: 'dsh-platform-readiness',
|
|
result: { ok: true, value: { entries: [
|
|
{ moduleName: 'active', enabled: true, fiberPhase: 'active' },
|
|
{ moduleName: 'disabled', enabled: false, fiberPhase: null },
|
|
{ moduleName: 'broken', enabled: true, fiberPhase: 'failed' },
|
|
] } },
|
|
}) }],
|
|
])
|
|
const context = await fixture(routes)
|
|
try {
|
|
await assert.rejects(
|
|
verifyDshWebReady({ port: context.port, managedReady: async () => {} }),
|
|
/DSH Plugins are not active: broken \(failed\)/,
|
|
)
|
|
} finally {
|
|
await new Promise(resolve => context.server.close(resolve))
|
|
}
|
|
})
|
|
|
|
test('DSH web readiness rejects Plugin inventory RPC failures', async () => {
|
|
const manifest = JSON.stringify({ rev: 'one', entries: [] })
|
|
const routes = new Map([
|
|
['/', { status: 200, type: 'text/html', body: `<script>window.__DSH_BOOT__ = ${manifest}</script>` }],
|
|
['POST /api/pluginInventory/list', { status: 200, type: 'application/json', body: JSON.stringify({
|
|
type: 'server-response',
|
|
rpcId: 'dsh-platform-readiness',
|
|
result: { ok: false, error: { code: 'internal', message: 'loader unavailable', details: {} } },
|
|
}) }],
|
|
])
|
|
const context = await fixture(routes)
|
|
try {
|
|
await assert.rejects(
|
|
verifyDshWebReady({ port: context.port, managedReady: async () => {} }),
|
|
/DSH Plugin inventory is unavailable: loader unavailable/,
|
|
)
|
|
} finally {
|
|
await new Promise(resolve => context.server.close(resolve))
|
|
}
|
|
})
|
|
|
|
test('DSH web readiness rejects a lifecycle generation change during verification', async () => {
|
|
const manifest = JSON.stringify({ rev: 'one', entries: [] })
|
|
const routes = new Map([
|
|
['/', { status: 200, type: 'text/html', body: `<script>window.__DSH_BOOT__ = ${manifest}</script>` }],
|
|
['POST /api/pluginInventory/list', { status: 200, type: 'application/json', body: JSON.stringify({
|
|
type: 'server-response',
|
|
rpcId: 'dsh-platform-readiness',
|
|
result: { ok: true, value: { entries: [] } },
|
|
}) }],
|
|
])
|
|
const context = await fixture(routes)
|
|
let observation = 0
|
|
try {
|
|
await assert.rejects(verifyDshWebReady({
|
|
port: context.port,
|
|
managedReady: async () => ({ generation: ++observation === 1 ? 'one' : 'two', ready: true, readyUrl: null }),
|
|
}), /lifecycle changed during Web readiness verification/)
|
|
assert.equal(context.requests.get('/'), 1)
|
|
assert.equal(context.requests.get('/api/pluginInventory/list'), 1)
|
|
} finally {
|
|
await new Promise(resolve => context.server.close(resolve))
|
|
}
|
|
})
|
|
|
|
test('DSH web readiness rejects a boot manifest mutation between observations', async () => {
|
|
const manifest = rev => JSON.stringify({ rev, entries: [] })
|
|
const routes = new Map([
|
|
['/', observation => ({
|
|
status: 200,
|
|
type: 'text/html',
|
|
body: `<script>window.__DSH_BOOT__ = ${manifest(observation === 1 ? 'one' : 'two')}</script>`,
|
|
})],
|
|
['POST /api/pluginInventory/list', { status: 200, type: 'application/json', body: JSON.stringify({
|
|
type: 'server-response',
|
|
rpcId: 'dsh-platform-readiness',
|
|
result: { ok: true, value: { entries: [] } },
|
|
}) }],
|
|
])
|
|
const context = await fixture(routes)
|
|
try {
|
|
await assert.rejects(
|
|
verifyDshWebReady({ port: context.port, managedReady: async () => ({ generation: 'one' }) }),
|
|
/boot manifest changed during Web readiness verification/,
|
|
)
|
|
} finally {
|
|
await new Promise(resolve => context.server.close(resolve))
|
|
}
|
|
})
|
|
|
|
test('DSH web readiness rejects a Plugin inventory mutation between observations', async () => {
|
|
const manifest = JSON.stringify({ rev: 'one', entries: [] })
|
|
const inventory = moduleName => JSON.stringify({
|
|
type: 'server-response',
|
|
rpcId: 'dsh-platform-readiness',
|
|
result: { ok: true, value: { entries: [{ moduleName, enabled: true, fiberPhase: 'active' }] } },
|
|
})
|
|
const routes = new Map([
|
|
['/', { status: 200, type: 'text/html', body: `<script>window.__DSH_BOOT__ = ${manifest}</script>` }],
|
|
['POST /api/pluginInventory/list', observation => ({
|
|
status: 200,
|
|
type: 'application/json',
|
|
body: inventory(observation === 1 ? 'one' : 'two'),
|
|
})],
|
|
])
|
|
const context = await fixture(routes)
|
|
try {
|
|
await assert.rejects(
|
|
verifyDshWebReady({ port: context.port, managedReady: async () => ({ generation: 'one' }) }),
|
|
/Plugin inventory changed during Web readiness verification/,
|
|
)
|
|
} finally {
|
|
await new Promise(resolve => context.server.close(resolve))
|
|
}
|
|
})
|
|
|
|
test('executes readiness checks when launched through the Bootstrap view symlink', async () => {
|
|
const root = await mkdtemp(join(tmpdir(), 'dsh-web-ready-view-'))
|
|
const executable = join(root, 'dsh-web-ready.mjs')
|
|
await symlink(new URL('../../control-plane/hooks/dsh-web-ready.mjs', import.meta.url), executable)
|
|
const result = await new Promise((resolve, reject) => {
|
|
const child = spawn(process.execPath, [executable], {
|
|
env: { ...process.env, DSH_PLATFORM_RUN: join(root, 'missing-run') },
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
})
|
|
const stderr = []
|
|
child.stderr.on('data', chunk => stderr.push(chunk))
|
|
child.once('error', reject)
|
|
child.once('exit', code => resolve({ code, stderr: Buffer.concat(stderr).toString('utf8') }))
|
|
})
|
|
assert.notEqual(result.code, 0)
|
|
assert.match(result.stderr, /dsh-lifecycle\.sock/)
|
|
})
|