tests: game services on both stores against the fakes (capability ensure, Game Center enable and read-back, leaderboard / achievement create, update, no-op, lost answers), entitlement and APP_ID checks, PGS console plan URLs and verify, validate findings; fake Games Configuration API

This commit is contained in:
CRTkafa 2026-10-21 05:06:24 +07:00
parent 9f48dd57a4
commit a4c31e096b
2 changed files with 255 additions and 2 deletions

View file

@ -75,6 +75,12 @@ const LISTS: Record<string, { type: string; one?: boolean }> = {
'appAssetLibraries/images': { type: 'appAssetLibraryImages' },
'appAssetLibraries/videos': { type: 'appAssetLibraryVideos' },
'appStoreVersionLocalizations/placements': { type: 'appAssetLibraryPlacements' },
'apps/gameCenterDetail': { type: 'gameCenterDetails', one: true },
'appStoreVersions/gameCenterAppVersion': { type: 'gameCenterAppVersions', one: true },
'gameCenterDetails/gameCenterLeaderboards': { type: 'gameCenterLeaderboards' },
'gameCenterDetails/gameCenterAchievements': { type: 'gameCenterAchievements' },
'gameCenterLeaderboards/localizations': { type: 'gameCenterLeaderboardLocalizations' },
'gameCenterAchievements/localizations': { type: 'gameCenterAchievementLocalizations' },
};
/** Stores refuse a second resource with the same key (409 ENTITY_ERROR ... already exists). */
const UNIQUE: Record<string, (r: R) => string> = {
@ -100,6 +106,12 @@ const UNIQUE: Record<string, (r: R) => string> = {
promotedPurchases: r => String(r.rels.inAppPurchaseV2 ?? r.rels.subscription),
winBackOffers: r => `${r.rels.subscription}|${r.attributes.offerId}`,
betaTesters: r => String(r.attributes.email).toLowerCase(),
gameCenterDetails: r => String(r.rels.app),
gameCenterAppVersions: r => String(r.rels.appStoreVersion),
gameCenterLeaderboards: r => String(r.attributes.vendorIdentifier),
gameCenterAchievements: r => String(r.attributes.vendorIdentifier),
gameCenterLeaderboardLocalizations: r => `${r.rels.gameCenterLeaderboard}|${r.attributes.locale}`,
gameCenterAchievementLocalizations: r => `${r.rels.gameCenterAchievement}|${r.attributes.locale}`,
};
const ASSETS = /Screenshots$|^appPreviews$|^appScreenshots$|^appAssetLibrary(Images|Videos)$/;
/** App Asset Library assets: a state of their own (AWAITING_UPLOAD -> PREPARE_FOR_SUBMISSION after the commit, as Apple's
@ -496,13 +508,43 @@ export class FakePlay {
// ------------------------------------------------------------------------------------------------- fetch + counting
export interface Stores { asc: FakeAsc; play: FakePlay; calls: Call[]; faults: Fault[]; failPart?: (n: number) => boolean }
// ----------------------------------------------------------------- Play Games Services Configuration API (v1configuration)
/** achievementConfigurations / leaderboardConfigurations: list (items, nextPageToken), insert (Google generates the
* id, CgkI...), update (PUT replaces the configuration; `published` stays Google's). Shapes from Google's reference. */
export class FakeGames {
achievements = new Map<string, Record<string, unknown>>();
leaderboards = new Map<string, Record<string, unknown>>();
next = 1;
handle(method: string, url: string, body: unknown): Response {
const p = new URL(url).pathname.replace(/^\/games\/v1configuration/, '');
let m: RegExpExecArray | null;
if ((m = /^\/applications\/(\d+)\/(achievements|leaderboards)$/.exec(p))) {
const map = this[m[2] as 'achievements' | 'leaderboards'];
if (method === 'GET') return json({ kind: `gamesConfiguration#${m[2] === 'achievements' ? 'achievement' : 'leaderboard'}ConfigurationListResponse`, items: [...map.values()] });
const id = `CgkI${m[1]}EAIQ${this.next++}`;
const r = { ...(body as Record<string, unknown>), id, token: id };
map.set(id, r);
return json(r);
}
if ((m = /^\/(achievements|leaderboards)\/([^/]+)$/.exec(p))) {
const map = this[m[1] as 'achievements' | 'leaderboards'];
const id = decodeURIComponent(m[2]!);
if (!map.has(id)) return json({ error: { code: 404, status: 'NOT_FOUND', message: 'Not found' } }, 404);
if (method === 'PUT') map.set(id, { ...(body as Record<string, unknown>), id, token: id, published: map.get(id)!.published });
return json(map.get(id));
}
return json({ error: { code: 599, message: `fake games: ${method} ${p}` } }, 599);
}
}
export interface Stores { asc: FakeAsc; play: FakePlay; games: FakeGames; calls: Call[]; faults: Fault[]; failPart?: (n: number) => boolean }
const realFetch = globalThis.fetch;
/** Installs both fakes as fetch. Faults apply in order (`times`, default 1). */
export function installStores(): Stores {
const s: Stores = { asc: new FakeAsc(), play: new FakePlay(), calls: [], faults: [] };
const s: Stores = { asc: new FakeAsc(), play: new FakePlay(), games: new FakeGames(), calls: [], faults: [] };
let partNo = 0;
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
const url = String(input instanceof Request ? input.url : input);
@ -524,6 +566,7 @@ export function installStores(): Stores {
res = new Response(null, { status: 200 });
} else if (url.startsWith(ASC)) res = s.asc.handle(method, path, body);
else if (url.startsWith('https://androidpublisher.googleapis.com/')) res = s.play.handle(method, url, body instanceof Uint8Array ? body : body);
else if (url.startsWith('https://gamesconfiguration.googleapis.com/')) res = s.games.handle(method, url, body);
else res = new Response(`fake: no route ${method} ${url}`, { status: 599 });
if (f?.when === 'after') return json({ errors: [{ status: String(f.status ?? 502), detail: 'injected after the write' }] }, f.status ?? 502);
return res;