Drift on existing items: offer-code and win-back campaigns that differ are named (Apple cannot edit them), in-app event attributes patched on drafts, a removed custom page deep link cleared, config languages without a version localization named, data safety dry run says Play has no read API; appinfo reads /v1/appPriceSchedules/{id}/manualPrices; a new Play language without an icon warns instead of stopping; fake store lists for the new reads

This commit is contained in:
CRTkafa 2026-10-22 20:38:54 +08:00
parent f2b841d735
commit e381bc2256
11 changed files with 206 additions and 10 deletions

View file

@ -23,6 +23,12 @@ const LISTS: Record<string, { type: string; one?: boolean }> = {
'inAppPurchases/offerCodes': { type: 'inAppPurchaseOfferCodes' },
'inAppPurchaseOfferCodes/customCodes': { type: 'inAppPurchaseOfferCodeCustomCodes' },
'inAppPurchaseOfferCodes/oneTimeUseCodes': { type: 'inAppPurchaseOfferCodeOneTimeUseCodes' },
'inAppPurchaseOfferCodes/prices': { type: 'inAppPurchaseOfferPrices' },
'inAppPurchasePriceSchedules/manualPrices': { type: 'inAppPurchasePrices' },
'inAppPurchasePriceSchedules/baseTerritory': { type: 'territories', one: true },
'appPriceSchedules/manualPrices': { type: 'appPrices' },
'inAppPurchaseAvailabilities/availableTerritories': { type: 'territories' },
'winBackOffers/prices': { type: 'winBackOfferPrices' },
'apps/appStoreVersions': { type: 'appStoreVersions' },
'appStoreVersions/appStoreVersionLocalizations': { type: 'appStoreVersionLocalizations' },
'appStoreVersions/appStoreReviewDetail': { type: 'appStoreReviewDetails', one: true },
@ -259,6 +265,11 @@ export class FakeAsc {
const self = this.res.get(id);
if (!rel) return self && self.type === type ? json({ data: this.view(self) }) : json({ errors: [{ status: '404' }] }, 404);
if (rel === 'relationships') return json({ data: this.children(id, rel2!).map(r => ({ type: r.type, id: r.id })) });
// an availability's territories are the ids its create named (a to-many relationship, not child resources)
if (rel === 'availableTerritories' && self) {
const ids = self.rels.availableTerritories;
return json({ data: (Array.isArray(ids) ? ids : ids ? [ids] : []).map(t => ({ type: 'territories', id: t })), links: {} });
}
const l = LISTS[`${type}/${rel}`];
if (!l) return json({ errors: [{ status: '599', detail: `fake: no list ${type}/${rel}` }] }, 599);
const kids = this.filtered(this.children(id, l.type), u.searchParams);

74
tests/misc-drift.test.ts Normal file
View file

@ -0,0 +1,74 @@
import { describe, expect, test } from 'bun:test';
import { campaignDiff } from '../cli/apple/offer-codes';
import { winBackDiff } from '../cli/apple/win-back';
import { cppDeepLinkReq, type AppEventDef, eventAttrDiff, eventPatchReq, extraCppLocales } from '../cli/lib/cpp-events';
import { missingVersionLocales } from '../cli/apple/screenshots';
import { dataSafetyDryNote } from '../cli/play/data-safety';
describe('offer code campaign drift', () => {
const cur = { eligibility: ['NON_SPENDER'], prices: [{ territory: 'USA', pointId: 'p1' }, { territory: 'DEU', pointId: 'p2' }] };
test('equal: no differences', () => {
expect(campaignDiff(cur, { eligibility: ['NON_SPENDER'], free: false, prices: cur.prices })).toEqual([]);
});
test('eligibility, territories, price and mode differ', () => {
const d = campaignDiff(cur, { eligibility: ['NON_SPENDER', 'CHURNED_SPENDER'], free: false, prices: [{ territory: 'USA', pointId: 'p9' }, { territory: 'FRA', pointId: 'p3' }] });
expect(d.join('|')).toContain('eligibility');
expect(d.join('|')).toContain('+1 (FRA)');
expect(d.join('|')).toContain('-1 (DEU)');
expect(d.join('|')).toContain('prices differ in 1 territory');
expect(campaignDiff(cur, { eligibility: ['NON_SPENDER'], free: true, prices: cur.prices }).join()).toContain('mode paid -> free');
});
});
describe('win-back drift', () => {
const want = { id: 'wb', type: 'pay-as-you-go', usd: 2.99, paidMonths: 3, lapsedMonths: { min: 1, max: 12 }, start: '2027-01-01', end: '2027-02-01' } as const;
test('equal', () => expect(winBackDiff({ offerMode: 'PAY_AS_YOU_GO', startDate: '2027-01-01', endDate: '2027-02-01' }, 2.99, want)).toEqual([]));
test('mode, price, dates', () => {
const d = winBackDiff({ offerMode: 'FREE_TRIAL', startDate: '2027-01-05', endDate: null }, 1.99, want);
expect(d.length).toBe(4);
expect(winBackDiff({ offerMode: 'PAY_AS_YOU_GO', startDate: '2027-01-01', endDate: '2027-02-01' }, undefined, want)).toEqual([]);
});
});
describe('event attribute drift', () => {
const ev: AppEventDef = { referenceName: 'Spring', badge: 'CHALLENGE', priority: 'high', schedule: { publishStart: '2027-03-01T00:00:00Z', eventStart: '2027-03-05T00:00:00Z', eventEnd: '2027-03-10T00:00:00Z' }, listings: {} };
const now = { badge: 'CHALLENGE', priority: 'HIGH', purpose: 'APPROPRIATE_FOR_ALL_USERS', territorySchedules: [{ territories: ['USA'], publishStart: '2027-03-01T00:00:00.000Z', eventStart: '2027-03-05T00:00:00.000Z', eventEnd: '2027-03-10T00:00:00.000Z' }] };
test('same instants in another format: no drift', () => {
const d = eventAttrDiff(ev, now);
expect(d.lines).toEqual([]);
expect(eventPatchReq('e1', 'Spring', d.attrs, d.lines)).toBeNull();
});
test('badge, priority and end differ: one PATCH', () => {
const d = eventAttrDiff({ ...ev, badge: 'MAJOR_UPDATE', priority: 'normal', schedule: { ...ev.schedule, eventEnd: '2027-03-12T00:00:00Z' } }, now);
expect(d.lines.length).toBe(3);
const r = eventPatchReq('e1', 'Spring', d.attrs, d.lines)!;
expect(r.method).toBe('PATCH');
expect(r.path).toBe('/v1/appEvents/e1');
expect((r.body as { data: { attributes: Record<string, unknown> } }).data.attributes.territorySchedules).toHaveLength(1);
});
});
describe('custom product page drift', () => {
test('deep link removed is cleared', () => {
const r = cppDeepLinkReq('v1', 'Page', undefined, 'example://old')!;
expect(r.note).toContain('removed');
expect((r.body as { data: { attributes: { deepLink: unknown } } }).data.attributes.deepLink).toBeNull();
});
test('equal or both empty: nothing', () => {
expect(cppDeepLinkReq('v1', 'Page', 'example://a', 'example://a')).toBeNull();
expect(cppDeepLinkReq('v1', 'Page', undefined, null)).toBeNull();
});
test('extra locales on the page', () => expect(extraCppLocales(['en-US', 'fr-FR'], ['en-US'])).toEqual(['fr-FR']));
});
describe('screenshots and data safety notes', () => {
test('version without a localization for a config language', () => {
expect(missingVersionLocales([{ apple: 'en-US' }, { apple: 'tr' }, {}], ['en-US'])).toEqual(['tr']);
});
test('data safety dry note says Play cannot be read and --force re-sends', () => {
const n = dataSafetyDryNote({ at: '2027-01-01T10:00:00Z' });
expect(n).toContain('no read API');
expect(n).toContain('--force');
expect(dataSafetyDryNote(null)).toContain('none recorded');
});
});