Files
Starlight_Lancher/scripts/axolotl/publish-skin-site.test.mjs
T

186 lines
6.3 KiB
JavaScript

import assert from 'node:assert/strict'
import { mkdtemp, writeFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import test from 'node:test'
import { spawnSync } from 'node:child_process'
import { publishSkinSite } from './publish-skin-site.mjs'
async function fixture(t) {
const directory = await mkdtemp(path.join(tmpdir(), 'skin-site-publish-'))
t.after(() => rm(directory, { recursive: true, force: true }))
const archivePath = path.join(directory, 'Starlight_Launcher_1.0.2-beta10_skin-site-update.zip')
const bytes = Buffer.from('a small archive split into several chunks')
await writeFile(archivePath, bytes)
return { archivePath, bytes }
}
function apiFixture(bytes, options = {}) {
const chunks = new Map()
const events = []
const release = { id: 17, version: '1.0.2-beta10', active: false }
let state = 'uploading'
let failedOnce = false
if (options.resume) chunks.set(0, bytes.subarray(0, 8))
const status = () => ({
id: 'session-1',
chunkSize: 8,
size: bytes.length,
received: [...chunks.values()].reduce((total, part) => total + part.length, 0),
receivedChunks: [...chunks.keys()],
uploadConcurrency: 3,
state,
...(state === 'ready' ? { release } : {}),
...(state === 'failed' ? { errorMessage: 'Invalid archive manifest' } : {}),
})
const reply = (payload) => Response.json({ success: true, payload })
return {
chunks,
events,
fetch: async (url, init) => {
assert.equal(init.headers.Authorization, 'Bearer test-token')
const route = new URL(url).pathname.replace('/starlight/', '')
events.push(route)
if (route === 'launcher/admin/uploads') {
assert.equal(JSON.parse(init.body).size, bytes.length)
assert.equal(JSON.parse(init.body).forceUpdate, false)
return reply({ ...status(), ...(options.invalidChunkSize ? { chunkSize: 0 } : {}) })
}
if (route.includes('/chunks/')) {
if (options.rejectChunks) return new Response('too large', { status: 413 })
if (options.retryChunk && !failedOnce) {
failedOnce = true
return new Response('temporary failure', { status: 503 })
}
const index = Number(route.split('/').at(-1))
const file = init.body.get('file')
assert.equal(file.name, 'chunk.bin')
chunks.set(index, Buffer.from(await file.arrayBuffer()))
return reply(status())
}
if (route.endsWith('/complete')) {
assert.deepEqual(
Buffer.concat([...chunks].sort(([a], [b]) => a - b).map(([, part]) => part)),
bytes,
)
state = 'processing'
return reply(status())
}
if (route.endsWith('/activate')) {
assert.equal(state, 'ready')
return options.activationFailure
? Response.json({ success: false, errorMessage: 'Activation denied' })
: reply({ ...release, active: true })
}
if (route === 'launcher/admin/uploads/session-1') {
if (state === 'processing' && !options.stuck)
state = options.processingFailure ? 'failed' : 'ready'
return reply({
...status(),
...(options.wrongVersion && state === 'ready'
? { release: { ...release, version: '1.0.2-beta9' } }
: {}),
})
}
throw new Error(`Unexpected route ${route}`)
},
}
}
function settings(archivePath, api) {
return {
archivePath,
tag: 'v1.0.2-beta10',
token: 'test-token',
baseUrl: 'https://skin.example',
fetchImpl: api.fetch,
pollIntervalMs: 1,
sleep: async () => {},
log: () => {},
}
}
test('uploads exact bounded chunks, retries temporary errors, then completes and activates', async (t) => {
const { archivePath, bytes } = await fixture(t)
const api = apiFixture(bytes, { retryChunk: true })
const result = await publishSkinSite(settings(archivePath, api))
assert.equal(result.active, true)
assert.equal(api.chunks.size, Math.ceil(bytes.length / 8))
assert.ok([...api.chunks.values()].every((part) => part.length <= 8))
assert.equal(api.events.at(-1), 'launcher/admin/17/activate')
})
for (const [name, options, error] of [
['HTTP upload rejection', { rejectChunks: true }, /413/],
['archive processing failure', { processingFailure: true }, /Invalid archive manifest/],
['application-level activation failure', { activationFailure: true }, /Activation denied/],
['invalid server chunk size', { invalidChunkSize: true }, /invalid upload status/],
['mismatched release version', { wrongVersion: true }, /version other than/],
]) {
test(`fails explicitly on ${name}`, async (t) => {
const { archivePath, bytes } = await fixture(t)
const api = apiFixture(bytes, options)
await assert.rejects(publishSkinSite(settings(archivePath, api)), error)
if (!options.activationFailure)
assert.ok(!api.events.some((event) => event.endsWith('/activate')))
})
}
test('fails instead of hanging when processing never finishes', async (t) => {
const { archivePath, bytes } = await fixture(t)
const api = apiFixture(bytes, { stuck: true })
let now = 0
await assert.rejects(
publishSkinSite({
...settings(archivePath, api),
timeoutMs: 10,
now: () => now,
sleep: async () => {
now += 5
},
}),
/timed out/i,
)
})
test('missing credentials fail before any network request', async (t) => {
const { archivePath } = await fixture(t)
await assert.rejects(
publishSkinSite({
...settings(archivePath, { fetch: () => assert.fail('Unexpected network request') }),
token: '',
}),
/SKIN_SITE_CI_UPLOAD_TOKEN/,
)
})
test('resumes server-acknowledged chunks without uploading them again', async (t) => {
const { archivePath, bytes } = await fixture(t)
const api = apiFixture(bytes, { resume: true })
await publishSkinSite(settings(archivePath, api))
assert.ok(!api.events.some((event) => event.endsWith('/chunks/0')))
})
test('CLI emits a GitHub error and exits nonzero when credentials are missing', () => {
const result = spawnSync(
process.execPath,
['scripts/axolotl/publish-skin-site.mjs', 'missing.zip', 'v1.0.2-beta10'],
{ encoding: 'utf8', env: { ...process.env, SKIN_SITE_TOKEN: '' } },
)
assert.equal(result.status, 1)
assert.match(result.stderr, /::error::.*SKIN_SITE_CI_UPLOAD_TOKEN/)
})
test('reports structured server errors when creating the upload session fails', async (t) => {
const { archivePath } = await fixture(t)
await assert.rejects(
publishSkinSite(
settings(archivePath, {
fetch: async () =>
Response.json({ errorMessage: 'Upload storage unavailable' }, { status: 500 }),
}),
),
/HTTP 500 - Upload storage unavailable/,
)
})