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:
parent
f2b841d735
commit
e381bc2256
11 changed files with 206 additions and 10 deletions
|
|
@ -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
74
tests/misc-drift.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue