72 lines
4.9 KiB
TypeScript
72 lines
4.9 KiB
TypeScript
// Store-only apps edit their listing straight on the store (`storebound listing set`, serve method listing.set): the
|
|
// plan (old -> new per field, only the fields sent are judged, the App Store version rule), and the serve rules: the
|
|
// only write a store-only app (virtual) may run, a dry run unless the host's token for exactly those store ids comes.
|
|
import { describe, expect, test } from 'bun:test';
|
|
import { mint } from '../cli/lib/confirm-token';
|
|
import { ERR } from '../cli/lib/desktop-protocol';
|
|
import { setPlan } from '../cli/listing-store';
|
|
import type { RunOptions, Running } from '../cli/lib/runner';
|
|
import { Session } from '../cli/serve';
|
|
|
|
describe('listing set: the plan', () => {
|
|
test('old -> new for the fields sent, the rest left alone', () => {
|
|
const p = setPlan('play', 'en-US', { title: 'Tidy', short: 'Old short', full: 'Full text.' }, { short: 'New short' }, { appName: 'Tidy' });
|
|
expect(p.changes).toEqual([{ field: 'short', from: 'Old short', to: 'New short' }]);
|
|
expect(p.issues.filter(i => i.level === 'error')).toEqual([]);
|
|
expect(setPlan('play', 'en-US', { short: 'Same' }, { short: 'Same ' }, { appName: 'A' }).changes).toEqual([]);
|
|
});
|
|
test('limits and rules of the sent fields; a new language', () => {
|
|
const p = setPlan('apple', 'en-US', null, { subtitle: 'x'.repeat(31) }, { appName: 'Tidy', editable: true });
|
|
expect(p.create).toBe(true);
|
|
expect(p.issues.map(i => [i.field, i.level])).toContainEqual(['subtitle', 'error']);
|
|
// required fields the run does not send are not this run's problem
|
|
expect(p.issues.some(i => i.field === 'keywords')).toBe(false);
|
|
});
|
|
test('App Store: without an editable version only name, subtitle and promotional text', () => {
|
|
expect(setPlan('apple', 'en-US', { promotionalText: 'a' }, { promotionalText: 'b' }, { appName: 'A', editable: false }).refused).toBeNull();
|
|
expect(setPlan('apple', 'en-US', { description: 'a' }, { description: 'b' }, { appName: 'A', editable: false }).refused).toMatch(/no editable App Store version/);
|
|
});
|
|
});
|
|
|
|
const SECRET_HEX = Array.from({ length: 32 }, (_, i) => (i + 3).toString(16).padStart(2, '0')).join('');
|
|
const SECRET = Buffer.from(SECRET_HEX, 'hex');
|
|
type J = Record<string, unknown>;
|
|
|
|
async function session(now = () => Date.now()) {
|
|
const calls: { argv: string[]; o: RunOptions }[] = [];
|
|
const run = (argv: string[], o: RunOptions): Running => { calls.push({ argv, o }); return { result: Promise.resolve({ code: 0, out: 'done', cancelled: false }), cancel: () => {} }; };
|
|
const s = new Session(() => {}, run, now);
|
|
let id = 0;
|
|
const call = async (method: string, params: unknown = {}) => (await s.handle({ jsonrpc: '2.0', id: ++id, method, params }))! as J;
|
|
await call('session.hello', { secret: SECRET_HEX });
|
|
return { calls, call };
|
|
}
|
|
const err = (r: J) => r.error as { code: number; message: string } | undefined;
|
|
|
|
describe('serve: listing.set is the one write of a store-only app', () => {
|
|
const virt = { name: 'com.example.tidy', play: { packageName: 'com.example.tidy' } };
|
|
const args = { store: 'play', lang: 'en-US', fields: JSON.stringify({ short: 'Lists that stay tidy' }) };
|
|
test('dry run with the store ids, the real run only with a token bound to them', async () => {
|
|
const now = 5_000_000;
|
|
const s = await session(() => now);
|
|
const dry = await s.call('listing.set', { virtual: virt, args });
|
|
expect(err(dry)).toBeUndefined();
|
|
expect(s.calls.at(-1)!.argv).toEqual(['listing', 'set', '--store', 'play', '--lang', 'en-US', '--fields', args.fields, '--dry', '--json']);
|
|
expect(err(await s.call('listing.set', { virtual: virt, args, dryRun: false }))!.code).toBe(ERR.confirmRequired);
|
|
// a token for a folder, or for another app's store ids, does not fit
|
|
expect(err(await s.call('listing.set', { virtual: virt, args, dryRun: false, confirmToken: mint(SECRET, 'listing.set', { project: '/x', args }, { now }) }))!.code).toBe(ERR.confirmInvalid);
|
|
expect(err(await s.call('listing.set', { virtual: virt, args, dryRun: false, confirmToken: mint(SECRET, 'listing.set', { virtual: { ...virt, name: 'other' }, args }, { now }) }))!.code).toBe(ERR.confirmInvalid);
|
|
const real = await s.call('listing.set', { virtual: virt, args, dryRun: false, confirmToken: mint(SECRET, 'listing.set', { virtual: virt, args }, { now }) });
|
|
expect(err(real)).toBeUndefined();
|
|
expect(s.calls.at(-1)!.argv).not.toContain('--dry');
|
|
});
|
|
test('folder writes and other store writes stay refused for store-only apps; listing.live reads', async () => {
|
|
const s = await session();
|
|
for (const m of ['play.listing', 'apple.listing', 'project.edit', 'play.images']) {
|
|
const r = await s.call(m, { virtual: virt, args: m === 'project.edit' ? { ops: '[]' } : {} });
|
|
expect(err(r)?.message, m).toMatch(/needs an app folder/);
|
|
}
|
|
expect(err(await s.call('listing.live', { virtual: virt }))).toBeUndefined();
|
|
expect(s.calls.at(-1)!.argv).toEqual(['listing', 'live', '--json']);
|
|
});
|
|
});
|