// Mac App Store build of a Tauri app (cli/lib/mas.ts, cli/apple/build-mas.ts): plan, entitlements, identities, path-free // flags, leak scan, app / pkg checks, and the pkg variant of the build upload requests. import { afterAll, describe, expect, test } from 'bun:test'; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { tauriCommand } from '../cli/apple/build-mas'; import { appProblems, categoryId, entitlementKeysOf, entitlementsXml, findTauri, masEntitlements, masSteps, parsePackageInfo, parsePkgSignature, pathFreeEnv, pickMasIdentities, pickMasProfiles, pkgProblems, scanPathLeaks, tauriOverlay } from '../cli/lib/mas'; import { altoolArgs, buildUploadReqs } from '../cli/lib/xcode'; const tmp = mkdtempSync(join(tmpdir(), 'sb-mas-')); afterAll(() => rmSync(tmp, { recursive: true, force: true })); const conf = { productName: 'Demo App', identifier: 'com.example.demo', version: '1.2.3', bundle: { category: 'Utility', macOS: { minimumSystemVersion: '12.0' } } }; function fixture(): string { const root = mkdtempSync(join(tmp, 'p-')); mkdirSync(join(root, 'apps/desktop/src-tauri'), { recursive: true }); writeFileSync(join(root, 'apps/desktop/src-tauri/tauri.conf.json'), JSON.stringify(conf)); return root; } describe('tauri project', () => { test('found by scan and by hint; fields read', () => { const root = fixture(); const t = findTauri(root); expect(t.productName).toBe('Demo App'); expect(t.identifier).toBe('com.example.demo'); expect(t.cwd).toBe(join(root, 'apps/desktop')); expect(t.minOs).toBe('12.0'); expect(findTauri(root, 'apps/desktop/src-tauri').dir).toBe(t.dir); expect(() => findTauri(root, 'nope')).toThrow('no tauri.conf.json'); }); test('none or several found', () => { const empty = mkdtempSync(join(tmp, 'e-')); expect(() => findTauri(empty)).toThrow('no tauri.conf.json found'); const two = fixture(); mkdirSync(join(two, 'other/src-tauri'), { recursive: true }); writeFileSync(join(two, 'other/src-tauri/tauri.conf.json'), JSON.stringify(conf)); expect(() => findTauri(two)).toThrow('several Tauri projects'); }); test('tauri command: project binary, cargo-tauri, else bunx', () => { expect(tauriCommand('/a/b', p => /a.node_modules..bin.tauri$/.test(p), () => null)).toHaveLength(1); expect(tauriCommand('/a/b', () => false, c => (c === 'cargo-tauri' ? '/x' : null))).toEqual(['cargo', 'tauri']); expect(tauriCommand('/a/b', () => false, () => null)[0]).toBe('bunx'); }); test('category ids', () => { expect(categoryId('Utility')).toBe('public.app-category.utilities'); expect(categoryId('public.app-category.games')).toBe('public.app-category.games'); expect(categoryId('Nope')).toBeNull(); }); }); describe('entitlements', () => { test('sandbox and identifiers forced, project keys kept', () => { const e = masEntitlements({ teamId: 'ABCDE12345', bundleId: 'com.example.demo', existing: { 'com.apple.security.app-sandbox': false, 'com.apple.security.files.user-selected.read-write': true } }); expect(e['com.apple.security.app-sandbox']).toBe(true); expect(e['com.apple.application-identifier']).toBe('ABCDE12345.com.example.demo'); expect(e['com.apple.developer.team-identifier']).toBe('ABCDE12345'); expect(e['com.apple.security.files.user-selected.read-write']).toBe(true); expect(e['com.apple.security.network.client']).toBe(true); }); test('xml round trip', () => { const e = masEntitlements({ teamId: 'ABCDE12345', bundleId: 'com.example.demo' }); expect(entitlementKeysOf(entitlementsXml(e))?.['com.apple.security.app-sandbox']).toBe(true); expect(entitlementKeysOf('garbage')).toBeNull(); }); }); describe('identities and profiles', () => { const ids = [ { sha1: 'A', name: 'Apple Development: X (AAAAAAAAAA)' }, { sha1: 'B', name: 'Apple Distribution: Co (ABCDE12345)' }, { sha1: 'C', name: '3rd Party Mac Developer Installer: Co (ABCDE12345)' }, { sha1: 'D', name: 'Apple Distribution: Other (ZZZZZZZZZZ)' }, ]; test('app and installer identity of the team', () => { const p = pickMasIdentities(ids, 'ABCDE12345'); expect(p.app?.sha1).toBe('B'); expect(p.installer?.sha1).toBe('C'); expect(pickMasIdentities(ids, 'QQQQQQQQQQ')).toEqual({ app: null, installer: null }); expect(pickMasIdentities([{ sha1: 'E', name: '3rd Party Mac Developer Application: Co (ABCDE12345)' }, ids[1]!], 'ABCDE12345').app?.sha1).toBe('E'); expect(pickMasIdentities([{ sha1: 'F', name: 'Mac Installer Distribution: Co (ABCDE12345)' }, ids[2]!], 'ABCDE12345').installer?.sha1).toBe('F'); }); const prof = (o: Record) => ({ path: '/p', profile: { UUID: 'u1', Name: 'n', TeamIdentifier: ['ABCDE12345'], Platform: ['OSX'], ExpirationDate: '2099-01-01T00:00:00Z', Entitlements: { 'com.apple.application-identifier': 'ABCDE12345.com.example.demo' }, ...o } as never }); test('Mac App Store profile: macOS, no devices, matching id, not expired', () => { expect(pickMasProfiles([prof({})], 'com.example.demo', 'ABCDE12345')).toHaveLength(1); expect(pickMasProfiles([prof({ Platform: ['iOS'] })], 'com.example.demo', null)).toHaveLength(0); expect(pickMasProfiles([prof({ ProvisionedDevices: ['x'] })], 'com.example.demo', null)).toHaveLength(0); expect(pickMasProfiles([prof({})], 'com.example.other', null)).toHaveLength(0); expect(pickMasProfiles([prof({ ExpirationDate: '2000-01-01T00:00:00Z' })], 'com.example.demo', null)).toHaveLength(0); }); }); describe('path-free', () => { test('remaps checkout, cargo, rustup, home; no debug info', () => { const e = pathFreeEnv({ home: '/Users/you', user: 'you', root: '/Users/you/work/app', rustflags: '-C target-cpu=native' }); expect(e.RUSTFLAGS).toContain('-C target-cpu=native'); expect(e.RUSTFLAGS).toContain('--remap-path-prefix=/Users/you/work/app=/src'); expect(e.RUSTFLAGS).toContain('--remap-path-prefix=/Users/you/.cargo=/cargo'); expect(e.RUSTFLAGS).toContain('--remap-path-prefix=/Users/you/.rustup=/rustup'); expect(e.RUSTFLAGS).toContain('--remap-path-prefix=/Users/you=/build-home'); expect(e.CARGO_PROFILE_RELEASE_DEBUG).toBe('0'); }); test('leak scan finds the home folder in a binary and a plist', () => { const app = mkdtempSync(join(tmp, 'app-')); mkdirSync(join(app, 'Contents/MacOS'), { recursive: true }); writeFileSync(join(app, 'Contents/MacOS/bin'), Buffer.concat([Buffer.from([0, 1, 2]), Buffer.from('panic at /Users/you/.cargo/registry/x.rs'), Buffer.from([0])])); writeFileSync(join(app, 'Contents/Info.plist'), 'clean'); const leaks = scanPathLeaks(app, ['/Users/you', '/Users/you/']); expect(leaks.map(l => `${l.file}:${l.needle}`)).toEqual(['Contents/MacOS/bin:/Users/you', 'Contents/MacOS/bin:/Users/you/']); expect(scanPathLeaks(app, ['/Users/me'])).toEqual([]); }); }); describe('app and pkg checks', () => { const good = { info: { CFBundleIdentifier: 'com.example.demo', CFBundleShortVersionString: '1.2.3', CFBundleVersion: '7', LSApplicationCategoryType: 'public.app-category.utilities', LSMinimumSystemVersion: '12.0', ITSAppUsesNonExemptEncryption: false }, entitlements: masEntitlements({ teamId: 'ABCDE12345', bundleId: 'com.example.demo' }), hasProfile: true, leaks: [] }; const o = { bundleId: 'com.example.demo', version: '1.2.3', build: '7', signed: true, pathFree: true }; test('a good app passes', () => { expect(appProblems(good, o)).toEqual({ errors: [], warnings: [] }); }); test('missing sandbox, category, min OS, profile, wrong build', () => { const bad = { ...good, info: { ...good.info, CFBundleVersion: '6', LSApplicationCategoryType: undefined, LSMinimumSystemVersion: undefined } as never, entitlements: { 'com.apple.security.app-sandbox': false }, hasProfile: false }; const e = appProblems(bad, o).errors.join('\n'); for (const s of ['CFBundleVersion is 6', 'LSApplicationCategoryType', 'LSMinimumSystemVersion', 'app-sandbox is not true', 'application-identifier', 'embedded.provisionprofile']) expect(e).toContain(s); expect(appProblems({ ...good, entitlements: null }, o).errors[0]).toContain('no entitlements'); }); test('get-task-allow refused; unsigned skips entitlement checks', () => { expect(appProblems({ ...good, entitlements: { ...good.entitlements!, 'com.apple.security.get-task-allow': true } }, o).errors.join()).toContain('get-task-allow'); expect(appProblems({ ...good, entitlements: null, hasProfile: false }, { ...o, signed: false }).errors).toEqual([]); }); test('path leaks are errors with --path-free, warnings without', () => { const leaks = [{ file: 'Contents/MacOS/bin', needle: '/Users/you', count: 3 }]; expect(appProblems({ ...good, leaks }, o).errors[0]).toContain('local paths'); const w = appProblems({ ...good, leaks }, { ...o, pathFree: false }); expect(w.errors).toEqual([]); expect(w.warnings.join()).toContain('--path-free'); }); test('pkgutil --check-signature', () => { const ok = parsePkgSignature('Package "a.pkg":\n Status: signed by a certificate trusted by macOS\n Certificate Chain:\n 1. 3rd Party Mac Developer Installer: Co (ABCDE12345)\n Expires: 2027'); expect(ok).toMatchObject({ signed: true, installerCert: true, certificate: '3rd Party Mac Developer Installer: Co (ABCDE12345)' }); expect(pkgProblems(ok)).toEqual([]); expect(pkgProblems(parsePkgSignature('Package "a.pkg":\n Status: no signature'))[0]).toContain('not signed'); const dev = parsePkgSignature(' Status: signed by a certificate trusted by macOS\n 1. Developer ID Installer: Co (ABCDE12345)'); expect(pkgProblems(dev)[0]).toContain('not a "3rd Party Mac Developer Installer"'); }); test('PackageInfo', () => { const x = ''; expect(parsePackageInfo(x)).toEqual({ bundleId: 'com.example.demo', version: '1.2.3', build: '7' }); expect(parsePackageInfo('')).toBeNull(); }); }); describe('plan', () => { const t = findTauri(fixture()); const base = { tauri: t, tauriCmd: ['cargo', 'tauri'], target: 'universal-apple-darwin', out: '/o', configFile: '/o/c.json', entitlementsFile: '/o/e', profile: '/o/p', appIdentity: 'Apple Distribution: Co (ABCDE12345)', installerIdentity: '3rd Party Mac Developer Installer: Co (ABCDE12345)', version: '1.2.3', build: '7', unsigned: false, pathFree: true, pkg: '/o/Demo App.pkg' }; test('signed steps: tauri app bundle, productbuild with the installer identity', () => { const s = masSteps(base); expect(s.tauri).toEqual(['cargo', 'tauri', 'build', '--bundles', 'app', '--target', 'universal-apple-darwin', '--config', '/o/c.json']); expect(s.productbuild.slice(0, 2)).toEqual(['productbuild', '--component']); expect(s.productbuild).toContain('--sign'); expect(s.productbuild.at(-1)).toBe('/o/Demo App.pkg'); expect(s.pkgCheck).toEqual(['pkgutil', '--check-signature', '/o/Demo App.pkg']); }); test('unsigned: no --sign', () => { expect(masSteps({ ...base, unsigned: true }).productbuild).not.toContain('--sign'); }); test('tauri overlay: identity, entitlements, embedded profile, bundleVersion', () => { const o = tauriOverlay({ ...base, minOs: '12.0' }) as { version: string; bundle: { macOS: Record } }; expect(o.version).toBe('1.2.3'); expect(o.bundle.macOS.bundleVersion).toBe('7'); expect(o.bundle.macOS.signingIdentity).toBe(base.appIdentity); expect(o.bundle.macOS.hardenedRuntime).toBe(true); expect(o.bundle.macOS.files).toEqual({ 'embedded.provisionprofile': '/o/p' }); const u = tauriOverlay({ ...base, unsigned: true }) as { bundle: { macOS: Record } }; expect(u.bundle.macOS.signingIdentity).toBeUndefined(); }); }); describe('build upload of a pkg', () => { const info = { bundleId: 'com.example.demo', version: '1.2.3', build: '7' }; test('platform MAC_OS and the pkg uti; iOS unchanged', () => { const m = buildUploadReqs('app1', info, 'Demo.pkg', 10, 'MAC_OS'); expect((m.upload.body.data.attributes as Record).platform).toBe('MAC_OS'); expect((m.file('u1').body.data.attributes as Record).uti).toBe('com.apple.pkg'); const i = buildUploadReqs('app1', info, 'Demo.ipa', 10); expect((i.upload.body.data.attributes as Record).platform).toBe('IOS'); expect((i.file('u1').body.data.attributes as Record).uti).toBe('com.apple.ipa'); }); test('altool gets --type macos for a pkg', () => { const k = { keyPath: 'k', keyId: 'i', issuerId: 's' }; expect(altoolArgs('/o/Demo.pkg', k)).toContain('macos'); expect(altoolArgs('/o/Demo.ipa', k)).not.toContain('--type'); }); });