import assert from 'node:assert/strict'; import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import test from 'node:test'; import { packageNotarizedApp, releaseMetadata, verifyNotarizedApp, } from '../macos-ci.mjs'; function fixture(t) { const directory = mkdtempSync(join(tmpdir(), 'kanbangit-ci-test-')); t.after(() => rmSync(directory, { recursive: true, force: true })); mkdirSync(join(directory, 'src-tauri')); const config = { version: '1.2.3', productName: 'KanbanGit', identifier: 'de.kanbangit.desktop', }; writeFileSync( join(directory, 'src-tauri/tauri.conf.json'), JSON.stringify(config), ); writeFileSync( join(directory, 'package.json'), JSON.stringify({ version: '1.2.3' }), ); writeFileSync( join(directory, 'src-tauri/Cargo.toml'), '[package]\nname = "kanbangit-native"\nversion = "1.2.3"\n[dependencies]\nother = "2"\n', ); const env = { RELEASE_TAG: 'macos-v1.2.3', APPLE_SIGNING_IDENTITY: 'Developer ID Application: Example (ZZX2KZGTHU)', }; const calls = []; const run = (command, args) => { calls.push({ command, args }); if (command === 'codesign' && args.includes('--display')) { return 'CodeDirectory flags=0x10000(runtime)\nCDHash=abcdef123456'; } if (command === 'plutil') { return '1.2.3'; } if (command === 'lipo') { return 'arm64'; } if (command === 'ditto' && args.includes('-c')) { writeFileSync(args.at(-1), 'notarized archive'); } return ''; }; return { directory, env, config, run, calls }; } test('release metadata binds tag, all app versions, identity and architecture', (t) => { const f = fixture(t); assert.deepEqual(releaseMetadata(f.env, f.directory), { version: '1.2.3', target: 'aarch64-apple-darwin', arch: 'arm64', filename: 'KanbanGit-1.2.3-aarch64-apple-darwin.zip', }); assert.equal( releaseMetadata( { ...f.env, MACOS_RELEASE_TARGET: 'x86_64-apple-darwin' }, f.directory, ).arch, 'x86_64', ); }); for (const env of [ { RELEASE_TAG: 'v1.2.3' }, { RELEASE_TAG: 'macos-v1.2.4' }, { RELEASE_TAG: 'macos-v1.2.3; echo bad' }, { MACOS_RELEASE_TARGET: 'x86_64-unknown-linux-gnu' }, { APPLE_SIGNING_IDENTITY: '-' }, { APPLE_SIGNING_IDENTITY: 'Apple Development: Example (ZZX2KZGTHU)' }, { APPLE_SIGNING_IDENTITY: 'Developer ID Application: Example (OLDTEAM123)', }, ]) { test(`rejects invalid release inputs ${JSON.stringify(env)}`, (t) => { const f = fixture(t); assert.throws(() => releaseMetadata({ ...f.env, ...env }, f.directory)); }); } for (const [file, contents] of [ ['package.json', '{"version":"1.2.4"}'], ['src-tauri/Cargo.toml', '[package]\nversion = "1.2.4"'], ['src-tauri/tauri.conf.json', '{"version":"01.2.3"}'], [ 'src-tauri/tauri.conf.json', '{"version":"1.2.3","identifier":"other","productName":"KanbanGit"}', ], ]) { test(`rejects inconsistent metadata in ${file}: ${contents}`, (t) => { const f = fixture(t); writeFileSync(join(f.directory, file), contents); assert.throws(() => releaseMetadata(f.env, f.directory)); }); } test('packages only a verified app and checks the extracted download again', (t) => { const f = fixture(t); const output = packageNotarizedApp(f); assert.equal( readFileSync(join(output.directory, output.filename), 'utf8'), 'notarized archive', ); assert.equal( readFileSync( join(output.directory, `${output.filename}.sha256`), 'utf8', ), `${output.checksum} ${output.filename}\n`, ); assert.equal(f.calls.filter((call) => call.command === 'spctl').length, 2); assert.equal(f.calls.filter((call) => call.command === 'xcrun').length, 2); assert.match( f.calls.findLast((call) => call.command === 'spctl').args.at(-1), /extracted\/KanbanGit\.app$/, ); assert.throws(() => packageNotarizedApp(f), /already exists/); }); for (const failure of ['codesign', 'lipo', 'xcrun', 'spctl', 'ditto']) { test(`does not publish local output when ${failure} verification fails`, (t) => { const f = fixture(t); const run = f.run; f.run = (command, args) => { if (command === failure) { throw new Error('verification failed'); } return run(command, args); }; assert.throws(() => packageNotarizedApp(f), /verification failed/); assert.equal( existsSync( join( f.directory, 'src-tauri/target/macos-release/1.2.3-aarch64-apple-darwin', ), ), false, ); }); } test('rejects an app built for a different architecture', (t) => { const f = fixture(t); const metadata = releaseMetadata(f.env, f.directory); assert.throws( () => verifyNotarizedApp('KanbanGit.app', metadata, (command, args) => command === 'lipo' ? 'x86_64' : f.run(command, args), ), /architecture/, ); }); test('failed validation of the extracted app discards staged download', (t) => { const f = fixture(t); const run = f.run; f.run = (command, args) => { if (command === 'spctl' && args.at(-1).includes('extracted/')) { throw new Error('roundtrip invalid'); } return run(command, args); }; assert.throws(() => packageNotarizedApp(f), /roundtrip invalid/); assert.equal( existsSync( join( f.directory, 'src-tauri/target/macos-release/1.2.3-aarch64-apple-darwin', ), ), false, ); });