Game Center switch before submit: apple submit / resubmit and signing ensure create gameCenterDetails and gameCenterAppVersions when Game Center is in use (config, entitlements or the built IPA), shown in the dry run; validate blocks on either missing; fake 409 BUILD_INDICATES_GAME_CENTER_ENABLED until both exist; pitfall 89
This commit is contained in:
parent
dd9e059036
commit
163e666834
9 changed files with 152 additions and 26 deletions
|
|
@ -11,6 +11,7 @@ import { verifyText } from '../cli/lib/console-verify';
|
|||
import { gameServicesFindings, type GameServicesStatus } from '../cli/games-status';
|
||||
import { run as gameCenter } from '../cli/apple/game-center';
|
||||
import { run as playGames } from '../cli/play/games';
|
||||
import { run as appleSubmit } from '../cli/apple/submit';
|
||||
import { upsertTiming } from '../cli/lib/upsert';
|
||||
import { ascEnv, BASE, capture, playEnv, project, restore } from './helpers/store-mock';
|
||||
import { installStores, type Stores, uninstallStores, writes } from './helpers/fake-stores';
|
||||
|
|
@ -202,9 +203,47 @@ describe('validate findings', () => {
|
|||
findings: [{ store: 'apple', level: 'error', id: 'gc.entitlement', msg: 'no entitlement' }], links: [],
|
||||
};
|
||||
const f = gameServicesFindings(st);
|
||||
expect(f.filter(x => x.level === 'blocker').map(x => x.code)).toEqual(['gc.entitlement', 'game-center.capability', 'game-center.detail']);
|
||||
expect(f.filter(x => x.level === 'blocker').map(x => x.code)).toEqual(['gc.entitlement', 'game-center.capability', 'game-center.detail', 'game-center.version']);
|
||||
expect(f.find(x => x.code === 'game-center.detail')!.msg).toContain('BUILD_INDICATES_GAME_CENTER_ENABLED');
|
||||
expect(f.map(x => x.code)).toContain('game-center.sync');
|
||||
expect(f.find(x => x.code === 'play-games')!.level).toBe('ok');
|
||||
expect(f.find(x => x.code === 'play-games.achievements')!.level).toBe('warn');
|
||||
});
|
||||
});
|
||||
|
||||
describe('apple submit with a Game Center build (live 409 STATE_ERROR.BUILD_INDICATES_GAME_CENTER_ENABLED)', () => {
|
||||
const fresh = () => {
|
||||
s.asc.gameCenterBuild = true;
|
||||
s.asc.put({ type: 'appStoreVersions', id: 'ver-1', attributes: { versionString: '1.0.0', appVersionState: 'PREPARE_FOR_SUBMISSION', platform: 'IOS' }, rels: { app: APP } });
|
||||
s.asc.put({ type: 'builds', id: 'b-8', attributes: { version: '8', processingState: 'VALID', usesNonExemptEncryption: false }, rels: { app: APP } });
|
||||
};
|
||||
test('the fake refuses the submit like App Store Connect while the switch is missing', async () => {
|
||||
fresh();
|
||||
const r = await capture(() => appleSubmit(project(BASE).ctx(['--build', '8', '--yes'])).catch(e => { console.error(String(e)); }));
|
||||
expect(r.err + r.out).toContain('BUILD_INDICATES_GAME_CENTER_ENABLED');
|
||||
expect(s.asc.all('reviewSubmissions').map(x => x.attributes.state)).not.toContain('WAITING_FOR_REVIEW');
|
||||
});
|
||||
test('dry run names the switch; --yes creates gameCenterDetails and gameCenterAppVersions before the submit, then passes', async () => {
|
||||
fresh();
|
||||
const p = project(GAME);
|
||||
const dry = await run2(() => appleSubmit(p.ctx(['--build', '8'])));
|
||||
expect(dry.writes).toEqual([]);
|
||||
expect(dry.out).toContain('would POST /v1/gameCenterDetails');
|
||||
expect(dry.out).toContain('would POST /v1/gameCenterAppVersions');
|
||||
const r = await run2(() => appleSubmit(p.ctx(['--build', '8', '--yes'])));
|
||||
const i = r.writes.indexOf('POST /v1/gameCenterDetails'), j = r.writes.indexOf('POST /v1/gameCenterAppVersions'), k = r.writes.findIndex(w => /^PATCH \/v1\/reviewSubmissions\//.test(w));
|
||||
expect(i).toBeGreaterThanOrEqual(0);
|
||||
expect(j).toBeGreaterThan(i);
|
||||
expect(k).toBeGreaterThan(j);
|
||||
expect(s.asc.all('reviewSubmissions').map(x => x.attributes.state)).toEqual(['WAITING_FOR_REVIEW']);
|
||||
expect(s.asc.all('gameCenterDetails')).toHaveLength(1);
|
||||
});
|
||||
test('apple game-center first, then submit: no second switch', async () => {
|
||||
fresh();
|
||||
const p = project(GAME);
|
||||
await run2(() => gameCenter(p.ctx(['--yes'])));
|
||||
const r = await run2(() => appleSubmit(p.ctx(['--build', '8', '--yes'])));
|
||||
expect(r.writes.filter(w => /gameCenter(Details|AppVersions)$/.test(w))).toEqual([]);
|
||||
expect(s.asc.all('gameCenterAppVersions')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -134,6 +134,9 @@ export class FakeAsc {
|
|||
lag: Record<string, number> = {};
|
||||
/** every uploaded part, by asset id */
|
||||
parts: string[] = [];
|
||||
/** The attached builds carry com.apple.developer.game-center: submitting answers Apple's 409 (seen live) until the
|
||||
* app has a gameCenterDetail and the version a gameCenterAppVersion. */
|
||||
gameCenterBuild = false;
|
||||
|
||||
constructor(readonly appId = '6400000101') {
|
||||
this.put({ type: 'apps', id: appId, attributes: { bundleId: 'com.example.notes', name: 'Example Notes' }, rels: {} });
|
||||
|
|
@ -335,6 +338,13 @@ export class FakeAsc {
|
|||
// seen live (2026-10-12): a REJECTED item that is not marked resolved blocks every submit with this 409, and an
|
||||
// empty draft cannot be canceled
|
||||
const items = this.children(self.id, 'reviewSubmissionItems');
|
||||
if (attrs.submitted && this.gameCenterBuild) {
|
||||
const app = String(self.rels.app);
|
||||
const ver = items.map(it => String(it.rels.appStoreVersion ?? '')).find(Boolean);
|
||||
if (!this.children(app, 'gameCenterDetails').length || (ver && !this.children(ver, 'gameCenterAppVersions').length))
|
||||
return json({ errors: [{ status: '409', code: 'STATE_ERROR.BUILD_INDICATES_GAME_CENTER_ENABLED', title: 'The request cannot be fulfilled because of the state of another resource.',
|
||||
detail: 'The build for this app store version has com.apple.developer.game-center entitlement but appStoreVersion is missing gameCenterConfigurations relationship.' }] }, 409);
|
||||
}
|
||||
if (attrs.submitted && items.some(it => it.attributes.state === 'REJECTED'))
|
||||
return json({ errors: [{ status: '409', code: 'STATE_ERROR', title: 'The request cannot be fulfilled because of the state of another resource.', detail: 'Version is not ready to be submitted yet, please try again later' }] }, 409);
|
||||
if (attrs.canceled && self.attributes.state === 'READY_FOR_REVIEW' && !items.length)
|
||||
|
|
|
|||
|
|
@ -86,6 +86,9 @@ describe('apple signing ensure', () => {
|
|||
{ 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' }]) },
|
||||
// GAME_CENTER is wanted: ensure reads the app's Game Center switch (on here; the editable version list is empty)
|
||||
{ match: /^\/v1\/apps\/6400000101\/gameCenterDetail/, answer: { data: { type: 'gameCenterDetails', id: 'GCD0001' } } },
|
||||
{ match: /^\/v1\/apps\/6400000101\/appStoreVersions/, answer: page([]) },
|
||||
{ method: 'POST', match: /^\/v1\/bundleIdCapabilities$/, answer: (c: { body: unknown }) => ({ data: cap((c.body as { data: { attributes: { capabilityType: string } } }).data.attributes.capabilityType) }) },
|
||||
];
|
||||
|
||||
|
|
@ -103,6 +106,17 @@ describe('apple signing ensure', () => {
|
|||
expect(calls.filter(c => c.method !== 'GET')).toEqual([]);
|
||||
});
|
||||
|
||||
test('GAME_CENTER without the app\'s Game Center detail: the dry run plans gameCenterDetails too (the submit 409 otherwise)', async () => {
|
||||
ascEnv();
|
||||
const p = project(cfg('automatic'));
|
||||
const rs = routes(['IN_APP_PURCHASE']);
|
||||
rs.unshift({ match: /^\/v1\/apps\/6400000101\/gameCenterDetail/, answer: { errors: [{ status: '404' }] } as never, status: 404 } as never);
|
||||
const calls = mockFetch(rs);
|
||||
const r = await capture(() => signing(p.ctx(['ensure', '--dry'])));
|
||||
expect(r.out + r.err).toContain('would POST /v1/gameCenterDetails');
|
||||
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'));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue