`console fill apple-app-privacy` without apple.appId stopped at "the app in the list" with "more than one app matches: X | X" for an account with one app: the Apps grid names each app twice (the icon link's aria-label and the title link), both opening the same /apps/<id>/... page. - pickApp tells hits apart by the app they open (/apps/<id>, else the link target), not by name. Two apps of one name with different targets still stop and are listed. - Without apple.appId, but with App Store Connect API credentials, the app id is looked up from apple.bundleId (GET /v1/apps?filter[bundleId], as `apple status` does), cached in .storebound/console-app-ids.json, and App Privacy opens by deep link. Without credentials, or when the lookup fails, the Apps list by name is used, and the log says so. Fixture asc-apps-card.html (a card with the name in the title and an aria-label); tests for both paths.
70 lines
4.5 KiB
TypeScript
70 lines
4.5 KiB
TypeScript
// console fill apple-app-privacy without apple.appId: the app id from the App Store Connect API (by bundle id, cached)
|
|
// -> deep link; no credentials / no app -> the Apps list by name. No network: the lookup is injected.
|
|
import { afterAll, describe, expect, test } from 'bun:test';
|
|
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { appleAppId, withAppleAppId } from '../cli/console/app-id';
|
|
import { appKey } from '../cli/console/fill';
|
|
import type { ShipConfig } from '../cli/lib/config';
|
|
import { navPlan } from '../cli/lib/console-forms';
|
|
|
|
const tmp = mkdtempSync(join(tmpdir(), 'sb-app-id-'));
|
|
afterAll(() => rmSync(tmp, { recursive: true, force: true }));
|
|
|
|
const config = {
|
|
app: { name: 'Example Game', profile: 'game', privacyPolicyUrl: 'https://example.com/privacy', supportUrl: 'https://example.com/support', website: 'https://example.com', contactEmail: 'support@example.com' },
|
|
languages: [{ id: 'en', play: 'en-US', apple: 'en-US' }],
|
|
apple: { bundleId: 'com.example.game', primaryLocale: 'en-US' },
|
|
} as ShipConfig;
|
|
|
|
describe('apple app id for console fill', () => {
|
|
test('API credentials: looked up by bundle id, cached, and the form opens by deep link', async () => {
|
|
const cacheFile = join(tmp, 'a', 'console-app-ids.json');
|
|
const said: string[] = [];
|
|
const asked: string[] = [];
|
|
const lookup = async (b: string) => { asked.push(b); return '6800000001'; };
|
|
const id = await appleAppId(tmp, config, { say: m => said.push(m), lookup, cacheFile });
|
|
expect(id).toBe('6800000001');
|
|
expect(asked).toEqual(['com.example.game']);
|
|
expect(JSON.parse(readFileSync(cacheFile, 'utf8'))).toEqual({ 'com.example.game': '6800000001' });
|
|
expect(said.some(m => m.includes('Apple ID 6800000001 (App Store Connect API)'))).toBe(true);
|
|
const plan = navPlan('apple-app-privacy', withAppleAppId(config, id));
|
|
expect(plan.map(s => s.kind)).toEqual(['goto', 'wait']);
|
|
expect(plan[0]).toMatchObject({ url: 'https://appstoreconnect.apple.com/apps/6800000001/distribution/privacy' });
|
|
// the second run reads the cache, no API call
|
|
const again = await appleAppId(tmp, config, { say: () => {}, lookup: async () => { throw new Error('not called'); }, cacheFile });
|
|
expect(again).toBe('6800000001');
|
|
});
|
|
|
|
test('no API credentials: null, said, and the Apps list by name is used', async () => {
|
|
const cacheFile = join(tmp, 'b', 'console-app-ids.json');
|
|
const said: string[] = [];
|
|
const id = await appleAppId(tmp, config, { say: m => said.push(m), lookup: async () => { throw new Error('ASC_KEY_ID and ASC_ISSUER_ID are not set (env, or .storebound/credentials.json)'); }, cacheFile });
|
|
expect(id).toBeNull();
|
|
expect(existsSync(cacheFile)).toBe(false);
|
|
expect(said.join('\n')).toContain('finding the app in the Apps list by name');
|
|
const plan = navPlan('apple-app-privacy', withAppleAppId(config, id));
|
|
expect(plan.map(s => s.kind)).toEqual(['goto', 'pickApp', 'click', 'wait']);
|
|
});
|
|
|
|
test('no app with that bundle id: null; apple.appId in the config wins without a lookup; no bundle id: null', async () => {
|
|
const cacheFile = join(tmp, 'c', 'console-app-ids.json');
|
|
expect(await appleAppId(tmp, config, { say: () => {}, lookup: async () => null, cacheFile })).toBeNull();
|
|
const set = { ...config, apple: { ...config.apple!, appId: '123' } } as ShipConfig;
|
|
expect(await appleAppId(tmp, set, { say: () => {}, lookup: async () => { throw new Error('not called'); }, cacheFile })).toBe('123');
|
|
expect(withAppleAppId(set, '999')).toBe(set);
|
|
const noBundle = { ...config, apple: { primaryLocale: 'en-US' } } as unknown as ShipConfig;
|
|
expect(await appleAppId(tmp, noBundle, { say: () => {}, lookup: async () => { throw new Error('not called'); }, cacheFile })).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('appKey (one app shown twice in the list is one app)', () => {
|
|
test('App Store Connect app URLs by id, any sub page; other links by URL without fragment', () => {
|
|
expect(appKey('https://appstoreconnect.apple.com/apps/6800000001/distribution/ios/version/inflight')).toBe('apps/6800000001');
|
|
expect(appKey('https://appstoreconnect.apple.com/apps/6800000001/appstore')).toBe('apps/6800000001');
|
|
expect(appKey('https://appstoreconnect.apple.com/apps/6800000001')).toBe('apps/6800000001');
|
|
expect(appKey('http://127.0.0.1:1/asc-app.html?id=1#x')).toBe('http://127.0.0.1:1/asc-app.html?id=1');
|
|
expect(appKey(null)).toBeNull();
|
|
});
|
|
});
|