price changes of existing products need --allow-price-changes (play products, play/apple subscriptions, prices import): --yes and flow run --apply --yes confirm creates, first prices and other updates only; without the flag the run writes the rest, names each held change and exits 3 (declined), dry runs mark them owner, flows refuse the flag; lib/price-gate.ts, tests/price-gate.test.ts. CLI wording: console fill leaves what it cannot open or fill to the calling agent's browser tool (console plan / verify) instead of 'yourself'; other 'by hand' output points at the config; do-it-dont-ask test scans cli/ output
This commit is contained in:
parent
b5b8f2da9d
commit
e01a27d7b7
33 changed files with 414 additions and 117 deletions
|
|
@ -353,13 +353,15 @@ describe('play products --dry: what would change on an existing product', () =>
|
|||
const w = { ...want, listings: [{ ...want.listings[0]!, title: 'Remove Ads' }], regions: [want.regions[0]!, region('TR', 'TRY', 99.99)] };
|
||||
expect(productChanges(CUR, w)).toEqual([
|
||||
{ text: 'listings changed: en-US', owner: false }, { text: 'listings removed: tr-TR', owner: false },
|
||||
{ text: 'prices in 1 region(s): TR TRY 129.99 -> TRY 99.99', owner: true }, { text: 'regions dropped: JP', owner: true }]);
|
||||
{ text: 'prices in 1 region(s): TR TRY 129.99 -> TRY 99.99', owner: true, prices: [{ region: 'TR', from: 'TRY 129.99', to: 'TRY 99.99' }] },
|
||||
{ text: 'regions dropped: JP', owner: true, prices: [{ region: 'JP', from: 'JPY 450.00', to: 'not sold' }] }]);
|
||||
// activation is not a price change: --yes confirms it
|
||||
const inactive = { ...CUR, purchaseOptions: [{ ...CUR.purchaseOptions![0]!, state: 'INACTIVE' }] };
|
||||
expect(productChanges(inactive, want)).toEqual([{ text: 'purchase option INACTIVE -> ACTIVE', owner: true }]);
|
||||
expect(productChanges(inactive, want)).toEqual([{ text: 'purchase option INACTIVE -> ACTIVE', owner: false }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('play products: price, region and activation changes of existing products ask first', () => {
|
||||
describe('play products: price and region changes of existing products are the owner\'s (--allow-price-changes, never --yes)', () => {
|
||||
// the existing product as listed (2 regions), and what convertRegionPrices answers for USD 2.99
|
||||
const LISTED = { productId: 'com.example.game.no_ads',
|
||||
listings: [{ languageCode: 'en-US', title: 'No Ads', description: 'Removes ads.' }],
|
||||
|
|
@ -389,26 +391,42 @@ describe('play products: price, region and activation changes of existing produc
|
|||
const out: string[] = [];
|
||||
const [l, w, e] = [console.log, console.warn, console.error];
|
||||
console.log = console.warn = console.error = (...a: unknown[]) => { out.push(a.join(' ')); };
|
||||
try { await productsRun({ loaded, args: parseArgs(argv) }); } finally { [console.log, console.warn, console.error] = [l, w, e]; }
|
||||
const before = process.exitCode;
|
||||
process.exitCode = 0;
|
||||
let exit = 0;
|
||||
try { await productsRun({ loaded, args: parseArgs(argv) }); } finally { [console.log, console.warn, console.error] = [l, w, e]; exit = Number(process.exitCode ?? 0); process.exitCode = before; }
|
||||
const writes = calls.filter(c => c.method !== 'GET' && !c.url.endsWith(':convertRegionPrices')).map(c => `${c.method} ${c.url.replace(API, '').replace(/regionsVersion\.version=[^&]+&?/, '')}`);
|
||||
return { writes, out: out.join('\n') };
|
||||
return { writes, out: out.join('\n'), exit };
|
||||
}
|
||||
|
||||
test('without a terminal: an existing product\'s price change is skipped, its listing change and new products still go', async () => {
|
||||
const r = await runWith({ overrides: [{ region: 'TR', currency: 'TRY', price: 99.99 }], listings: { en: { title: 'Remove Ads', description: 'Removes ads.' } } });
|
||||
expect(r.out).toContain('update listings changed: en-US; prices in 1 region(s): TR TRY 129.99 -> TRY 99.99');
|
||||
expect(r.writes).toEqual([
|
||||
'PATCH /onetimeproducts/com.example.game.no_ads?updateMask=listings',
|
||||
'PATCH /onetimeproducts/com.example.game.gems_new?allowMissing=true&updateMask=listings,purchaseOptions',
|
||||
'POST /oneTimeProducts/com.example.game.gems_new/purchaseOptions:batchUpdateStates',
|
||||
]);
|
||||
expect(r.out).toContain('skipped: confirm in a terminal');
|
||||
test('flow-style run (--yes, no terminal): the listing change and new products go, the price stays, exit 3 naming the change', async () => {
|
||||
for (const argv of [[], ['--yes']]) {
|
||||
const r = await runWith({ overrides: [{ region: 'TR', currency: 'TRY', price: 99.99 }], listings: { en: { title: 'Remove Ads', description: 'Removes ads.' } } }, argv);
|
||||
expect(r.out).toContain('update listings changed: en-US; prices in 1 region(s): TR TRY 129.99 -> TRY 99.99');
|
||||
expect(r.writes).toEqual([
|
||||
'PATCH /onetimeproducts/com.example.game.no_ads?updateMask=listings',
|
||||
'PATCH /onetimeproducts/com.example.game.gems_new?allowMissing=true&updateMask=listings,purchaseOptions',
|
||||
'POST /oneTimeProducts/com.example.game.gems_new/purchaseOptions:batchUpdateStates',
|
||||
]);
|
||||
expect(r.out).toContain('price change on com.example.game.no_ads (TR TRY 129.99 -> TRY 99.99) needs the owner: re-run with --allow-price-changes');
|
||||
expect(r.out).toContain('declined: 1 price change(s) of existing products need the owner');
|
||||
expect(r.exit).toBe(3);
|
||||
}
|
||||
});
|
||||
|
||||
test('a price-only change is not written at all without a confirmation; --yes confirms in a terminal-less run', async () => {
|
||||
test('a price-only change: never written with --yes; --allow-price-changes writes it (exit 0)', async () => {
|
||||
const over = { overrides: [{ region: 'TR', currency: 'TRY', price: 99.99 }] };
|
||||
expect((await runWith(over)).writes.filter(w => w.includes('no_ads'))).toEqual([]);
|
||||
expect((await runWith(over, ['--yes'])).writes.filter(w => w.includes('no_ads'))).toEqual(['PATCH /onetimeproducts/com.example.game.no_ads?allowMissing=true&updateMask=listings,purchaseOptions']);
|
||||
expect((await runWith(over, ['--yes'])).writes.filter(w => w.includes('no_ads'))).toEqual([]);
|
||||
const ok = await runWith(over, ['--yes', '--allow-price-changes']);
|
||||
expect(ok.writes.filter(w => w.includes('no_ads'))).toEqual(['PATCH /onetimeproducts/com.example.game.no_ads?allowMissing=true&updateMask=listings,purchaseOptions']);
|
||||
expect([ok.exit, ok.out.includes('needs the owner')]).toEqual([0, false]);
|
||||
});
|
||||
|
||||
test('the dry run marks the price row "owner"', async () => {
|
||||
const r = await runWith({ overrides: [{ region: 'TR', currency: 'TRY', price: 99.99 }] }, ['--dry']);
|
||||
expect(r.out).toMatch(/com\.example\.game\.no_ads\s+update \(owner: price change, --allow-price-changes\)/);
|
||||
expect(r.writes).toEqual([]);
|
||||
});
|
||||
|
||||
test('under serve: refused even with --yes; a verified desktop owner confirmation lets it through', async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue