313 lines
23 KiB
TypeScript
313 lines
23 KiB
TypeScript
// Game Center and Play Games Services: config validation, the capability ensure, Game Center enable + read-back, the
|
|
// leaderboard / achievement upsert on both stores (create, update, no-op) against the fake stores, the entitlement
|
|
// and APP_ID checks, the PGS console plan's start URLs and the validate findings. Nothing reaches the network.
|
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
|
|
import type { ShipConfig } from '../cli/lib/config';
|
|
import { validateConfig } from '../cli/lib/config';
|
|
import { capabilitySpecs, planCapabilities } from '../cli/lib/signing';
|
|
import { gamesPrivacyFindings, gcFindings, gcLocaleMap, liveGcLocales, suggestLocaleMap, pgsFindings, planGameCenter, playAchievementBody, playLeaderboardBody, readPgsProject, validateGames } from '../cli/lib/game-services';
|
|
import { consolePlan } from '../cli/lib/console-plan';
|
|
import { verifyText } from '../cli/lib/console-verify';
|
|
import { gameItemRows, 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';
|
|
|
|
let s: Stores;
|
|
beforeAll(() => { ascEnv(); playEnv(); });
|
|
afterAll(restore);
|
|
beforeEach(() => { upsertTiming.waitMs = 0; s = installStores(); });
|
|
afterEach(() => { uninstallStores(); });
|
|
|
|
const APP = '6400000101';
|
|
const PGS = '123456789012';
|
|
const GAME: ShipConfig = {
|
|
...BASE,
|
|
app: { ...BASE.app, profile: 'game' },
|
|
languages: [{ id: 'en', apple: 'en-US', play: 'en-US' }, { id: 'tr', apple: 'tr', play: 'tr-TR' }],
|
|
games: {
|
|
gameCenter: true,
|
|
playGames: { appId: PGS },
|
|
leaderboards: [{ id: 'com.example.game.highscore', locales: { en: { name: 'High score', suffix: ' points' }, tr: { name: 'En yüksek skor' } } }],
|
|
achievements: [{ id: 'com.example.game.first-win', points: 10, locales: { en: { name: 'First win', description: 'Win a round.' } } },
|
|
{ id: 'com.example.game.secret', points: 20, hidden: true, locales: { en: { name: 'Secret', description: 'Find the secret room.', earned: 'You found it.' } } }],
|
|
},
|
|
};
|
|
|
|
const run2 = async (fn: () => Promise<void>) => {
|
|
const a = s.calls.length;
|
|
const r = await capture(fn);
|
|
if (r.exit) throw new Error(`exit ${r.exit}\n${r.out}\n${r.err}`);
|
|
return { ...r, writes: writes(s.calls.slice(a)).map(c => `${c.method} ${decodeURIComponent(c.path).replace(/\?.*/, '')}`) };
|
|
};
|
|
|
|
describe('config', () => {
|
|
test('a valid games section passes; limits and ids are checked', () => {
|
|
expect(validateConfig(GAME)).toEqual([]);
|
|
const bad = validateGames({ gameCenter: true, playGames: {}, achievements: [{ id: 'a', points: 7, locales: { en: { name: 'A', description: 'x' } } }, { id: 'a', points: 101, locales: {} }] }, GAME.languages);
|
|
expect(bad.join('\n')).toContain('not a multiple of 5');
|
|
expect(bad.join('\n')).toContain('used twice');
|
|
expect(bad.join('\n')).toContain('points 0-100');
|
|
expect(validateGames({ leaderboards: [{ id: 'x', locales: { de: { name: 'X' } } }] }, GAME.languages).join()).toContain('not a language id');
|
|
});
|
|
test('Play bodies: number format, hidden, suffix per language', () => {
|
|
expect(playLeaderboardBody({ id: 't', format: 'ELAPSED_TIME_SECOND', sort: 'low', locales: { en: { name: 'Time' } } }, GAME.languages)).toMatchObject({ scoreOrder: 'SMALLER_IS_BETTER', draft: { scoreFormat: { numberFormatType: 'TIME_DURATION' } } });
|
|
expect(playAchievementBody(GAME.games!.achievements![1]!, GAME.languages)).toMatchObject({ initialState: 'HIDDEN', achievementType: 'STANDARD', draft: { pointValue: 20 } });
|
|
});
|
|
});
|
|
|
|
describe('capability ensure', () => {
|
|
test('games.gameCenter adds GAME_CENTER to what signing ensure wants, once', () => {
|
|
expect(capabilitySpecs(undefined, true)).toEqual([{ type: 'GAME_CENTER' }]);
|
|
expect(capabilitySpecs({ capabilities: ['GAME_CENTER', 'IN_APP_PURCHASE'] }, true).map(c => c.type)).toEqual(['GAME_CENTER', 'IN_APP_PURCHASE']);
|
|
const p = planCapabilities('bid-1', ['IN_APP_PURCHASE'], capabilitySpecs({ capabilities: ['IN_APP_PURCHASE'] }, true));
|
|
expect(p.create.map(r => (r.body as { data: { attributes: { capabilityType: string } } }).data.attributes.capabilityType)).toEqual(['GAME_CENTER']);
|
|
expect(planCapabilities('bid-1', ['GAME_CENTER'], capabilitySpecs(undefined, true)).create).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('apple game-center (fake App Store Connect)', () => {
|
|
const version = () => s.asc.put({ type: 'appStoreVersions', id: 'ver-1', attributes: { versionString: '1.0.0', appVersionState: 'PREPARE_FOR_SUBMISSION', platform: 'IOS' }, rels: { app: APP } });
|
|
|
|
test('--dry writes nothing and names every create', async () => {
|
|
version();
|
|
const r = await run2(() => gameCenter(project(GAME).ctx(['--dry'])));
|
|
expect(r.writes).toEqual([]);
|
|
expect(r.out).toContain('POST /v1/gameCenterDetails');
|
|
expect(r.out).toContain('leaderboard com.example.game.highscore');
|
|
});
|
|
|
|
test('enable + version + create, read back; the re-run writes nothing; a changed text is one PATCH', async () => {
|
|
version();
|
|
const p = project(GAME);
|
|
const first = await run2(() => gameCenter(p.ctx(['--yes'])));
|
|
expect(first.writes).toEqual([
|
|
'POST /v1/gameCenterDetails', 'POST /v1/gameCenterAppVersions',
|
|
'POST /v1/gameCenterLeaderboards', 'POST /v1/gameCenterLeaderboardLocalizations', 'POST /v1/gameCenterLeaderboardLocalizations',
|
|
'POST /v1/gameCenterAchievements', 'POST /v1/gameCenterAchievementLocalizations',
|
|
'POST /v1/gameCenterAchievements', 'POST /v1/gameCenterAchievementLocalizations',
|
|
]);
|
|
expect(s.asc.all('gameCenterLeaderboards')[0]!.attributes).toMatchObject({ vendorIdentifier: 'com.example.game.highscore', scoreSortType: 'DESC', submissionType: 'BEST_SCORE', defaultFormatter: 'INTEGER' });
|
|
expect(s.asc.all('gameCenterAchievements')[1]!.attributes).toMatchObject({ showBeforeEarned: false, points: 20 });
|
|
expect(first.err).not.toContain('read back');
|
|
const again = await run2(() => gameCenter(p.ctx(['--yes'])));
|
|
expect(again.writes).toEqual([]);
|
|
expect(again.out).toContain('in step');
|
|
const changed: ShipConfig = { ...GAME, games: { ...GAME.games!, achievements: [{ ...GAME.games!.achievements![0]!, points: 15, locales: { en: { name: 'First victory', description: 'Win a round.' } } }, GAME.games!.achievements![1]!] } };
|
|
const upd = await run2(() => gameCenter(project(changed).ctx(['--yes'])));
|
|
expect(upd.writes).toEqual(['PATCH /v1/gameCenterAchievements/' + s.asc.all('gameCenterAchievements')[0]!.id, 'PATCH /v1/gameCenterAchievementLocalizations/' + s.asc.all('gameCenterAchievementLocalizations')[0]!.id]);
|
|
});
|
|
|
|
test('an existing detail with the leaderboard already there is kept (matched by vendor id, no duplicate)', () => {
|
|
const plan = planGameCenter(GAME.games!, GAME.languages, { leaderboards: [{ id: 'lb-1', attributes: { vendorIdentifier: 'com.example.game.highscore', referenceName: 'com.example.game.highscore', defaultFormatter: 'INTEGER', submissionType: 'BEST_SCORE', scoreSortType: 'DESC' },
|
|
localizations: [{ id: 'l1', attributes: { locale: 'en-US', name: 'High score', formatterSuffix: ' points' } }, { id: 'l2', attributes: { locale: 'tr', name: 'En yüksek skor' } }] }], achievements: [] });
|
|
expect(plan[0]).toMatchObject({ action: 'keep', locales: [{ action: 'keep' }, { action: 'keep' }] });
|
|
});
|
|
|
|
test('a lost create answer (5xx after the write) is not sent twice', async () => {
|
|
version();
|
|
s.faults.push({ method: 'POST', match: /\/v1\/gameCenterLeaderboards$/, when: 'after' });
|
|
const r = await run2(() => gameCenter(project(GAME).ctx(['--yes'])));
|
|
expect(r.writes.filter(w => w === 'POST /v1/gameCenterLeaderboards')).toHaveLength(1);
|
|
expect(s.asc.all('gameCenterLeaderboards')).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe('Game Center locale map', () => {
|
|
const live = { leaderboards: [{ id: 'lb-1', attributes: { vendorIdentifier: 'com.example.game.highscore', referenceName: 'com.example.game.highscore', defaultFormatter: 'INTEGER', submissionType: 'BEST_SCORE', scoreSortType: 'DESC' },
|
|
localizations: [{ id: 'l1', attributes: { locale: 'en-GB', name: 'High score', formatterSuffix: ' points' } }, { id: 'l2', attributes: { locale: 'tr', name: 'En yüksek skor' } }] }], achievements: [] };
|
|
const only = { ...GAME.games!, achievements: [] };
|
|
const sig = (p: ReturnType<typeof planGameCenter>) => p[0]!.locales.map(l => l.locale + ':' + l.action);
|
|
test('without a map the config en-US is created next to the live en-GB, and the suggestion names the map', () => {
|
|
expect(sig(planGameCenter(only, GAME.languages, live))).toEqual(['en-US:create', 'tr:keep']);
|
|
expect(suggestLocaleMap(GAME.languages, liveGcLocales(live))).toEqual([{ id: 'en', from: 'en-US', to: 'en-GB' }]);
|
|
});
|
|
test('--locale-map en=en-GB (or games.localeMap) leaves nothing to create or update, and no suggestion remains', () => {
|
|
const map = gcLocaleMap(only, ['en=en-GB']);
|
|
expect(map).toEqual({ en: 'en-GB' });
|
|
const plan = planGameCenter(only, GAME.languages, live, map);
|
|
expect(sig(plan)).toEqual(['en-GB:keep', 'tr:keep']);
|
|
expect(plan[0]!.action).toBe('keep');
|
|
expect(suggestLocaleMap(GAME.languages, liveGcLocales(live), map)).toEqual([]);
|
|
expect(gcLocaleMap({ ...only, localeMap: { en: 'en-AU' } }, ['en=en-GB,tr=tr'])).toEqual({ en: 'en-GB', tr: 'tr' });
|
|
expect(planGameCenter(only, GAME.languages, live, { 'en-US': 'en-GB' })[0]!.locales[0]!.locale).toBe('en-GB');
|
|
});
|
|
test('two English variants live: no guess; a bad map entry is refused', () => {
|
|
expect(suggestLocaleMap(GAME.languages, ['en-GB', 'en-AU', 'tr'])).toEqual([]);
|
|
expect(() => gcLocaleMap(only, ['en'])).toThrow('--locale-map');
|
|
expect(validateGames({ ...only, localeMap: { en: 'english' } }, GAME.languages).join()).toContain('games.localeMap.en');
|
|
});
|
|
test('apple game-center against the fake store: the dry run warns with the exact flag; with the flag nothing is written', async () => {
|
|
s.asc.put({ type: 'appStoreVersions', id: 'ver-1', attributes: { versionString: '1.0.0', appVersionState: 'PREPARE_FOR_SUBMISSION', platform: 'IOS' }, rels: { app: APP } });
|
|
const base = await run2(() => gameCenter(project(GAME).ctx(['--yes'])));
|
|
expect(base.writes.length).toBeGreaterThan(0);
|
|
for (const l of [...s.asc.all('gameCenterLeaderboardLocalizations'), ...s.asc.all('gameCenterAchievementLocalizations')]) if (l.attributes.locale === 'en-US') l.attributes.locale = 'en-GB';
|
|
const dry = await run2(() => gameCenter(project(GAME).ctx(['--dry'])));
|
|
expect(dry.out + dry.err).toContain('--locale-map en=en-GB');
|
|
const mapped = await run2(() => gameCenter(project(GAME).ctx(['--yes', '--locale-map', 'en=en-GB'])));
|
|
expect(mapped.writes).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('play games (fake Games Configuration API)', () => {
|
|
test('create, no-op re-run, update by name match; playId wins', async () => {
|
|
const p = project(GAME);
|
|
const first = await run2(() => playGames(p.ctx(['--yes'])));
|
|
expect(first.writes).toEqual([`POST /games/v1configuration/applications/${PGS}/leaderboards`, `POST /games/v1configuration/applications/${PGS}/achievements`, `POST /games/v1configuration/applications/${PGS}/achievements`]);
|
|
const lb = [...s.games.leaderboards.values()][0] as { draft: { name: { translations: { locale: string }[] }; scoreFormat: unknown } };
|
|
expect(lb.draft.name.translations.map(t => t.locale)).toEqual(['en-US', 'tr-TR']);
|
|
expect((await run2(() => playGames(p.ctx(['--yes'])))).writes).toEqual([]);
|
|
const id = [...s.games.achievements.keys()][0]!;
|
|
const changed: ShipConfig = { ...GAME, games: { ...GAME.games!, achievements: [{ ...GAME.games!.achievements![0]!, points: 15 }, GAME.games!.achievements![1]!] } };
|
|
expect((await run2(() => playGames(project(changed).ctx(['--yes'])))).writes).toEqual([`PUT /games/v1configuration/achievements/${id}`]);
|
|
expect((s.games.achievements.get(id) as { draft: { pointValue: number } }).draft.pointValue).toBe(15);
|
|
});
|
|
test('--dry writes nothing', async () => {
|
|
const r = await run2(() => playGames(project(GAME).ctx(['--dry'])));
|
|
expect(r.writes).toEqual([]);
|
|
expect(r.out).toContain('achievement com.example.game.first-win');
|
|
});
|
|
test('an insert with an unclear answer is not retried blindly', async () => {
|
|
s.faults.push({ method: 'POST', match: /\/leaderboards$/, when: 'after', status: 503 });
|
|
await run2(() => playGames(project(GAME).ctx(['--yes'])));
|
|
expect(s.games.leaderboards.size).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('play games with the Games Configuration API disabled', () => {
|
|
test('a 403 SERVICE_DISABLED names the API, the enable URL for the project and the console plan', async () => {
|
|
s.faults.push({ method: 'GET', match: /leaderboards/, when: 'before', status: 403, times: 5, code: 'SERVICE_DISABLED', detail: 'Google Play Game Services Publishing API has not been used in project 987654321 before or it is disabled. SERVICE_DISABLED accessNotConfigured' });
|
|
const err = await run2(() => playGames(project(GAME).ctx(['--dry']))).then(() => null, (e: Error) => e);
|
|
expect(err?.message).toContain('Google Play Game Services Publishing API');
|
|
expect(err?.message).toContain('gamesconfiguration.googleapis.com');
|
|
expect(err?.message).toContain('https://console.cloud.google.com/apis/library/gamesconfiguration.googleapis.com?project=987654321');
|
|
expect(err?.message).toContain('storebound console plan play-games-services');
|
|
});
|
|
test('the console plan has the enable step', () => {
|
|
const plan = consolePlan('play-games-services', GAME);
|
|
expect(JSON.stringify(plan)).toContain('gamesconfiguration.googleapis.com');
|
|
});
|
|
});
|
|
|
|
describe('project checks', () => {
|
|
const ENT = '<?xml version="1.0"?><plist><dict><key>com.apple.developer.game-center</key><true/></dict></plist>';
|
|
test('Game Center entitlement: missing, unreferenced, present, present without config', () => {
|
|
expect(gcFindings(GAME, [])[0]).toMatchObject({ level: 'error', id: 'gc.entitlement' });
|
|
expect(gcFindings(GAME, [{ file: 'ios/App/App.entitlements', caps: [{ cap: 'GAME_CENTER' }], referenced: false }])[0]!.msg).toContain('CODE_SIGN_ENTITLEMENTS');
|
|
expect(gcFindings(GAME, [{ file: 'ios/App/App.entitlements', caps: [{ cap: 'GAME_CENTER' }], referenced: true }])[0]!.level).toBe('ok');
|
|
expect(gcFindings(BASE, [{ file: 'x.entitlements', caps: [{ cap: 'GAME_CENTER' }], referenced: true }])[0]!.id).toBe('gc.config');
|
|
void ENT;
|
|
});
|
|
const MANIFEST = '<manifest><application><meta-data android:name="com.google.android.gms.games.APP_ID" android:value="@string/game_services_project_id"/></application></manifest>';
|
|
const GRADLE = 'dependencies { implementation "com.google.android.gms:play-services-games-v2:20.1.2" }';
|
|
test('APP_ID: resolved from games-ids.xml and compared with the config', () => {
|
|
const p = project(GAME, { 'android/app/build.gradle': GRADLE, 'android/app/src/main/AndroidManifest.xml': MANIFEST,
|
|
'android/app/src/main/res/values/games-ids.xml': `<resources><string name="game_services_project_id" translatable="false">${PGS}</string></resources>` });
|
|
const pgs = readPgsProject(p.root);
|
|
expect(pgs.appId).toEqual({ value: PGS, file: 'android/app/src/main/res/values/games-ids.xml' });
|
|
expect(pgsFindings(GAME, pgs).map(f => f.level)).toEqual(['ok']);
|
|
expect(pgsFindings({ games: { playGames: { appId: '999999999999' } } }, pgs)[0]).toMatchObject({ level: 'error', id: 'pgs.app-id' });
|
|
});
|
|
test('APP_ID: the dependency without the meta-data is an error; without config a warning', () => {
|
|
const p = project(BASE, { 'android/app/build.gradle': GRADLE, 'android/app/src/main/AndroidManifest.xml': '<manifest/>' });
|
|
const f = pgsFindings(BASE, readPgsProject(p.root));
|
|
expect(f.map(x => x.id)).toEqual(['pgs.config', 'pgs.manifest']);
|
|
});
|
|
test('player id reminders: App Privacy USER_ID and Data safety PSL_USER_ACCOUNT', () => {
|
|
const f = gamesPrivacyFindings({ ...GAME, dataSafety: { collectsData: true, data: {} } as never, appPrivacy: { types: {} } as never });
|
|
expect(f.map(x => x.id)).toEqual(['games.data-safety', 'games.app-privacy']);
|
|
expect(gamesPrivacyFindings({ ...GAME, dataSafety: { data: { PSL_USER_ACCOUNT: {} } } as never, appPrivacy: { types: { USER_ID: {} } } as never })).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('console plan play-games-services', () => {
|
|
test('deep link with ids, absolute Google Cloud links, Publish is the owner\'s', () => {
|
|
const p = consolePlan('play-games-services', { ...GAME, play: { ...GAME.play!, developerId: '4444', appId: '9876' } });
|
|
expect(p.start).toEqual({ url: 'https://play.google.com/console/u/0/developers/4444/app/9876/play-games-services/setup', deepLink: true, reach: [] });
|
|
const text = p.steps.map(x => x.text).join('\n');
|
|
expect(text).toContain('https://play.google.com/console/u/0/developers/4444/app/9876/keymanagement');
|
|
expect(text).toContain('https://console.cloud.google.com/apis/credentials');
|
|
expect(p.buttons.final).toBe('Publish');
|
|
expect(p.buttons.neverPress).toContain('Publish');
|
|
expect(p.ownerBatch.some(o => o.kind === 'final' && o.text.includes('Publish'))).toBe(true);
|
|
expect(p.askOwner.join()).toContain('Web OAuth client');
|
|
});
|
|
test('without ids: the console and how to reach it', () => {
|
|
const p = consolePlan('play-games-services', GAME);
|
|
expect(p.start.deepLink).toBe(false);
|
|
expect(p.start.url.startsWith('https://play.google.com/console')).toBe(true);
|
|
});
|
|
test('verify from the page text', () => {
|
|
expect(verifyText('play-games-services', GAME, `Project ID ${PGS}\nCredentials\nAndroid com.example.notes\nReady to publish`).ok).toBe(true);
|
|
expect(verifyText('play-games-services', GAME, 'Project ID 1\nNo credentials').mismatches).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('validate findings', () => {
|
|
test('missing capability and detail are blockers; in step is ok; fewer than 5 Play achievements warns', () => {
|
|
const st: GameServicesStatus = {
|
|
apple: { capability: false, enabled: false, version: '1.0.0', versionEnabled: false, leaderboards: 0, achievements: 0, config: { leaderboards: 1, achievements: 2 }, changes: ['leaderboard x: create'] },
|
|
play: { appId: PGS, resource: null, leaderboards: 1, achievements: 2, config: { leaderboards: 1, achievements: 2 }, changes: [] },
|
|
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', '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-0', attributes: { versionString: '0.9.0', appVersionState: 'READY_FOR_SALE', platform: 'IOS' }, rels: { app: APP } }); // an update, not a first release
|
|
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);
|
|
});
|
|
});
|
|
|
|
describe('games status rows (desktop Players section)', () => {
|
|
test('one row per item, store states from the plans, null when a store was not read', () => {
|
|
const g = { leaderboards: [{ id: 'com.example.lb', locales: { en: { name: 'Best' } } }], achievements: [{ id: 'com.example.a1', points: 10, hidden: true, platforms: ['android' as const], locales: { en: { name: 'First', description: 'd' } } }] };
|
|
const plan = [{ kind: 'leaderboard' as const, id: 'com.example.lb', action: 'update' as const, attributes: {}, changed: ['scoreSortType'], locales: [{ locale: 'en-US', action: 'create' as const, attributes: {}, changed: [] }] }];
|
|
const rows = gameItemRows(g, plan, null);
|
|
expect(rows.map(r => [r.kind, r.id, r.name, r.ios, r.android])).toEqual([['leaderboard', 'com.example.lb', 'Best', true, true], ['achievement', 'com.example.a1', 'First', false, true]]);
|
|
expect(rows[0]!.apple).toEqual({ action: 'update', changed: ['scoreSortType'], locales: [{ locale: 'en-US', action: 'create' }] });
|
|
expect(rows[0]!.play).toBeNull();
|
|
expect(rows[1]).toMatchObject({ points: 10, hidden: true, apple: null });
|
|
});
|
|
});
|