storebound/tests/portfolio.test.ts

88 lines
6.9 KiB
TypeScript

import { describe, expect, test } from 'bun:test';
import type { ProjectSummary } from '../cli/lib/desktop-protocol';
import { appIdentity, applePhase, appleProductIds, appleReleases, catalogParity, cmpVersion, phasedFraction, playProductIds, playReleases, releaseTrains, versionOfName } from '../cli/lib/portfolio';
const summary: ProjectSummary = {
dir: '/games/demo', config: '/games/demo/storebound.config.ts', name: 'Demo', languages: ['en'],
play: { packageName: 'com.example.demo' }, apple: { bundleId: 'com.example.demo', appId: null },
platforms: ['ios', 'android'], products: 2, productIds: ['coins', 'no_ads'], subscriptions: 1, subscriptionIds: ['pro_monthly'],
catalog: { ios: ['coins', 'no_ads', 'pro_monthly'], android: ['coins', 'no_ads', 'pro_monthly'] }, shots: false, testflightGroups: 0,
};
const apple = {
versions: [
{ attributes: { versionString: '2.3.0', appVersionState: 'WAITING_FOR_REVIEW', platform: 'IOS' }, relationships: { build: { data: { id: 'b2' } } } },
{ attributes: { versionString: '2.2.1', appStoreState: 'READY_FOR_SALE', platform: 'IOS' }, relationships: { build: { data: { id: 'b1' } } } },
{ attributes: { versionString: '9.9.9', appStoreState: 'READY_FOR_SALE', platform: 'TV_OS' } },
],
builds: [{ id: 'b1', attributes: { version: '41' } }, { id: 'b2', attributes: { version: '45' } }],
iaps: [{ attributes: { productId: 'coins' } }, { attributes: { productId: 'ios_only' } }],
};
const play = {
tracks: [
{ track: 'production', releases: [{ name: '2.2.1 (220)', versionCodes: ['220'], status: 'completed' }, { name: '2.3.0', versionCodes: ['230', '231'], status: 'inProgress', userFraction: 0.2 }] },
{ track: 'internal', releases: [{ name: '2.4.0', versionCodes: ['240'], status: 'completed' }] },
],
products: [{ productId: 'coins' }, { productId: 'no_ads' }, { sku: 'android_only' }],
publicPage: { state: 'public', versionName: '2.2.1' },
};
describe('portfolio (apps, release trains, catalog parity)', () => {
test('identity: one app, both platforms', () => {
expect(appIdentity(summary)).toEqual({ id: '/games/demo', name: 'Demo', platforms: { ios: { bundleId: 'com.example.demo', ascAppId: null }, android: { packageName: 'com.example.demo' } } });
expect(appIdentity({ ...summary, apple: null }).platforms.ios).toBeNull();
expect(appIdentity({ ...summary, platforms: ['android'] }).platforms.ios).toBeNull();
});
test('versions and phases', () => {
expect(cmpVersion('2.10.0', '2.9.1')).toBeGreaterThan(0);
expect(cmpVersion('2.3', '2.3.0')).toBe(0);
expect(versionOfName('2.3.0 (45)')).toBe('2.3.0');
expect(versionOfName(undefined)).toBeNull();
expect(applePhase('READY_FOR_SALE')).toBe('live');
expect(applePhase('PENDING_DEVELOPER_RELEASE')).toBe('approved');
expect(applePhase('METADATA_REJECTED')).toBe('rejected');
expect(applePhase('PREPARE_FOR_SUBMISSION')).toBe('draft');
expect(applePhase('SOMETHING_NEW')).toBe('other');
});
test('iOS phased release: day -> share, paused -> halted, complete -> live', () => {
expect(phasedFraction({ state: 'ACTIVE', currentDayNumber: 5 })).toBe(0.2);
expect(phasedFraction({ state: 'ACTIVE', currentDayNumber: 1 })).toBe(0.01);
expect(phasedFraction({ state: 'PAUSED', currentDayNumber: 6 })).toBe(0.5);
expect(phasedFraction({ state: 'COMPLETE', currentDayNumber: 7 })).toBe(1);
expect(phasedFraction({ state: 'ACTIVE', currentDayNumber: 99 })).toBe(1);
expect(phasedFraction(null)).toBeNull();
const v = (id: string, state: string) => ({ id, attributes: { versionString: id, appStoreState: state, platform: 'IOS' } });
const rs = appleReleases({ versions: [v('3.0.0', 'READY_FOR_SALE'), v('2.9.0', 'READY_FOR_SALE'), v('2.8.0', 'READY_FOR_SALE'), v('2.7.0', 'WAITING_FOR_REVIEW')],
phased: { '3.0.0': { attributes: { phasedReleaseState: 'ACTIVE', currentDayNumber: 4 } }, '2.9.0': { attributes: { phasedReleaseState: 'PAUSED', currentDayNumber: 2 } }, '2.8.0': { attributes: { phasedReleaseState: 'COMPLETE', currentDayNumber: 7 } } } });
expect(rs.map(r => [r.version, r.phase, r.fraction, r.phased?.state ?? null])).toEqual([['3.0.0', 'rollout', 0.1, 'ACTIVE'], ['2.9.0', 'halted', 0.02, 'PAUSED'], ['2.8.0', 'live', 1, 'COMPLETE'], ['2.7.0', 'review', null, null]]);
});
test('store releases from status data; garbage gives nothing', () => {
const ios = appleReleases(apple);
expect(ios.map(r => [r.version, r.build, r.phase])).toEqual([['2.3.0', '45', 'review'], ['2.2.1', '41', 'live']]);
const android = playReleases(play);
expect(android.map(r => [r.version, r.build, r.phase, r.fraction, r.track])).toEqual([['2.2.1', '220', 'live', null, 'production'], ['2.3.0', '231', 'rollout', 0.2, 'production'], ['2.4.0', '240', 'testing', null, 'internal']]);
expect(appleReleases(null)).toEqual([]);
expect(playReleases({ tracks: 'x' })).toEqual([]);
});
test('trains: current = highest version reaching users (live or rolling out), next = highest above it, per store', () => {
const t = releaseTrains(appleReleases(apple), playReleases(play));
expect(t.current).toMatchObject({ version: '2.3.0', ios: { phase: 'review', build: '45' }, android: { phase: 'rollout', fraction: 0.2, build: '231' } });
expect(t.next).toMatchObject({ version: '2.4.0', ios: null, android: { phase: 'testing' } });
const done = releaseTrains(appleReleases(apple), playReleases({ publicPage: play.publicPage, tracks: [play.tracks[0]!.releases[0] && { track: 'production', releases: [play.tracks[0]!.releases[0]] }] }));
expect(done.current).toMatchObject({ version: '2.2.1', ios: { phase: 'live', build: '41' }, android: { phase: 'live', build: '220' } });
expect(done.next).toMatchObject({ version: '2.3.0', ios: { phase: 'review' }, android: null });
expect(releaseTrains([], [])).toEqual({ current: null, next: null });
const onlyNext = releaseTrains(appleReleases({ versions: [apple.versions[0]] }), []);
expect(onlyNext.current).toBeNull();
expect(onlyNext.next?.version).toBe('2.3.0');
});
test('catalog parity: config only offline, both stores live', () => {
expect(catalogParity(summary)).toEqual({ checked: 'config', products: 2, subscriptions: 1, onlyIos: [], onlyAndroid: [], missingIos: [], missingAndroid: [], ok: true });
const live = catalogParity(summary, appleProductIds(apple), playProductIds(play));
expect(live).toMatchObject({ checked: 'live', onlyIos: ['ios_only'], onlyAndroid: ['no_ads', 'android_only'], missingIos: ['no_ads'], missingAndroid: [], ok: false });
// an item sold on Android only is not missing on iOS
expect(catalogParity({ ...summary, catalog: { ios: ['coins'], android: ['coins', 'no_ads'] } }, appleProductIds(apple), playProductIds(play)).missingIos).toEqual([]);
// one store unreadable: no cross-store claims
expect(catalogParity(summary, undefined, playProductIds(play))).toMatchObject({ onlyIos: [], onlyAndroid: [], missingIos: [], missingAndroid: [] });
});
});