apple testflight distribute: a build's groups read per group (GET /v1/betaGroups/{id}/relationships/builds) instead of GET /v1/betaGroups?filter[builds]=, which App Store Connect refuses with 400 although the spec declares it; internal all-builds groups reported as already available and never sent the build; a re-run adds nothing; configured groups missing on App Store Connect point at groups sync, unknown ones list what exists; APPLE_LIVE_REJECTED (refused by storebound api too), tests/asc-query-guard.test.ts checks every App Store Connect GET with a query in cli/ against the spec's declared parameters, and the fake App Store Connect answers 400 to undeclared or live-refused parameters (pitfall 42)

This commit is contained in:
CRTkafa 2026-10-13 06:03:16 +08:00
parent f741c15829
commit bba46b5c7f
13 changed files with 354 additions and 14 deletions

View file

@ -98,6 +98,53 @@ describe('apple testflight groups: bare lists, sync creates', () => {
});
});
describe('apple testflight distribute (real use 2026-10-13)', () => {
const TF: ShipConfig = { ...CONFIG, apple: { ...CONFIG.apple!, testflight: { groups: [{ name: 'Team', internal: true, allBuilds: true }, { name: 'Friends' }, { name: 'Devs', internal: true }] } as never } };
const groupsAndBuild = () => {
s.asc.put({ type: 'betaGroups', id: 'grp-team', attributes: { name: 'Team', isInternalGroup: true, hasAccessToAllBuilds: true }, rels: { app: APP } });
s.asc.put({ type: 'betaGroups', id: 'grp-friends', attributes: { name: 'Friends', isInternalGroup: false }, rels: { app: APP } });
s.asc.put({ type: 'betaGroups', id: 'grp-devs', attributes: { name: 'Devs', isInternalGroup: true, hasAccessToAllBuilds: false }, rels: { app: APP } });
s.asc.put({ type: 'builds', id: 'b-1', attributes: { version: '1', processingState: 'VALID', usesNonExemptEncryption: false, expired: false }, rels: { app: APP } });
};
test('live shape: GET /v1/betaGroups?filter[builds]= is a 400 (the old lookup fails here as it did live)', async () => {
const r = s.asc.handle('GET', `/v1/betaGroups?filter[app]=${APP}&filter[builds]=b-1&limit=200`, undefined);
expect(r.status).toBe(400);
expect(JSON.stringify(await r.json())).toContain('PARAMETER_ERROR.ILLEGAL');
});
test('an internal all-builds group: reported as already available, nothing added', async () => {
groupsAndBuild();
const p = project(TF);
const r = await twice(() => appleTestflight(p.ctx(['distribute', '--build', '1', '--group', 'Team', '--yes'])));
expect(r.first).toEqual([]);
expect(r.second).toEqual([]);
const out = await capture(() => appleTestflight(p.ctx(['distribute', '--build', '1', '--group', 'Team', '--yes'])));
expect(out.out).toContain('already available to Team (internal, all builds)');
expect(s.calls.some(c => /filter\[builds\]/.test(decodeURIComponent(c.path)))).toBe(false);
});
test('an internal group without all builds and an external group: added once, the re-run adds nothing', async () => {
groupsAndBuild();
const p = project(TF);
const r = await twice(() => appleTestflight(p.ctx(['distribute', '--build', '1', '--group', 'Team,Devs,Friends', '--yes'])));
expect(r.first).toEqual(['POST /v1/betaGroups/grp-devs/relationships/builds', 'POST /v1/betaGroups/grp-friends/relationships/builds']);
expect(r.second).toEqual([]);
const out = await capture(() => appleTestflight(p.ctx(['distribute', '--build', '1', '--group', 'Devs', '--yes'])));
expect(out.out).toContain('already in Devs (internal)');
});
test('a group in the config but not on App Store Connect points at groups sync; an unknown one lists what exists', async () => {
const p = project(TF);
s.asc.put({ type: 'builds', id: 'b-1', attributes: { version: '1', processingState: 'VALID' }, rels: { app: APP } });
await expect(capture(() => appleTestflight(p.ctx(['distribute', '--build', '1', '--group', 'Team', '--yes']))))
.rejects.toThrow('group "Team" is in the config but not on App Store Connect yet: create it with `storebound apple testflight groups sync`');
s.asc.put({ type: 'betaGroups', id: 'grp-friends', attributes: { name: 'Friends', isInternalGroup: false }, rels: { app: APP } });
await expect(capture(() => appleTestflight(p.ctx(['distribute', '--build', '1', '--group', 'Nope', '--yes']))))
.rejects.toThrow('unknown group "Nope": App Store Connect has "Friends"');
});
});
describe('App Store Connect: a second run writes nothing', () => {
test('apple iap: product, localizations, price schedule, availability once', async () => {
const p = project(CONFIG);