import assert from 'node:assert/strict'; import { Buffer } from 'node:buffer'; import { spawnSync } from 'node:child_process'; import { createHash } from 'node:crypto'; import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import process from 'node:process'; import test from 'node:test'; import { fileURLToPath } from 'node:url'; import { conditionalEtag, publishMacos, storageConfiguration, } from '../publish-macos.mjs'; import { signedFixture } from './updater-fixture.mjs'; test('publisher rejects the removed resume command before accessing storage', () => { const result = spawnSync( process.execPath, [ fileURLToPath(new URL('../publish-macos.mjs', import.meta.url)), 'resume', ], { encoding: 'utf8' }, ); assert.equal(result.status, 1); assert.equal(result.stdout, ''); assert.match( result.stderr, /Use config or upload; upload requires RELEASE_DIRECTORY/, ); }); test('conditional writes use bare S3 ETags without weakening the condition', () => { const etag = 'b88312b96169f4b2098a4cc43b0abb6a'; for (const value of [etag, `"${etag}"`, `${etag}-2`, `"${etag}-2"`]) { assert.equal(conditionalEtag(value), value.replaceAll('"', '')); } for (const invalid of [ undefined, null, '', '*', '"*"', `W/"${etag}"`, `"${etag}`, `${etag}"`, `${etag},${etag}`, `${etag}\r\n`, {}, 42, ]) { assert.throws(() => conditionalEtag(invalid), /valid S3 ETag/); } }); function fixture( t, conflictMessage = 'aws failed: (PreconditionFailed) object exists', ) { 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.dmg', updaterFilename: 'KanbanGit-1.2.3-aarch64-apple-darwin.app.tar.gz', }; 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 updater = Buffer.from('signed update'); const signing = signedFixture(updater); writeFileSync(join(directory, metadata.updaterFilename), updater); writeFileSync( join(directory, `${metadata.updaterFilename}.sig`), signing.signature, ); writeFileSync( join(directory, `${metadata.updaterFilename}.sha256`), `${createHash('sha256').update(updater).digest('hex')} ${metadata.updaterFilename}\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, args }); if (operation === 'get-bucket-versioning') { assert.equal(value('--output'), 'json'); return ''; } if (operation === 'put-object') { if (!args.includes('--if-match')) { assert.equal(value('--if-none-match'), '*'); } if (objects.has(key)) { if (!args.includes('--if-match')) { throw new Error(conflictMessage); } assert.equal( value('--if-match'), 'b88312b96169f4b2098a4cc43b0abb6a', ); } objects.set(key, readFileSync(value('--body'))); return '{}'; } if (operation === 'get-object') { if (!objects.has(key)) { throw new Error('aws failed: (NoSuchKey) object missing'); } writeFileSync(args.at(-1), objects.get(key)); return JSON.stringify({ ETag: '"b88312b96169f4b2098a4cc43b0abb6a"', }); } 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, publicKey: signing.publicKey, feedBase: 'https://release-bucket.nbg1.your-objectstorage.com/releases/macos', metadata, env, objects, events, run, fetchPublic, wait: async () => {}, }; } test('uploads revalidated 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.equal( f.events.filter((event) => event.operation === 'put-object').length, 9, ); assert.equal(f.objects.size, 9); for (const event of f.events.filter( (event) => event.operation === 'put-object', )) { assert.equal( event.args[event.args.indexOf('--cache-control') + 1], 'no-cache, max-age=0, must-revalidate', ); } assert(f.events.at(-1).key.endsWith('/latest.json')); assert.equal( f.events.at(-1).key, 'releases/macos/aarch64-apple-darwin/latest.json', ); const downloadKey = 'releases/macos/aarch64-apple-darwin/KanbanGit.dmg'; assert.deepEqual( f.objects.get(downloadKey), readFileSync(join(f.directory, f.metadata.filename)), ); const downloadPut = f.events.findIndex( (event) => event.operation === 'put-object' && event.key === downloadKey, ); const downloadVerified = f.events.findIndex( (event) => event.operation === 'public-get' && event.key === downloadKey, ); const feedPut = f.events.findIndex( (event) => event.operation === 'put-object' && event.key.endsWith('/latest.json'), ); assert(downloadPut > 0 && downloadVerified > downloadPut); assert(feedPut > downloadVerified); assert.deepEqual( JSON.parse( f.objects.get( 'releases/macos/aarch64-apple-darwin/1.2.3/release.json', ), ), { version: '1.2.3', target: f.metadata.target, url, sha256: createHash('sha256') .update('verified release') .digest('hex'), checksumUrl: `${url}.sha256`, }, ); }); test('replaces the stable download only for a newer verified release', async (t) => { const f = fixture(t); const downloadKey = `releases/macos/${f.metadata.target}/KanbanGit.dmg`; const latestKey = `releases/macos/${f.metadata.target}/latest.json`; f.objects.set(downloadKey, Buffer.from('previous download')); f.objects.set(latestKey, Buffer.from(JSON.stringify({ version: '1.0.0' }))); await publishMacos(f); assert.deepEqual( f.objects.get(downloadKey), readFileSync(join(f.directory, f.metadata.filename)), ); assert.equal(JSON.parse(f.objects.get(latestKey)).version, '1.2.3'); const replacement = f.events.find( (event) => event.operation === 'put-object' && event.key === downloadKey && event.args.includes('--if-match'), ); assert(replacement); }); test('a newer published version protects the stable download even when its alias is missing', async (t) => { const f = fixture(t); const downloadKey = `releases/macos/${f.metadata.target}/KanbanGit.dmg`; const latestKey = `releases/macos/${f.metadata.target}/latest.json`; f.objects.set(latestKey, Buffer.from(JSON.stringify({ version: '2.0.0' }))); await assert.rejects(publishMacos(f), /downgrade/); assert.equal(f.objects.has(downloadKey), false); assert.equal(JSON.parse(f.objects.get(latestKey)).version, '2.0.0'); }); test('a published version cannot replace a different stable download', async (t) => { const f = fixture(t); const downloadKey = `releases/macos/${f.metadata.target}/KanbanGit.dmg`; const latestKey = `releases/macos/${f.metadata.target}/latest.json`; const original = Buffer.from('published download'); f.objects.set(downloadKey, original); f.objects.set(latestKey, Buffer.from(JSON.stringify({ version: '1.2.3' }))); await assert.rejects(publishMacos(f), /bump the app version/); assert.deepEqual(f.objects.get(downloadKey), original); }); test('a failed public stable download blocks feed promotion', async (t) => { const f = fixture(t); const fetchPublic = f.fetchPublic; f.fetchPublic = (url, options) => url.endsWith('/KanbanGit.dmg') ? Promise.resolve({ ok: false, status: 403 }) : fetchPublic(url, options); await assert.rejects(publishMacos(f), /KanbanGit\.dmg: HTTP 403/); assert.equal( f.objects.has(`releases/macos/${f.metadata.target}/latest.json`), false, ); }); test('concurrent stable download replacement preserves the winner', async (t) => { const f = fixture(t); const downloadKey = `releases/macos/${f.metadata.target}/KanbanGit.dmg`; const latestKey = `releases/macos/${f.metadata.target}/latest.json`; f.objects.set(downloadKey, Buffer.from('previous download')); f.objects.set(latestKey, Buffer.from(JSON.stringify({ version: '1.0.0' }))); const run = f.run; f.run = (command, args) => { if (args.includes('--if-match') && args.includes(downloadKey)) { f.objects.set(downloadKey, Buffer.from('concurrent winner')); throw new Error('(PreconditionFailed) concurrent replacement'); } return run(command, args); }; await assert.rejects(publishMacos(f), /concurrent replacement/); assert.equal(f.objects.get(downloadKey).toString(), 'concurrent winner'); assert.equal(JSON.parse(f.objects.get(latestKey)).version, '1.0.0'); }); 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, 9); assert.equal(f.events.at(-1).operation, 'public-get'); assert(f.events.at(-1).key.endsWith('/latest.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, 11, ); }); for (const published of ['1.2.3', '1.2.4']) { test(`published release stays protected when the feed is at ${published}`, 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')); f.objects.set( `releases/macos/${f.metadata.target}/latest.json`, Buffer.from(JSON.stringify({ version: published })), ); await assert.rejects(publishMacos(f), /bump the app version/); assert.equal(f.objects.get(key).toString(), 'previous version content'); assert.equal(f.objects.size, 2); }); } 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', status: failure === 'private' ? 403 : 200, 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 be retried 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, 3); f.run = run; await publishMacos(f); assert.equal(f.objects.size, 9); }); function rebuild(f) { const updater = Buffer.from('rebuilt signed update'); const signing = signedFixture(updater); f.publicKey = signing.publicKey; f.metadata.notes = 'Fixed build of the same version'; for (const [name, contents] of [ [f.metadata.filename, Buffer.from('rebuilt disk image')], [f.metadata.updaterFilename, updater], ]) { writeFileSync(join(f.directory, name), contents); writeFileSync( join(f.directory, `${name}.sha256`), `${createHash('sha256').update(contents).digest('hex')} ${name}\n`, ); } writeFileSync( join(f.directory, `${f.metadata.updaterFilename}.sig`), signing.signature, ); } for (const previous of [null, '1.0.0']) { for (const interruptedFile of ['.sha256', '/latest.json']) { test(`rebuild replaces unfinished ${interruptedFile} release with previous feed ${previous}`, async (t) => { const f = fixture(t); const latestKey = `releases/macos/${f.metadata.target}/latest.json`; const previousBytes = previous === null ? undefined : Buffer.from(JSON.stringify({ version: previous })); if (previousBytes) { f.objects.set(latestKey, previousBytes); } const run = f.run; f.run = (command, args) => { if ( args.includes('put-object') && args[args.indexOf('--key') + 1].endsWith(interruptedFile) ) { throw new Error('upload interrupted'); } return run(command, args); }; await assert.rejects(publishMacos(f), /upload interrupted/); assert.deepEqual(f.objects.get(latestKey), previousBytes); rebuild(f); f.events.length = 0; let promoted = false; f.run = (command, args) => { if (!promoted) { assert.deepEqual(f.objects.get(latestKey), previousBytes); } const result = run(command, args); if (args.includes('put-object') && args.includes(latestKey)) { promoted = true; } return result; }; await publishMacos(f); assert.equal(f.objects.size, 9); assert.equal(JSON.parse(f.objects.get(latestKey)).version, '1.2.3'); assert.equal( JSON.parse(f.objects.get(latestKey)).notes, f.metadata.notes, ); for (const name of [ f.metadata.filename, `${f.metadata.filename}.sha256`, f.metadata.updaterFilename, `${f.metadata.updaterFilename}.sig`, `${f.metadata.updaterFilename}.sha256`, ]) { assert.deepEqual( f.objects.get( `releases/macos/${f.metadata.target}/1.2.3/${name}`, ), readFileSync(join(f.directory, name)), ); } const published = [...f.objects.entries()]; rebuild(f); await assert.rejects(publishMacos(f), /already been released/); assert.deepEqual([...f.objects.entries()], published); }); } } test('failure after promoting the feed still protects that version from rebuilding', async (t) => { const f = fixture(t); const fetchPublic = f.fetchPublic; f.fetchPublic = async (url, options) => url.endsWith('/latest.json') ? { ok: false, status: 403 } : fetchPublic(url, options); await assert.rejects(publishMacos(f), /HTTP 403/); const published = [...f.objects.entries()]; rebuild(f); await assert.rejects(publishMacos(f), /already been released/); assert.deepEqual([...f.objects.entries()], published); }); test('a concurrent change to a partial artifact is not 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('unfinished old build')); const run = f.run; f.run = (command, args) => { if (args.includes('--if-match')) { assert(args.includes(key)); f.objects.set(key, Buffer.from('concurrent winner')); throw new Error('(PreconditionFailed) concurrent replacement'); } return run(command, args); }; await assert.rejects(publishMacos(f), /concurrent replacement/); assert.equal(f.objects.get(key).toString(), 'concurrent winner'); assert.equal(f.objects.size, 1); }); for (const failure of [ 'AccessDenied', 'network failure', 'invalid JSON', 'invalid version', ]) { test(`cannot replace a partial artifact when the feed check has ${failure}`, async (t) => { const f = fixture(t); const key = `releases/macos/${f.metadata.target}/1.2.3/${f.metadata.filename}`; const latestKey = `releases/macos/${f.metadata.target}/latest.json`; f.objects.set(key, Buffer.from('unfinished old build')); f.objects.set( latestKey, Buffer.from( failure === 'invalid version' ? '{"version":"invalid"}' : '{', ), ); const originals = [...f.objects.entries()]; const run = f.run; f.run = (command, args) => { if ( args.includes('get-object') && args.includes(latestKey) && !failure.startsWith('invalid') ) { throw new Error(failure); } return run(command, args); }; await assert.rejects(publishMacos(f)); assert.deepEqual([...f.objects.entries()], originals); }); } for (const listing of [ { KeyCount: 0, IsTruncated: false }, { KeyCount: 1, IsTruncated: false, Contents: [{ Key: 'latest.json' }] }, {}, { KeyCount: 0, IsTruncated: true }, { KeyCount: 0, IsTruncated: false, Contents: '' }, 'AccessDenied', ]) { test(`ambiguous feed read error needs a proven empty listing: ${JSON.stringify(listing)}`, async (t) => { const f = fixture(t); const key = `releases/macos/${f.metadata.target}/1.2.3/${f.metadata.filename}`; const latestKey = `releases/macos/${f.metadata.target}/latest.json`; f.objects.set(key, Buffer.from('unfinished old build')); const run = f.run; f.run = (command, args) => { if (args.includes('get-object') && args.includes(latestKey)) { throw new Error( "argument of type 'NoneType' is not a container or iterable", ); } if (args.includes('list-objects-v2')) { assert.equal(args[args.indexOf('--prefix') + 1], latestKey); if (listing === 'AccessDenied') { throw new Error('AccessDenied'); } return JSON.stringify(listing); } return run(command, args); }; if ( listing.KeyCount === 0 && listing.IsTruncated === false && !('Contents' in listing) ) { await publishMacos(f); assert.equal(f.objects.size, 9); } else { await assert.rejects(publishMacos(f), /NoneType|AccessDenied/); assert.equal(f.objects.get(key).toString(), 'unfinished old build'); assert.equal(f.objects.size, 1); } }); } for (const status of [403, 404, 408, 429, 500, 502, 503, 504, 'network']) { test(`temporary public download failure ${status} recovers before completion`, async (t) => { const f = fixture(t); const fetchPublic = f.fetchPublic; let attempts = 0; let cancelled = 0; const waits = []; f.wait = async (ms) => waits.push(ms); f.fetchPublic = async (...args) => { if (++attempts <= 2) { if (status === 'network') { throw new Error('temporary network failure'); } return { ok: false, status, body: { cancel: async () => cancelled++ }, }; } return fetchPublic(...args); }; await publishMacos(f); assert.deepEqual(waits, [2000, 2000]); assert.equal(cancelled, status === 'network' ? 0 : 2); assert.equal(f.objects.size, 9); assert(f.events.at(-1).key.endsWith('/latest.json')); }); } test('persistent public access denial stops after five attempts with HTTP status', async (t) => { const f = fixture(t); let attempts = 0; const waits = []; f.wait = async (ms) => waits.push(ms); f.fetchPublic = async () => { attempts++; return { ok: false, status: 403 }; }; await assert.rejects(publishMacos(f), /HTTP 403/); assert.equal(attempts, 5); assert.equal(waits.length, 4); assert.equal(f.objects.size, 1); }); test('incorrect public bytes fail immediately without retry or completion record', async (t) => { const f = fixture(t); f.wait = async () => assert.fail('Do not retry mismatched content'); f.fetchPublic = async () => ({ ok: true, arrayBuffer: async () => Buffer.from('incorrect'), }); await assert.rejects(publishMacos(f), /SHA-256 mismatch/); assert.equal(f.objects.size, 1); }); test('checksum access retries finish before publishing the completion record', async (t) => { const f = fixture(t); const fetchPublic = f.fetchPublic; let failures = 0; f.fetchPublic = async (url, options) => { if (url.endsWith(`${f.metadata.filename}.sha256`) && ++failures <= 2) { assert.equal(f.objects.size, 5); return { ok: false, status: 403 }; } return fetchPublic(url, options); }; await publishMacos(f); assert.equal(failures, 3); assert.equal(f.objects.size, 9); }); 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'); } }); test('refuses a destination that differs from the built-in update source', async (t) => { const f = fixture(t); f.feedBase = 'https://another.example/releases/macos'; await assert.rejects(publishMacos(f), /does not match/); assert.equal(f.objects.size, 0); }); test('rejects a corrupt updater archive before publishing any object', async (t) => { const f = fixture(t); writeFileSync(join(f.directory, f.metadata.updaterFilename), 'tampered'); await assert.rejects(publishMacos(f), /signature/); assert.equal(f.objects.size, 0); }); test('atomically promotes a newer feed only after all release files are public', async (t) => { const f = fixture(t); const latestKey = `releases/macos/${f.metadata.target}/latest.json`; f.objects.set(latestKey, Buffer.from(JSON.stringify({ version: '1.0.0' }))); await publishMacos(f); assert.equal(JSON.parse(f.objects.get(latestKey)).version, '1.2.3'); const releaseVerified = f.events.findIndex( (event) => event.operation === 'public-get' && event.key.endsWith('/update.json'), ); const latestPut = f.events.findIndex( (event) => event.operation === 'put-object' && event.key === latestKey, ); assert(latestPut > releaseVerified); }); for (const previous of [ { version: '2.0.0' }, { version: '1.2.3', notes: 'different' }, ]) { test(`does not replace conflicting update feed ${previous.version}`, async (t) => { const f = fixture(t); const key = `releases/macos/${f.metadata.target}/latest.json`; const original = Buffer.from(JSON.stringify(previous)); f.objects.set(key, original); await assert.rejects(publishMacos(f), /downgrade|differs/); assert.deepEqual(f.objects.get(key), original); }); } test('concurrent feed replacement fails without overwriting the winner', async (t) => { const f = fixture(t); const key = `releases/macos/${f.metadata.target}/latest.json`; f.objects.set(key, Buffer.from(JSON.stringify({ version: '1.0.0' }))); const run = f.run; f.run = (command, args) => { if (args.includes('--if-match')) { f.objects.set( key, Buffer.from(JSON.stringify({ version: '2.0.0' })), ); throw new Error('(PreconditionFailed) concurrent publication'); } return run(command, args); }; await assert.rejects(publishMacos(f), /concurrent publication/); assert.equal(JSON.parse(f.objects.get(key)).version, '2.0.0'); }); for (const message of [ "aws failed (exit 255). aws: [ERROR]: argument of type 'NoneType' is not a container or iterable", "aws failed (exit 255). argument of type 'NoneType' is not iterable", ]) { test(`empty S3 error message permits verified promotion and identical retry: ${message}`, async (t) => { const f = fixture(t, message); const key = `releases/macos/${f.metadata.target}/latest.json`; f.objects.set(key, Buffer.from(JSON.stringify({ version: '1.0.0' }))); await publishMacos(f); assert.equal(JSON.parse(f.objects.get(key)).version, '1.2.3'); const published = [...f.objects.entries()]; await publishMacos(f); assert.deepEqual([...f.objects.entries()], published); }); test(`empty S3 error cannot replace a published release: ${message}`, async (t) => { const f = fixture(t, message); const key = `releases/macos/${f.metadata.target}/1.2.3/${f.metadata.filename}`; f.objects.set(key, Buffer.from('different existing release')); f.objects.set( `releases/macos/${f.metadata.target}/latest.json`, Buffer.from(JSON.stringify({ version: '1.2.3' })), ); await assert.rejects(publishMacos(f), /bump the app version/); assert.equal(f.objects.size, 2); assert.equal( f.objects.get(key).toString(), 'different existing release', ); }); for (const previous of [ { version: '2.0.0' }, { version: '1.2.3', notes: 'different' }, ]) { test(`empty S3 error cannot bypass feed conflict checks ${previous.version}: ${message}`, async (t) => { const f = fixture(t, message); const key = `releases/macos/${f.metadata.target}/latest.json`; const original = Buffer.from(JSON.stringify(previous)); f.objects.set(key, original); await assert.rejects(publishMacos(f), /downgrade|differs/); assert.deepEqual(f.objects.get(key), original); }); } test(`empty S3 error aborts when existing contents cannot be read: ${message}`, async (t) => { const f = fixture(t, message); const run = f.run; f.run = (command, args) => { if (args.includes('put-object')) { throw new Error(message); } if (args.includes('get-object')) { throw new Error('AccessDenied'); } return run(command, args); }; await assert.rejects(publishMacos(f), /S3 get-object .*AccessDenied/); assert.equal(f.objects.size, 0); }); test(`empty S3 error during conditional replacement preserves a concurrent winner: ${message}`, async (t) => { const f = fixture(t, message); const key = `releases/macos/${f.metadata.target}/latest.json`; f.objects.set(key, Buffer.from(JSON.stringify({ version: '1.0.0' }))); const run = f.run; let attempts = 0; f.run = (command, args) => { if (args.includes('--if-match')) { attempts++; f.objects.set( key, Buffer.from(JSON.stringify({ version: '2.0.0' })), ); throw new Error(message); } return run(command, args); }; await assert.rejects( publishMacos(f), /S3 put-object .*latest.json: .*NoneType/, ); assert.equal(attempts, 1); assert.equal(JSON.parse(f.objects.get(key)).version, '2.0.0'); }); }