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 } 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('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'))) } })