import test from 'node:test' import assert from 'node:assert/strict' import { mkdtemp, writeFile, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { stringify } from 'yaml' import { createBuildConfig, validateFeedUrl, APP_ID } from '../build/config.mjs' import { digest, writeReleaseManifest, verifyRelease } from '../scripts/releaseArtifacts.mjs' import { uploadRelease, UPLOAD_CHUNK_SIZE } from '../scripts/uploadRelease.mjs' const feed = 'https://downloads.thetempusproject.com/capsule/windows/x64/' const env = { CAPSULE_UPDATE_URL: feed, CAPSULE_PUBLISHER_NAME: 'Test Publisher', CSC_NAME: 'Test Publisher' } test('release builds require explicit HTTPS hosting and signing identity', () => { assert.throws(() => createBuildConfig({ release: true, env: {} }), /HTTPS/) assert.throws(() => createBuildConfig({ release: true, env: { CAPSULE_UPDATE_URL: feed } }), /PUBLISHER/) assert.throws(() => createBuildConfig({ release: true, env: { ...env, CSC_NAME: '' } }), /CSC_LINK/) for (const url of ['http://host/path/', 'https://u:p@host/path/', 'https://host/path/?token=secret', 'https://host/path/#x', 'https://localhost/', 'https://example.com/', 'https://host/path']) { assert.throws(() => validateFeedUrl(url)) } const config = createBuildConfig({ release: true, env }) assert.equal(config.appId, APP_ID) assert.equal(config.forceCodeSigning, true) assert.equal(config.win.verifyUpdateCodeSignature, true) assert.equal(config.win.signtoolOptions.certificateSubjectName, env.CSC_NAME) assert.equal(config.publish[0].url, feed) assert.equal(config.extraMetadata.capsuleUpdates.enabled, true) }) test('local installer has separate identity, no feed, and cannot wipe app data', () => { const config = createBuildConfig({ env: {} }) assert.notEqual(config.appId, APP_ID) assert.equal(config.extraMetadata.name, 'capsule-local') assert.equal(config.extraMetadata.capsuleUpdates.enabled, false) assert.equal(config.publish, null) assert.equal(config.nsis.perMachine, false) assert.equal(config.nsis.deleteAppDataOnUninstall, false) }) test('unsigned updates require explicit isolated test mode and retain the Local install identity', () => { const testEnv = { CAPSULE_TEST_UPDATE_URL: 'https://ttp.joeykimsey.com/capsule/testfeed/' } assert.throws(() => createBuildConfig({ testUpdates: true, env: {} }), /HTTPS/) assert.throws(() => createBuildConfig({ release: true, testUpdates: true, env: testEnv }), /combined/) assert.throws(() => createBuildConfig({ bootstrap: true }), /Bootstrap/) assert.throws(() => createBuildConfig({ testUpdates: true, env: { CAPSULE_TEST_UPDATE_URL: feed } }), /separate/) const local = createBuildConfig({ env: {} }) const testBuild = createBuildConfig({ testUpdates: true, env: testEnv }) assert.equal(testBuild.appId, local.appId) assert.equal(testBuild.extraMetadata.name, local.extraMetadata.name) assert.equal(testBuild.extraMetadata.capsuleUpdates.enabled, true) assert.equal(testBuild.extraMetadata.capsuleUpdates.testing, true) assert.equal(testBuild.win.verifyUpdateCodeSignature, false) assert.equal(testBuild.forceCodeSigning, false) assert.equal(testBuild.publish[0].url, testEnv.CAPSULE_TEST_UPDATE_URL) assert.equal(testBuild.nsis.deleteAppDataOnUninstall, false) const bootstrap = createBuildConfig({ testUpdates: true, bootstrap: true, env: testEnv }) assert.equal(bootstrap.extraMetadata.version, '0.1.0') assert.notEqual(bootstrap.directories.output, testBuild.directories.output) assert.equal(createBuildConfig({ release: true, env }).win.verifyUpdateCodeSignature, true) }) test('release verification detects tampering and publishes metadata last', async (t) => { const directory = await mkdtemp(join(tmpdir(), 'capsule-release-test-')) t.after(() => rm(directory, { recursive: true, force: true })) const binary = Buffer.from('synthetic installer fixture') const name = 'Capsule-0.1.1-x64-Setup.exe' const metadata = { version: '0.1.1', files: [{ url: name, sha512: digest(binary, 'sha512', 'base64'), size: binary.length }] } await writeFile(join(directory, name), binary) await writeFile(join(directory, `${name}.blockmap`), 'synthetic blockmap') await writeFile(join(directory, 'latest.yml'), stringify(metadata)) await assert.rejects(verifyRelease(directory), /ENOENT/) await writeReleaseManifest(directory, feed) const verified = await verifyRelease(directory) assert.deepEqual(verified.files.map((file) => file.name), [name, `${name}.blockmap`, 'latest.yml']) await writeFile(join(directory, `${name}.blockmap`), 'tampered') await assert.rejects(verifyRelease(directory), /changed/) await writeFile(join(directory, name), 'wrong installer') await assert.rejects(verifyRelease(directory), /checksum/) metadata.files[0].url = '../../outside.exe' await writeFile(join(directory, 'latest.yml'), stringify(metadata)) await assert.rejects(verifyRelease(directory), /filename/) }) test('upload verifies public assets before announcing release and keeps credentials private', async () => { const files = ['Capsule-0.1.1-x64-Setup.exe', 'Capsule-0.1.1-x64-Setup.exe.blockmap', 'latest.yml'] .map((name) => ({ name, data: Buffer.from(name) })) const calls = [] await uploadRelease({ feedUrl: feed, files }, { uploadUrl: 'https://upload.thetempusproject.com/capsule/', token: 'test-token', log: () => {}, fetchImpl: async (url, options) => { const name = url.pathname.split('/').pop() calls.push(`${options.method || 'GET'} ${name}`) assert.equal(options.redirect, 'error') if (options.method === 'PUT') { assert.equal(options.headers.Authorization, 'Bearer test-token') assert.equal(options.headers['If-None-Match'], name === 'latest.yml' ? undefined : '*') return { ok: true, status: 201 } } assert.equal(options.headers, undefined) return { ok: true, arrayBuffer: async () => files.find((file) => file.name === name).data } } }) assert.deepEqual(calls, files.flatMap(({ name }) => [`PUT ${name}`, `GET ${name}`])) }) test('upload stops before metadata on failed upload or mismatched public artifact', async () => { const release = { feedUrl: feed, files: [ { name: 'Capsule-0.1.1-x64-Setup.exe', data: Buffer.from('expected') }, { name: 'latest.yml', data: Buffer.from('metadata') } ] } for (const failUpload of [true, false]) { const calls = [] await assert.rejects(uploadRelease(release, { uploadUrl: 'https://upload.thetempusproject.com/capsule/', token: 'test-token', log: () => {}, fetchImpl: async (url, options) => { calls.push(url.pathname) return options.method === 'PUT' ? { ok: !failUpload, status: failUpload ? 500 : 201 } : { ok: true, arrayBuffer: async () => Buffer.from('wrong') } } }), /failed/) assert.ok(calls.every((path) => !path.endsWith('latest.yml'))) } }) test('large artifacts use contiguous bounded chunks and verify the assembled public file', async () => { const data = Buffer.alloc(UPLOAD_CHUNK_SIZE * 2 + 17, 42) const requests = [] await uploadRelease({ feedUrl: feed, files: [{ name: 'Capsule-0.1.1-x64-Setup.exe', data }] }, { uploadUrl: 'https://upload.thetempusproject.com/capsule/upload/', token: 'test-token', log: () => {}, fetchImpl: async (_url, options) => { if (options.method !== 'PUT') return { ok: true, arrayBuffer: async () => data } requests.push(options) return { ok: true, status: requests.length < 3 ? 202 : 201 } } }) assert.equal(requests.length, 3) assert.deepEqual(requests.map((request) => request.headers['Content-Range']), [ `bytes 0-524287/${data.length}`, `bytes 524288-1048575/${data.length}`, `bytes 1048576-1048592/${data.length}` ]) assert.equal(new Set(requests.map((request) => request.headers['X-Capsule-Upload-Id'])).size, 1) assert.deepEqual(Buffer.concat(requests.map((request) => request.body)), data) })