import assert from 'node:assert/strict'; import { Buffer } from 'node:buffer'; import { createHash } from 'node:crypto'; import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import test from 'node:test'; import { publishMacos, storageConfiguration } from '../publish-macos.mjs'; function fixture(t) { const directory = mkdtempSync(join(tmpdir(), 'kanbangit-upload-test-')); t.after(() => rmSync(directory, { recursive: true, force: true })); const metadata = { version: '1.2.3', target: 'aarch64-apple-darwin', filename: 'KanbanGit-1.2.3-aarch64-apple-darwin.zip', }; const archive = Buffer.from('verified release'); const checksum = createHash('sha256').update(archive).digest('hex'); writeFileSync(join(directory, metadata.filename), archive); writeFileSync( join(directory, `${metadata.filename}.sha256`), `${checksum} ${metadata.filename}\n`, ); const env = { S3_BUCKET: 'release-bucket', S3_REGION: 'nbg1', AWS_ACCESS_KEY_ID: 'test-key', AWS_SECRET_ACCESS_KEY: 'test-secret', }; const objects = new Map(); const events = []; const run = (command, args) => { assert.equal(command, 'aws'); assert.equal(args[1], 'https://nbg1.your-objectstorage.com'); const operation = args[args.indexOf('s3api') + 1]; const value = (name) => args[args.indexOf(name) + 1]; const key = value('--key'); events.push({ operation, key }); if (operation === 'get-bucket-versioning') { assert.equal(value('--output'), 'json'); return ''; } if (operation === 'put-object') { assert.equal(value('--if-none-match'), '*'); if (objects.has(key)) { throw new Error( 'aws failed: (PreconditionFailed) object exists', ); } objects.set(key, readFileSync(value('--body'))); return '{}'; } if (operation === 'get-object') { writeFileSync(args.at(-1), objects.get(key)); return '{}'; } throw new Error(`Unexpected S3 operation: ${operation}`); }; const fetchPublic = async (url, options) => { assert.equal(options.redirect, 'error'); assert.equal( new URL(url).hostname, 'release-bucket.nbg1.your-objectstorage.com', ); const key = new URL(url).pathname.slice(1); events.push({ operation: 'public-get', key }); return { ok: objects.has(key), arrayBuffer: async () => objects.get(key), }; }; return { directory, metadata, env, objects, events, run, fetchPublic }; } test('uploads immutable files and writes completion record after public verification', async (t) => { const f = fixture(t); const url = await publishMacos(f); assert.equal( url, `https://release-bucket.nbg1.your-objectstorage.com/releases/macos/aarch64-apple-darwin/1.2.3/${f.metadata.filename}`, ); assert.deepEqual( f.events.map((event) => event.operation), [ 'get-bucket-versioning', 'put-object', 'public-get', 'put-object', 'public-get', 'put-object', 'public-get', ], ); assert.equal(f.objects.size, 3); assert(f.events.at(-1).key.endsWith('/release.json')); assert(![...f.objects.keys()].some((key) => key.includes('latest'))); assert.deepEqual(JSON.parse(f.objects.get(f.events.at(-1).key)), { version: '1.2.3', target: f.metadata.target, url, sha256: createHash('sha256').update('verified release').digest('hex'), checksumUrl: `${url}.sha256`, }); }); for (const response of ['', ' \n\t', '{}']) { test(`unversioned bucket response ${JSON.stringify(response)} permits a verified upload`, async (t) => { const f = fixture(t); const run = f.run; f.run = (command, args) => { const result = run(command, args); return args.includes('get-bucket-versioning') ? response : result; }; await publishMacos(f); assert.equal(f.objects.size, 3); assert.equal(f.events.at(-1).operation, 'public-get'); assert(f.events.at(-1).key.endsWith('/release.json')); }); } for (const response of ['{', 'null', '[]', '"Enabled"', 'false']) { test(`invalid versioning response ${response} aborts before any upload`, async (t) => { const f = fixture(t); f.run = (_command, args) => { assert(args.includes('get-bucket-versioning')); return response; }; await assert.rejects(publishMacos(f), /Invalid .*versioning/); assert.equal(f.objects.size, 0); }); } test('retry accepts byte-identical objects without replacing them', async (t) => { const f = fixture(t); await publishMacos(f); const originals = [...f.objects.entries()]; await publishMacos(f); assert.deepEqual([...f.objects.entries()], originals); assert.equal( f.events.filter((event) => event.operation === 'get-object').length, 3, ); }); test('different existing release is never overwritten', async (t) => { const f = fixture(t); const key = `releases/macos/${f.metadata.target}/1.2.3/${f.metadata.filename}`; f.objects.set(key, Buffer.from('previous version content')); await assert.rejects(publishMacos(f), /bump the app version/); assert.equal(f.objects.get(key).toString(), 'previous version content'); assert.equal(f.objects.size, 1); }); test('checksum mismatch aborts before contacting S3', async (t) => { const f = fixture(t); writeFileSync(join(f.directory, f.metadata.filename), 'tampered'); await assert.rejects(publishMacos(f), /SHA-256/); assert.equal(f.events.length, 0); }); for (const status of ['Enabled', 'Suspended']) { test(`rejects ${status} bucket versioning before any write`, async (t) => { const f = fixture(t); f.run = () => JSON.stringify({ Status: status }); await assert.rejects(publishMacos(f), /unversioned/); assert.equal(f.objects.size, 0); }); } for (const operation of ['get-bucket-versioning', 'put-object', 'get-object']) { test(`${operation} access or network failure cannot be mistaken for a missing object`, async (t) => { const f = fixture(t); await publishMacos(f); const run = f.run; f.events.length = 0; f.run = (command, args) => { if (args.includes(operation)) { throw new Error('AccessDenied'); } return run(command, args); }; await assert.rejects(publishMacos(f), /AccessDenied/); assert(!f.events.some((event) => event.key.endsWith('/release.json'))); }); } for (const failure of ['private', 'wrong-content', 'network']) { test(`public download ${failure} prevents completion record`, async (t) => { const f = fixture(t); f.fetchPublic = async () => { if (failure === 'network') { throw new Error('network failure'); } return { ok: failure !== 'private', arrayBuffer: async () => Buffer.from('wrong'), }; }; await assert.rejects(publishMacos(f)); assert( ![...f.objects.keys()].some((key) => key.endsWith('/release.json')), ); }); } test('a partial upload can resume with the same files', async (t) => { const f = fixture(t); const run = f.run; f.run = (command, args) => { if ( args.includes('put-object') && args[args.indexOf('--key') + 1].endsWith('.sha256') ) { throw new Error('upload interrupted'); } return run(command, args); }; await assert.rejects(publishMacos(f), /interrupted/); assert.equal(f.objects.size, 1); f.run = run; await publishMacos(f); assert.equal(f.objects.size, 3); }); for (const overrides of [ { S3_REGION: '' }, { S3_REGION: 'eu-central-1' }, { S3_BUCKET: '' }, { S3_BUCKET: 'other/../bucket' }, { S3_PREFIX: '../other' }, { S3_PUBLIC_BASE_URL: 'http://downloads.example.com' }, { S3_PUBLIC_BASE_URL: 'https://secret@downloads.example.com' }, { S3_PUBLIC_BASE_URL: 'https://downloads.example.com?token=secret' }, { AWS_ACCESS_KEY_ID: '' }, { AWS_SECRET_ACCESS_KEY: '' }, ]) { test(`invalid storage configuration is rejected before S3 access: ${JSON.stringify(overrides)}`, async (t) => { const f = fixture(t); f.env = { ...f.env, ...overrides }; await assert.rejects(publishMacos(f)); assert.equal(f.events.length, 0); }); } test('derives Hetzner endpoints and supports an explicit public proxy URL', () => { for (const region of ['nbg1', 'fsn1', 'hel1']) { const config = storageConfiguration({ S3_REGION: region, S3_BUCKET: 'release-bucket', S3_PUBLIC_BASE_URL: 'https://downloads.example.com/', }); assert.equal( config.endpoint, `https://${region}.your-objectstorage.com`, ); assert.equal(config.publicBase, 'https://downloads.example.com'); } });