validate: macOS versions need Mac screenshots, Play reads survive an ended edit; macOS build numbers across platforms; Mac build prebuild + apple.macAppCheck; signing wording off the Mac; dry flows plan uploads
This commit is contained in:
parent
b1f1b206d3
commit
f5da7c9f7a
21 changed files with 519 additions and 57 deletions
91
tests/validate-play-edit.test.ts
Normal file
91
tests/validate-play-edit.test.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
// `storebound validate --play` read its screenshots with a bare POST /edits and a DELETE in a finally: not under the
|
||||
// machine lock and never retried. When Play (or another storebound process) ended that edit meanwhile, the next read
|
||||
// answered 400 "This Edit has been deleted" and validate printed it as a store blocker ("GET .../phoneScreenshots 400").
|
||||
// Now the reads run in play.read (one locked edit, deleted only after all reads, redone once with a new edit), and a
|
||||
// vanished edit that stays gone is a tool warning, never a blocker.
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
||||
import { generateKeyPairSync } from 'node:crypto';
|
||||
import { mkdtempSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { parseArgs } from '../cli/lib/args';
|
||||
import type { Loaded, ShipConfig } from '../cli/lib/config';
|
||||
import { collectPlay, run } from '../cli/validate';
|
||||
import { installStores, type Stores, uninstallStores } from './helpers/fake-stores';
|
||||
|
||||
const PKG = 'com.example.game';
|
||||
const GONE = { error: { code: 400, status: 'FAILED_PRECONDITION', message: 'This Edit has been deleted.' } };
|
||||
let s: Stores;
|
||||
let savedKey: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
savedKey = process.env.PLAY_SERVICE_ACCOUNT;
|
||||
const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
|
||||
process.env.PLAY_SERVICE_ACCOUNT = JSON.stringify({ client_email: 'sb@example.com', private_key: privateKey.export({ type: 'pkcs8', format: 'pem' }).toString(), token_uri: 'https://oauth2.googleapis.com/token' });
|
||||
s = installStores();
|
||||
// the fake has no countryAvailability route (the console owns it): a plain 404, which validate reads as "not readable"
|
||||
s.faults.push({ method: 'GET', match: /countryAvailability/, status: 404, when: 'before', times: 99 });
|
||||
s.play.live.listings['en-US'] = { language: 'en-US', title: 'Example Game', shortDescription: 'Short.', fullDescription: 'Full.' };
|
||||
});
|
||||
afterEach(() => {
|
||||
uninstallStores();
|
||||
if (savedKey === undefined) delete process.env.PLAY_SERVICE_ACCOUNT; else process.env.PLAY_SERVICE_ACCOUNT = savedKey;
|
||||
});
|
||||
|
||||
function loaded(): Loaded {
|
||||
const root = mkdtempSync(join(tmpdir(), 'sb-vpe-'));
|
||||
const config = { app: { name: 'Example Game', privacyPolicyUrl: '' }, languages: [{ id: 'en', play: 'en-US' }], play: { packageName: PKG } } as unknown as ShipConfig;
|
||||
return { config, root, path: join(root, 'storebound.config.ts'), resolve: p => join(root, p) };
|
||||
}
|
||||
|
||||
/** Play ends the edit under the reader: the DELETE happens on the store, the read that follows answers 400. */
|
||||
function endEditOnImageRead(times: number): { hits: () => number } {
|
||||
const inner = globalThis.fetch;
|
||||
let left = times, hits = 0;
|
||||
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
|
||||
const url = String(input instanceof Request ? input.url : input);
|
||||
const m = /\/edits\/([^/]+)\/listings\/[^/]+\/phoneScreenshots$/.exec(url);
|
||||
if (m && (init?.method ?? 'GET').toUpperCase() === 'GET' && left > 0) {
|
||||
left--; hits++;
|
||||
s.play.edits.delete(m[1]!);
|
||||
return new Response(JSON.stringify(GONE), { status: 400, headers: { 'content-type': 'application/json' } });
|
||||
}
|
||||
return inner(input, init);
|
||||
}) as typeof fetch;
|
||||
return { hits: () => hits };
|
||||
}
|
||||
|
||||
describe('validate --play: the read-only edit', () => {
|
||||
test('an edit ended during the screenshot reads is reopened once; the snapshot is complete and every edit is closed', async () => {
|
||||
const fault = endEditOnImageRead(1);
|
||||
const snap = await collectPlay({ loaded: loaded(), args: parseArgs(['--play']) });
|
||||
expect(fault.hits()).toBe(1);
|
||||
expect(snap.listings.map(l => l.language)).toEqual(['en-US']);
|
||||
expect(Object.keys(snap.listings[0]!.images)).toContain('phoneScreenshots');
|
||||
expect(s.play.edits.size).toBe(0);
|
||||
const edits = s.calls.filter(c => c.method === 'POST' && /\/edits$/.test(c.path));
|
||||
expect(edits.length).toBe(2);
|
||||
});
|
||||
|
||||
test('no image read runs after the edit was deleted', async () => {
|
||||
await collectPlay({ loaded: loaded(), args: parseArgs(['--play']) });
|
||||
const del = s.calls.findIndex(c => c.method === 'DELETE' && /\/edits\/[^/]+$/.test(c.path));
|
||||
expect(del).toBeGreaterThan(-1);
|
||||
expect(s.calls.slice(del + 1).filter(c => /\/edits\/[^/]+\//.test(c.path))).toEqual([]);
|
||||
});
|
||||
|
||||
test('an edit that stays gone is a tool warning, never a store blocker', async () => {
|
||||
endEditOnImageRead(10);
|
||||
const out: string[] = [];
|
||||
const log = console.log;
|
||||
console.log = (...a: unknown[]) => { out.push(a.join(' ')); };
|
||||
const prev = process.exitCode;
|
||||
try { await run({ loaded: loaded(), args: parseArgs(['--play', '--json', '--no-scan', '--no-health', '--no-local']) }); } finally { console.log = log; }
|
||||
process.exitCode = prev;
|
||||
const res = JSON.parse(out.join('\n').slice(out.join('\n').indexOf('{'))) as { blockers: number; findings: { level: string; code: string; msg: string }[] };
|
||||
expect(res.findings.filter(f => f.level === 'blocker' && /Edit has been deleted|phoneScreenshots/.test(f.msg))).toEqual([]);
|
||||
expect(res.findings).toContainEqual(expect.objectContaining({ level: 'warn', code: 'edit-gone' }));
|
||||
// (the template-answers blocker of the example config is unrelated: only an api blocker would be the bug)
|
||||
expect(res.findings.filter(f => f.level === 'blocker' && f.code === 'api')).toEqual([]);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue