storebound/tests/apple-platforms.test.ts

108 lines
7.9 KiB
TypeScript

// One app record, two App Store platforms (pitfall 79). Seen live: `apple resubmit` said "nothing to resubmit" while
// the macOS version was REJECTED in an UNRESOLVED_ISSUES submission, and `apple listing --version 0.8.7` refused
// because the iOS 0.8.7 was live while the macOS 0.8.7 was rejected. Against the fake App Store Connect.
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
import type { ShipConfig } from '../cli/lib/config';
import { planVersion } from '../cli/lib/listing-plan';
import { upsertTiming } from '../cli/lib/upsert';
import { appleRules } from '../cli/lib/validate-rules';
import { applePlatformWarnings, appleReleases } from '../cli/lib/portfolio';
import { parsePlatform, pickPlatform, platformLabel } from '../cli/apple/platform';
import { run as appleResubmit } from '../cli/apple/resubmit';
import { run as appleStatus } from '../cli/apple/status';
import { run as appleRelease } from '../cli/apple/release';
import { ascEnv, BASE, capture, project, restore } from './helpers/store-mock';
import { installStores, type Stores, uninstallStores } from './helpers/fake-stores';
const APP = '6400000101';
const CONFIG: ShipConfig = { ...BASE };
let s: Stores;
beforeAll(ascEnv);
afterAll(restore);
beforeEach(() => { upsertTiming.waitMs = 0; s = installStores(); });
afterEach(uninstallStores);
const ver = (id: string, platform: string, versionString: string, state: string) =>
s.asc.put({ type: 'appStoreVersions', id, attributes: { versionString, appVersionState: state, platform }, rels: { app: APP } });
/** iOS 0.8.7 live, macOS 0.8.7 rejected in an UNRESOLVED_ISSUES submission, build 8 uploaded. */
function split(): void {
ver('v-ios', 'IOS', '0.8.7', 'READY_FOR_DISTRIBUTION');
ver('v-mac', 'MAC_OS', '0.8.7', 'REJECTED');
s.asc.put({ type: 'builds', id: 'b-8', attributes: { version: '8', processingState: 'VALID', usesNonExemptEncryption: false }, rels: { app: APP } });
s.asc.put({ type: 'reviewSubmissions', id: 'rs-mac', attributes: { platform: 'MAC_OS', state: 'UNRESOLVED_ISSUES' }, rels: { app: APP } });
s.asc.put({ type: 'reviewSubmissionItems', id: 'it-mac', attributes: { state: 'REJECTED' }, rels: { reviewSubmission: 'rs-mac', appStoreVersion: 'v-mac' } });
}
describe('platform picking (pure)', () => {
const v = (platform: string, versionString: string, state: string) => ({ attributes: { platform, versionString, appVersionState: state } });
const vs = [v('IOS', '0.8.7', 'READY_FOR_DISTRIBUTION'), v('MAC_OS', '0.8.7', 'REJECTED')];
test('--platform values, labels', () => {
expect(parsePlatform('macos')).toBe('MAC_OS');
expect(parsePlatform('MAC_OS')).toBe('MAC_OS');
expect(parsePlatform('visionOS')).toBe('VISION_OS');
expect(parsePlatform(undefined)).toBeUndefined();
expect(() => parsePlatform('android')).toThrow(/ios \| macos/);
expect(platformLabel('MAC_OS')).toBe('macOS');
});
test('one platform has it -> that one; both -> refuse naming both; none -> the only platform, else iOS', () => {
const rejected = (x: typeof vs[0]) => x.attributes.appVersionState === 'REJECTED';
expect(pickPlatform(vs, rejected, undefined, 'a rejected version')).toBe('MAC_OS');
expect(pickPlatform(vs, rejected, 'IOS', 'a rejected version')).toBe('IOS');
expect(() => pickPlatform([...vs, v('IOS', '0.8.8', 'REJECTED')], rejected, undefined, 'a rejected version'))
.toThrow(/a rejected version on more than one platform \(0\.8\.7 \(macOS\) REJECTED, 0\.8\.8 \(iOS\) REJECTED\): run it again with --platform macos or --platform ios/);
expect(pickPlatform([v('MAC_OS', '1.0', 'READY_FOR_SALE')], rejected, undefined, 'x')).toBe('MAC_OS');
expect(pickPlatform(vs, () => false, undefined, 'x')).toBe('IOS');
});
test('listing --version 0.8.7: the macOS one is editable, so macOS; its plan updates it instead of refusing', () => {
const p = pickPlatform(vs, [x => x.attributes.versionString === '0.8.7' && x.attributes.appVersionState === 'REJECTED', () => true], undefined, 'x');
expect(p).toBe('MAC_OS');
const rows = [{ id: 'v-mac', attributes: { versionString: '0.8.7', appVersionState: 'REJECTED', platform: 'MAC_OS', copyright: '2026 Example', releaseType: 'MANUAL' } }];
expect(planVersion(rows, '0.8.7', { copyright: '2026 Example', releaseType: 'MANUAL' }, 'MAC_OS').action).toBe('keep');
const ios = [{ id: 'v-ios', attributes: { versionString: '0.8.7', appVersionState: 'READY_FOR_DISTRIBUTION', platform: 'IOS' } }];
expect(planVersion(ios, '0.8.7', { copyright: 'c', releaseType: 'MANUAL' }, 'IOS').action).toBe('refuse');
expect(planVersion([], '1.0.0', { copyright: 'c', releaseType: 'MANUAL' }, 'MAC_OS').attributes.platform).toBe('MAC_OS');
});
test('validate and release status name the other platform', () => {
const f = appleRules({ version: { versionString: '0.8.7', state: 'READY_FOR_DISTRIBUTION', platform: 'IOS' }, otherPlatformVersions: [{ versionString: '0.8.7', state: 'REJECTED', platform: 'MAC_OS' }],
build: null, expectedLocales: [], locales: [], requiredDisplayTypes: [], reviewDetail: null, ageRating: null,
priceSchedule: true, availability: true, iaps: [], expectedProducts: [], privacyUrl: { url: 'https://example.com/privacy', status: 200 } });
expect(f.find(x => x.code === 'version')!.msg).toBe('version 0.8.7 (iOS) is READY_FOR_DISTRIBUTION (nothing to prepare)');
expect(f.find(x => x.code === 'version-platform')).toMatchObject({ level: 'warn', msg: 'version 0.8.7 (macOS) is REJECTED too: this report covers 0.8.7 (iOS) only' });
const data = { versions: [{ id: 'v-ios', attributes: { platform: 'IOS', versionString: '0.8.7', appVersionState: 'READY_FOR_DISTRIBUTION' } }, { id: 'v-mac', attributes: { platform: 'MAC_OS', versionString: '0.8.7', appVersionState: 'REJECTED' } }] };
expect(applePlatformWarnings(data)).toEqual(['App Store macOS 0.8.7: REJECTED (storebound apple status --platform macos)']);
expect(appleReleases(data).map(r => [r.version, r.platform])).toEqual([['0.8.7', 'IOS']]);
});
});
describe('commands against an app with iOS live and macOS rejected', () => {
test('apple resubmit picks the macOS version and its UNRESOLVED_ISSUES submission', async () => {
split();
const r = await capture(() => appleResubmit(project(CONFIG).ctx(['--build', '8', '--dry'])));
expect(r.out + r.err).toContain('app version 0.8.7 (macOS): REJECTED');
expect(r.out).toMatch(/rs-mac\s+UNRESOLVED_ISSUES\s+reuse/);
});
test('apple resubmit --platform ios: nothing on iOS, said per platform', async () => {
split();
await expect(appleResubmit(project(CONFIG).ctx(['--build', '8', '--platform', 'ios', '--dry']))).rejects.toThrow(/no rejected iOS App Store version and no iOS review submission/);
});
test('both platforms rejected: refuse and ask for --platform', async () => {
split();
s.asc.all('appStoreVersions').find(x => x.id === 'v-ios')!.attributes.appVersionState = 'REJECTED';
await expect(appleResubmit(project(CONFIG).ctx(['--build', '8', '--dry']))).rejects.toThrow(/more than one platform .*--platform (ios|macos) or --platform (ios|macos)/);
});
test('apple status labels every version and submission with its platform; --platform shows one', async () => {
split();
const r = await capture(() => appleStatus(project(CONFIG).ctx([])));
expect(r.out).toMatch(/version 0\.8\.7\s+iOS\s+READY_FOR_DISTRIBUTION/);
expect(r.out).toMatch(/version 0\.8\.7\s+macOS\s+REJECTED/);
expect(r.out).toContain('review submission rs-mac (macOS) is back from App Review');
expect(r.out).toContain('storebound apple resubmit --build <n> --platform macos');
const ios = await capture(() => appleStatus(project(CONFIG).ctx(['--platform', 'ios'])));
expect(ios.out).not.toContain('macOS');
});
test('apple release --phased on picks the platform with a version not yet live', async () => {
split();
const r = await capture(() => appleRelease(project(CONFIG).ctx(['--phased', 'on', '--dry'])));
expect(r.out + r.err).toContain('0.8.7 macOS (REJECTED)');
});
});