// Bundle id capabilities from real use (STAR DASH 1.0.1, October 2026): automatic signing must not create profiles or // certificates, a capability write is read back (lag, never sticking), the App ID / config / project entitlements // comparison, unused storebound profiles, and the `api` write message naming the dedicated command. Answers follow // the live bundleIds?include=bundleIdCapabilities shape (capability id = _, settings null); every // id is a placeholder. import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; import { run as signing } from '../cli/apple/signing'; import { compareCapabilities, entitlementKeys, ensureScope, projectEntitlements, unusedStoreboundProfiles, verifyCapabilities } from '../cli/lib/signing'; import { dedicatedCommand, decide } from '../cli/api/gate'; import { upsertTiming } from '../cli/lib/upsert'; import { ascEnv, BASE, capture, mockFetch, project, restore } from './helpers/store-mock'; const BID = 'BUNDLE0001'; const cap = (t: string) => ({ type: 'bundleIdCapabilities', id: `${BID}_${t}`, attributes: { settings: null, capabilityType: t } }); const bundles = (types: string[]) => ({ data: [{ type: 'bundleIds', id: BID, attributes: { identifier: 'com.example.notes', platform: 'UNIVERSAL', seedId: 'ABCDE12345' }, relationships: { bundleIdCapabilities: { data: types.map(t => ({ type: 'bundleIdCapabilities', id: `${BID}_${t}` })) } } }], included: types.map(cap), links: { self: '' }, meta: { paging: { total: 1, limit: 200 } }, }); const page = (data: unknown[]) => ({ data, links: { self: '' }, meta: { paging: { total: data.length, limit: 200 } } }); const sbProfile = { type: 'profiles', id: 'PROF0001', attributes: { name: 'storebound com.example.notes App Store 2026-10-14 10:45', profileType: 'IOS_APP_STORE', profileState: 'ACTIVE', expirationDate: '2027-09-15T01:29:54.000+00:00', uuid: 'profile-uuid-1' } }; const ENT = ` aps-environmentdevelopment com.apple.developer.game-center com.apple.developer.applesignin `; const PBX = (ref: boolean) => `// !$*UTF8*$!\n{ buildSettings = { CODE_SIGN_STYLE = Automatic; ${ref ? 'CODE_SIGN_ENTITLEMENTS = App/App.entitlements;' : ''} }; }\n`; beforeEach(() => { upsertTiming.waitMs = 0; }); afterEach(() => { restore(); upsertTiming.waitMs = 3000; }); describe('entitlements and the three-way comparison', () => { test('entitlementKeys skips comments and keys', () => { expect(entitlementKeys(ENT)).toEqual(['aps-environment', 'com.apple.developer.game-center']); expect(projectEntitlements('App/App.entitlements', ENT, true).caps.map(c => c.cap)).toEqual(['PUSH_NOTIFICATIONS', 'GAME_CENTER']); }); test('names what is missing where', () => { const proj = [projectEntitlements('ios/App/App/App.entitlements', ENT, true)]; const m = compareCapabilities({ appId: ['IN_APP_PURCHASE'], config: ['IN_APP_PURCHASE', 'PUSH_NOTIFICATIONS'], project: proj }); const s = m.map(x => `${x.cap}:${x.missingIn}`); expect(s).toContain('PUSH_NOTIFICATIONS:app-id'); // config wants it, App ID lacks it expect(s).toContain('GAME_CENTER:app-id'); // entitlement without App ID capability expect(s).toContain('GAME_CENTER:config'); // entitlement not in the config expect(s).not.toContain('IN_APP_PURCHASE:project'); // IAP needs no entitlement expect(m.find(x => x.cap === 'GAME_CENTER' && x.missingIn === 'app-id')!.msg).toContain('com.apple.developer.game-center'); }); test('config capabilities without entitlements, and an entitlements file the project does not reference', () => { const none = compareCapabilities({ appId: ['IN_APP_PURCHASE', 'PUSH_NOTIFICATIONS', 'GAME_CENTER'], config: ['IN_APP_PURCHASE', 'PUSH_NOTIFICATIONS', 'GAME_CENTER'], project: [] }); expect(none.map(x => `${x.cap}:${x.missingIn}`)).toEqual(['PUSH_NOTIFICATIONS:project', 'GAME_CENTER:project']); expect(none[0]!.msg).toContain('aps-environment'); const unref = compareCapabilities({ appId: ['PUSH_NOTIFICATIONS', 'GAME_CENTER'], config: ['PUSH_NOTIFICATIONS', 'GAME_CENTER'], project: [projectEntitlements('App/App.entitlements', ENT, false)] }); expect(unref.map(x => x.msg).join('\n')).toContain('not referenced by CODE_SIGN_ENTITLEMENTS'); expect(compareCapabilities({ appId: ['PUSH_NOTIFICATIONS', 'GAME_CENTER'], config: ['PUSH_NOTIFICATIONS', 'GAME_CENTER'], project: [projectEntitlements('App/App.entitlements', ENT, true)] })).toEqual([]); expect(compareCapabilities({ appId: ['IN_APP_PURCHASE'], config: ['GAME_CENTER'], project: null }).map(x => x.missingIn)).toEqual(['app-id']); }); }); describe('read-back after a capability write', () => { test('lag: the third read has it', async () => { const reads = [['IN_APP_PURCHASE'], ['IN_APP_PURCHASE'], ['IN_APP_PURCHASE', 'GAME_CENTER']]; const r = await verifyCapabilities(async () => reads.shift() ?? [], ['IN_APP_PURCHASE', 'GAME_CENTER'], { waitMs: 0, sleep: async () => {} }); expect(r).toEqual({ have: ['IN_APP_PURCHASE', 'GAME_CENTER'], missing: [], reads: 3 }); }); test('stops at the timeout', async () => { const r = await verifyCapabilities(async () => ['IN_APP_PURCHASE'], ['GAME_CENTER'], { tries: 50, waitMs: 1000, timeoutMs: 1500, sleep: async () => {} }); expect(r.missing).toEqual(['GAME_CENTER']); expect(r.reads).toBeLessThan(50); }); }); describe('apple signing ensure', () => { const cfg = (style?: 'automatic' | 'manual') => ({ ...BASE, apple: { ...BASE.apple!, signing: { ...(style ? { style } : {}), capabilities: ['IN_APP_PURCHASE' as const, 'PUSH_NOTIFICATIONS' as const, 'GAME_CENTER' as const] } } }); const routes = (after: string[]) => [ { match: /^\/v1\/bundleIds\?filter\[identifier\]=com\.example\.notes/, answer: bundles(['IN_APP_PURCHASE']), times: 1 }, { match: /^\/v1\/bundleIds\?filter\[identifier\]=com\.example\.notes/, answer: bundles(after) }, { match: /^\/v1\/certificates\?/, answer: page([]) }, { match: /^\/v1\/bundleIds\/BUNDLE0001\/profiles/, answer: page([sbProfile]) }, { match: /^\/v1\/profiles\/PROF0001\/relationships\/certificates/, answer: page([{ type: 'certificates', id: 'CERT0001' }]) }, { method: 'POST', match: /^\/v1\/bundleIdCapabilities$/, answer: (c: { body: unknown }) => ({ data: cap((c.body as { data: { attributes: { capabilityType: string } } }).data.attributes.capabilityType) }) }, ]; test('automatic: --dry plans only the capabilities and says why; lists the unused storebound profile with its delete command', async () => { ascEnv(); const p = project(cfg(), { 'ios/App/App.xcodeproj/project.pbxproj': PBX(true), 'ios/App/App/App.entitlements': ENT }); const calls = mockFetch(routes(['IN_APP_PURCHASE'])); const r = await capture(() => signing(p.ctx(['ensure', '--dry']))); const all = r.out + r.err; expect(all).toContain('automatic signing: ensure only keeps the App ID'); expect(all).toContain('would POST /v1/bundleIdCapabilities'); expect(all).not.toMatch(/POST \/v1\/(profiles|certificates)/); expect(all).toContain('storebound api apple profiles_deleteInstance --param id=PROF0001'); expect(all).toContain('PUSH_NOTIFICATIONS is in apple.signing.capabilities but not on the App ID'); expect(calls.filter(c => c.method !== 'GET')).toEqual([]); }); test('automatic: a real run adds capabilities, reads them back and creates no certificate or profile', async () => { ascEnv(); const p = project(cfg('automatic')); const calls = mockFetch(routes(['IN_APP_PURCHASE', 'PUSH_NOTIFICATIONS', 'GAME_CENTER'])); const r = await capture(() => signing(p.ctx(['ensure', '--yes']))); expect(r.exit).toBeNull(); const writes = calls.filter(c => c.method !== 'GET').map(c => `${c.method} ${c.path}`); expect(writes).toEqual(['POST /v1/bundleIdCapabilities', 'POST /v1/bundleIdCapabilities']); expect(r.out + r.err).toContain('read back: the App ID has IN_APP_PURCHASE, PUSH_NOTIFICATIONS, GAME_CENTER'); }); test('a write that never sticks fails with the API answers', async () => { ascEnv(); const p = project(cfg()); mockFetch(routes(['IN_APP_PURCHASE'])); const r = await capture(() => signing(p.ctx(['ensure', '--yes']))).catch(e => ({ out: '', err: String(e), exit: 1 })); expect(r.err).toContain('capability write did not stick'); expect(r.err).toContain('PUSH_NOTIFICATIONS, GAME_CENTER'); expect(r.err).toContain(`${BID}_GAME_CENTER`); // the POST answers are in the message }); }); describe('scope and leftovers', () => { test('ensureScope', () => { expect(ensureScope(undefined)).toMatchObject({ certificates: false, profiles: false }); expect(ensureScope('manual')).toMatchObject({ certificates: true, profiles: true }); }); test('unusedStoreboundProfiles keeps Xcode-managed and in-use ones', () => { const managed = { id: 'X', attributes: { name: 'iOS Team Store Provisioning Profile: com.example.notes' }, managed: true }; expect(unusedStoreboundProfiles([sbProfile, managed], new Set()).map(p => p.id)).toEqual(['PROF0001']); expect(unusedStoreboundProfiles([sbProfile], new Set(['PROF0001']))).toEqual([]); }); }); describe('storebound api writes outside a terminal', () => { test('the dry reason says TTY by design and the dedicated command is named', () => { const d = decide({ kind: 'write', method: 'POST', dry: false, yes: true, getOnly: false, tty: false, agent: false }); expect(d).toMatchObject({ action: 'dry' }); expect((d as { why: string }).why).toContain('by design'); expect(dedicatedCommand('apple', 'bundleIdCapabilities_createInstance')).toContain('apple signing ensure'); expect(dedicatedCommand('apple', 'apps_getInstance')).toBeNull(); }); });