108 lines
7.9 KiB
TypeScript
108 lines
7.9 KiB
TypeScript
// Pitfall 70 (seen live): the app's first in-app purchase (a starter pack) was not in the 1.0.0 review submission;
|
|
// its items held only the appStoreVersion, the IAP stayed READY_TO_SUBMIT and nothing warned for two days in
|
|
// WAITING_FOR_REVIEW. Against the stateful App Store Connect double: apple status warns (text and --json), validate's
|
|
// rules raise a blocker before submitting, apple submit refuses to leave a never-approved first product out, and
|
|
// iap-review never calls READY_TO_SUBMIT "complete".
|
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
|
|
import type { ShipConfig } from '../cli/lib/config';
|
|
import { firstProductGaps, planIapSubmission, productStateText, type SubmitState } from '../cli/lib/review-submit';
|
|
import { appleFirstProductWarnings } from '../cli/lib/portfolio';
|
|
import { type AppleSnapshot, appleRules } from '../cli/lib/validate-rules';
|
|
import { run as appleStatus } from '../cli/apple/status';
|
|
import { upsertTiming } from '../cli/lib/upsert';
|
|
import { ascEnv, BASE, capture, jsonOut, project, restore } from './helpers/store-mock';
|
|
import { installStores, type Stores, uninstallStores } from './helpers/fake-stores';
|
|
|
|
const APP = '6400000101';
|
|
const PACK = 'com.example.game.starter_pack';
|
|
const CONFIG: ShipConfig = { ...BASE, products: [{ id: PACK, type: 'non_consumable', usd: 1.99, listings: { en: { title: 'Starter pack', description: 'A head start.' } } }] };
|
|
|
|
let s: Stores;
|
|
beforeAll(ascEnv);
|
|
afterAll(restore);
|
|
beforeEach(() => { upsertTiming.waitMs = 0; s = installStores(); });
|
|
afterEach(uninstallStores);
|
|
|
|
/** 1.0.0 WAITING_FOR_REVIEW in rs-1; the starter pack READY_TO_SUBMIT (never approved). */
|
|
function waiting(o: { packInItems?: boolean; packState?: string } = {}): void {
|
|
s.asc.put({ type: 'appStoreVersions', id: 'ver-1', attributes: { versionString: '1.0.0', appVersionState: 'WAITING_FOR_REVIEW', platform: 'IOS' }, rels: { app: APP } });
|
|
s.asc.put({ type: 'inAppPurchases', id: 'iap-pack', attributes: { productId: PACK, inAppPurchaseType: 'NON_CONSUMABLE', state: o.packState ?? 'READY_TO_SUBMIT' }, rels: { app: APP } });
|
|
s.asc.put({ type: 'inAppPurchaseVersions', id: 'iapv-pack', attributes: { version: 1, state: 'PREPARE_FOR_SUBMISSION' }, rels: { inAppPurchase: 'iap-pack' } });
|
|
s.asc.put({ type: 'reviewSubmissions', id: 'rs-1', attributes: { platform: 'IOS', state: 'WAITING_FOR_REVIEW' }, rels: { app: APP } });
|
|
s.asc.put({ type: 'reviewSubmissionItems', id: 'it-ver', attributes: { state: 'READY_FOR_REVIEW' }, rels: { reviewSubmission: 'rs-1', appStoreVersion: 'ver-1' } });
|
|
if (o.packInItems) s.asc.put({ type: 'reviewSubmissionItems', id: 'it-pack', attributes: { state: 'READY_FOR_REVIEW' }, rels: { reviewSubmission: 'rs-1', inAppPurchaseVersion: 'iapv-pack' } });
|
|
}
|
|
|
|
describe('apple status (pitfall 70)', () => {
|
|
test('only the version in the submission, the first IAP READY_TO_SUBMIT: WARN in text and --json', async () => {
|
|
waiting();
|
|
const p = project({ ...CONFIG, apple: { ...BASE.apple!, appId: APP } });
|
|
const r = await capture(() => appleStatus(p.ctx([])));
|
|
expect(r.err).toContain(`WARN ${PACK} is not in this review`);
|
|
expect(r.err).toContain('it will not be sellable after approval');
|
|
expect(r.err).toContain('add it with the next version, or withdraw the submission');
|
|
const j = await capture(() => appleStatus(p.ctx(['--json'], ['json'])));
|
|
const gaps = jsonOut(j.out).firstProductGaps as { productId: string; level: string }[];
|
|
expect(gaps.map(g => [g.productId, g.level])).toEqual([[PACK, 'warn']]);
|
|
// release status / the desktop read the same JSON
|
|
expect(appleFirstProductWarnings(jsonOut(j.out))[0]).toStartWith(`App Store: ${PACK} is not in this review`);
|
|
});
|
|
|
|
test('the IAP in the submission items: no warning', async () => {
|
|
waiting({ packInItems: true });
|
|
const p = project({ ...CONFIG, apple: { ...BASE.apple!, appId: APP } });
|
|
const r = await capture(() => appleStatus(p.ctx([])));
|
|
expect(r.err).not.toContain('is not in this review');
|
|
});
|
|
|
|
test('an approved IAP: no warning', async () => {
|
|
waiting({ packState: 'APPROVED' });
|
|
const p = project({ ...CONFIG, apple: { ...BASE.apple!, appId: APP } });
|
|
const r = await capture(() => appleStatus(p.ctx([])));
|
|
expect(r.err).not.toContain('is not in this review');
|
|
});
|
|
});
|
|
|
|
describe('firstProductGaps (pure)', () => {
|
|
const iap = (state: string, type = 'NON_CONSUMABLE', productId = PACK) => ({ productId, type, state, versionIds: [`v-${productId}`] });
|
|
test('an open draft without the never-approved first IAP: error; with it: nothing', () => {
|
|
const sub = { id: 'rs-1', state: 'READY_FOR_REVIEW', itemVersionIds: ['ver-1'] };
|
|
expect(firstProductGaps({ iaps: [iap('READY_TO_SUBMIT')], subs: [], submission: sub, versionState: 'PREPARE_FOR_SUBMISSION' }).map(g => g.level)).toEqual(['error']);
|
|
expect(firstProductGaps({ iaps: [iap('READY_TO_SUBMIT')], subs: [], submission: { ...sub, itemVersionIds: ['ver-1', `v-${PACK}`] } })).toEqual([]);
|
|
});
|
|
test('another product of the type approved, or the product approved: nothing', () => {
|
|
expect(firstProductGaps({ iaps: [iap('READY_TO_SUBMIT'), iap('APPROVED', 'NON_CONSUMABLE', 'com.example.game.old')], subs: [], versionState: 'WAITING_FOR_REVIEW' })).toEqual([]);
|
|
expect(firstProductGaps({ iaps: [iap('APPROVED')], subs: [], versionState: 'WAITING_FOR_REVIEW' })).toEqual([]);
|
|
});
|
|
test('missing metadata before submitting: error; a first subscription of a group counts too; only config ids', () => {
|
|
expect(firstProductGaps({ iaps: [iap('MISSING_METADATA')], subs: [] })[0]!.level).toBe('error');
|
|
const g = firstProductGaps({ iaps: [], subs: [{ productId: 'com.example.game.vip', groupId: 'g1', state: 'READY_TO_SUBMIT' }], versionState: 'IN_REVIEW' });
|
|
expect(g.map(x => [x.kind, x.level])).toEqual([['subscription', 'warn']]);
|
|
expect(firstProductGaps({ iaps: [iap('READY_TO_SUBMIT')], subs: [], versionState: 'IN_REVIEW', only: ['com.example.other'] })).toEqual([]);
|
|
});
|
|
test('validate rules: error is a blocker, warn a warning', () => {
|
|
const snap: AppleSnapshot = {
|
|
version: { versionString: '1.0.0', state: 'PREPARE_FOR_SUBMISSION' }, build: null, expectedLocales: [], locales: [], requiredDisplayTypes: [], reviewDetail: null, ageRating: null,
|
|
priceSchedule: true, availability: true, iaps: [], expectedProducts: [], privacyUrl: { url: 'https://example.com/privacy', status: 200 },
|
|
firstProductGaps: [{ level: 'error', msg: 'm1', fix: 'f' }, { level: 'warn', msg: 'm2', fix: 'f' }],
|
|
};
|
|
const f = appleRules(snap).filter(x => x.code === 'iap-first');
|
|
expect(f.map(x => [x.level, x.msg])).toEqual([['blocker', 'm1'], ['warn', 'm2']]);
|
|
});
|
|
});
|
|
|
|
test('apple submit plan: a never-approved first IAP left out (missing metadata) blocks the version submission', () => {
|
|
const st: SubmitState = { appVersions: [{ id: 'ver-1', versionString: '1.0.0', state: 'PREPARE_FOR_SUBMISSION' }], subs: [], groups: [], draft: null,
|
|
iaps: [{ id: 'iap-pack', productId: PACK, type: 'NON_CONSUMABLE', state: 'MISSING_METADATA', versions: [{ id: 'iapv-pack', version: 1, state: 'PREPARE_FOR_SUBMISSION' }] }] };
|
|
const plan = planIapSubmission(st, [PACK], { withVersion: true, appVersionId: 'ver-1' });
|
|
expect(plan.blockers.join(' ')).toContain(`never-approved first in-app purchases would be left out of this review`);
|
|
const ok = planIapSubmission({ ...st, iaps: [{ ...st.iaps[0]!, state: 'READY_TO_SUBMIT' }] }, [PACK], { withVersion: true, appVersionId: 'ver-1' });
|
|
expect(ok.blockers).toEqual([]);
|
|
expect(ok.items.map(i => i.kind)).toEqual(['appStoreVersion', 'inAppPurchaseVersion']);
|
|
});
|
|
|
|
test('iap-review wording: READY_TO_SUBMIT is never "complete"', () => {
|
|
expect(productStateText('READY_TO_SUBMIT', false)).toBe('READY_TO_SUBMIT: ready, not submitted yet (goes in with the next app version: storebound apple submit --build <n> adds it)');
|
|
expect(productStateText('READY_TO_SUBMIT', true)).not.toContain('complete');
|
|
expect(productStateText('APPROVED', true)).toBe('APPROVED');
|
|
});
|