onboard reads review access from the live App Store version (READY_FOR_SALE / READY_FOR_DISTRIBUTION, newest live first; the newest version only when nothing is live): a newer draft's empty review detail no longer hides the live demo account or turns signInRequired off; reviewVersion in cli/lib/onboard.ts, tests with a live version holding a demo account and a newer draft without

This commit is contained in:
CRTkafa 2026-10-14 02:08:39 +08:00
parent 0c4689a81b
commit 21fa91afd1
3 changed files with 41 additions and 5 deletions

View file

@ -8,7 +8,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { catalogDiff, type StoreItem, wantedCatalog } from '../cli/lib/catalog';
import { loadConfig, type ShipConfig } from '../cli/lib/config';
import { contactProposal, deletionLinks, findDeletionUrl, importCatalog, isTemplateValue, maskContact, signInDecision } from '../cli/lib/onboard';
import { contactProposal, deletionLinks, findDeletionUrl, importCatalog, isTemplateValue, maskContact, reviewVersion, signInDecision } from '../cli/lib/onboard';
import { toTs } from '../cli/lib/scan/config-edit';
import { run as onboard } from '../cli/onboard';
import { run as catalog } from '../cli/catalog';
@ -27,7 +27,7 @@ const money = (currencyCode: string, units: string, nanos = 0) => ({ currencyCod
/** App Store Connect + Google Play as they answered for a live app (sanitized): Pro has its price schedule's base
* territory in Turkey, Tips is on the App Store only, Plus monthly is a Play-only subscription. */
function liveStores(o: { review?: Record<string, unknown> } = {}): Call[] {
function liveStores(o: { review?: Record<string, unknown>; draft?: boolean } = {}): Call[] {
const P = '/androidpublisher/v3/applications/com.example.notes';
return mockFetch([
// ---- App Store Connect: catalog
@ -48,7 +48,13 @@ function liveStores(o: { review?: Record<string, unknown> } = {}): Call[] {
{ match: /^\/v1\/apps\/6400000101\/subscriptionGroups/, answer: page([]) },
// ---- App Store Connect: app details and review detail
{ match: /^\/v1\/apps\/6400000101$/, answer: { data: { type: 'apps', id: '6400000101', attributes: { bundleId: 'com.example.notes', primaryLocale: 'en-US' } } } },
{ match: /^\/v1\/apps\/6400000101\/appStoreVersions\?filter\[platform\]=IOS/, answer: page([{ type: 'appStoreVersions', id: 'v1', attributes: { versionString: '2.3.0', appStoreState: 'READY_FOR_SALE', appVersionState: 'READY_FOR_DISTRIBUTION' } }]) },
// with `draft`: a newer 2.4.0 in PREPARE_FOR_SUBMISSION (listed first, as App Store Connect does) whose review detail is empty
{ match: /^\/v1\/apps\/6400000101\/appStoreVersions\?filter\[platform\]=IOS/, answer: page([
...(o.draft ? [{ type: 'appStoreVersions', id: 'v2', attributes: { versionString: '2.4.0', appStoreState: 'PREPARE_FOR_SUBMISSION', appVersionState: 'PREPARE_FOR_SUBMISSION', createdDate: '2026-09-20T01:00:00-07:00' } }] : []),
{ type: 'appStoreVersions', id: 'v1', attributes: { versionString: '2.3.0', appStoreState: 'READY_FOR_SALE', appVersionState: 'READY_FOR_DISTRIBUTION', createdDate: '2026-09-01T01:00:00-07:00' } }]) },
{ match: /^\/v1\/appStoreVersions\/v2\/appStoreVersionLocalizations/, answer: page([{ type: 'appStoreVersionLocalizations', id: 'vl2', attributes: { locale: 'en-US', supportUrl: 'https://notes.example.org/help', marketingUrl: 'https://notes.example.org' } }]) },
{ match: /^\/v1\/appStoreVersions\/v2\/appStoreReviewDetail/, answer: { data: { type: 'appStoreReviewDetails', id: 'rd2', attributes: { contactFirstName: 'Ada', contactLastName: 'Example', contactPhone: '+1 555 0100 77', contactEmail: 'review@example.org',
demoAccountName: null, demoAccountPassword: null, demoAccountRequired: false, notes: null } } } },
{ match: /^\/v1\/appStoreVersions\/v1\/appStoreVersionLocalizations/, answer: page([{ type: 'appStoreVersionLocalizations', id: 'vl1', attributes: { locale: 'en-US', supportUrl: 'https://notes.example.org/help', marketingUrl: 'https://notes.example.org' } }]) },
{ match: /^\/v1\/appStoreVersions\/v1\/appStoreReviewDetail/, answer: { data: { type: 'appStoreReviewDetails', id: 'rd1', attributes: { contactFirstName: 'Ada', contactLastName: 'Example', contactPhone: '+1 555 0100 77', contactEmail: 'review@example.org',
demoAccountName: 'demo-reviewer', demoAccountPassword: 'demo-secret-sentinel', demoAccountRequired: true, notes: 'Sign in with the demo account.', ...o.review } } } },
@ -125,6 +131,12 @@ describe('contact, sign-in, deletion page (pure)', () => {
expect(isTemplateValue('https://notes.example.org')).toBe(false);
expect(maskContact({ firstName: 'Ada', lastName: 'Example', phone: '+1 555 0100 77', email: 'review@example.org' })).toBe('A. E. r***@example.org, phone ***77');
});
test('review version: the live one, newest first; the newest only when nothing is live', () => {
const v = (id: string, state: string, createdDate: string) => ({ id, attributes: { appStoreState: state, appVersionState: state === 'READY_FOR_SALE' ? 'READY_FOR_DISTRIBUTION' : state, createdDate } });
expect(reviewVersion([v('d', 'PREPARE_FOR_SUBMISSION', '2026-09-20'), v('l1', 'READY_FOR_SALE', '2026-08-01'), v('l2', 'READY_FOR_SALE', '2026-09-01')])?.id).toBe('l2');
expect(reviewVersion([v('a', 'PREPARE_FOR_SUBMISSION', '2026-08-01'), v('b', 'WAITING_FOR_REVIEW', '2026-09-01')])?.id).toBe('b');
expect(reviewVersion([])).toBeNull();
});
test('sign-in: Apple review detail first, then the code, never a default yes', () => {
expect(signInDecision({ auth: [], apple: null })).toMatchObject({ signInRequired: false, question: null });
expect(signInDecision({ auth: ['src/auth.ts:1'], apple: null })).toMatchObject({ signInRequired: null });
@ -187,6 +199,17 @@ describe('storebound onboard (recorded live shapes)', () => {
expect(d.exit).toBeNull();
expect(d.err + d.out).toContain('"errors": 0');
});
test('a newer draft with an empty review detail: the live version demo account is imported and sign-in is required', async () => {
ascEnv(); playEnv();
const p = liveProject();
liveStores({ draft: true });
const r = await capture(() => onboard(p.ctx(['--write'], ['write'])));
expect(r.out + r.err).not.toContain('demo-secret-sentinel');
expect(r.out + r.err).toContain('sign-in for review: required (App Store review detail: demo account required (a demo account is set there))');
const creds = JSON.parse(readFileSync(join(p.root, '.storebound', 'credentials.json'), 'utf8'));
expect(creds.reviewDemoAccount).toEqual({ username: 'demo-reviewer', password: 'demo-secret-sentinel' });
expect((await loadConfig(p.loaded.path)).config.reviewAccess).toEqual({ signInRequired: true });
});
test('without --write nothing is written; sign-in unproven becomes the one owner question', async () => {
ascEnv(); playEnv();
const p = liveProject();