storebound/tests/desktop-complete.test.ts

64 lines
4.2 KiB
TypeScript

// Desktop full CLI coverage, CLI side: store declarations as data (ask the owner, never guess), Data safety changes
// since the last upload, renders as data: URLs only from inside the renders folder.
import { describe, expect, test } from 'bun:test';
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import type { ShipConfig } from '../cli/lib/config';
import { dataSafetyCsv } from '../cli/lib/data-safety';
import { applePurpose, dataSafetyChanges, declarations } from '../cli/lib/declarations';
import { RENDER_FILE_RE, renderImage } from '../cli/lib/project-files';
const base = (o: Partial<ShipConfig> = {}): ShipConfig => ({
app: { name: 'Demo', privacyPolicyUrl: 'https://example.com/p', supportUrl: 'https://example.com/s', website: 'https://example.com', contactEmail: 'a@example.com', profile: 'app' },
languages: [{ id: 'en', play: 'en-US', apple: 'en-US' }], play: { packageName: 'com.example.demo' }, apple: { bundleId: 'com.example.demo' }, ...o,
} as ShipConfig);
const none = { demoUser: null, contactEmail: null, uploaded: null };
describe('declarations', () => {
test('an empty config asks the owner everything it cannot prove', () => {
const d = declarations(base(), none);
expect(d.ask.map(a => a.id).sort()).toEqual(['ai-age', 'ai-categories', 'ai-rights', 'ap-unknown', 'ds-missing', 'enc-unset', 'rv-contact', 'rv-signin']);
expect(d.encryption).toBeNull();
expect(d.dataSafety).toBeNull();
});
test('Data safety answers become App Privacy suggestions; linked / tracking stay questions', () => {
const cfg = base({
dataSafety: { collectsData: true, encryptedInTransit: true, data: {
PSL_DEVICE_ID: { shared: true, collect: ['analytics'], share: ['advertising'] }, PSL_CRASH_LOGS: { shared: false, collect: ['analytics'] } } },
apple: { bundleId: 'com.example.demo', usesNonExemptEncryption: false, info: { categories: { primary: 'UTILITIES' }, ageRating: {}, contentRights: 'DOES_NOT_USE_THIRD_PARTY_CONTENT' } },
reviewAccess: { signInRequired: false },
});
const d = declarations(cfg, { ...none, contactEmail: 'a***@example.com' });
expect(d.appPrivacy!.items.find(x => x.apple === 'Device ID')).toEqual({ apple: 'Device ID', category: 'Identifiers', from: ['PSL_DEVICE_ID'], purposes: ['Analytics', 'Third-Party Advertising'], thirdParty: true });
expect(d.ask.map(a => a.id)).toEqual(['ap-linked']);
expect(d.encryption).toBe(false);
expect(d.review.contact).toBe('a***@example.com');
// ads in an app come from an ad SDK: Third-Party Advertising only; Developer's Advertising is the owner's answer
expect(applePurpose('advertising')).toBe('Third-Party Advertising');
expect(applePurpose('communications')).toBe('App Functionality');
});
test('changes since the last upload compare answers, not labels', () => {
const ds = { collectsData: true, encryptedInTransit: false, data: { PSL_CRASH_LOGS: { shared: false, collect: ['analytics' as const] } } };
const before = dataSafetyCsv(ds);
expect(dataSafetyChanges(before, before)).toEqual([]);
const after = dataSafetyCsv({ ...ds, encryptedInTransit: true });
expect(dataSafetyChanges(before, after)).toEqual([{ question: 'PSL_DATA_COLLECTION_ENCRYPTED_IN_TRANSIT', response: '', from: 'FALSE', to: 'TRUE' }]);
const d = declarations(base({ dataSafety: { ...ds, encryptedInTransit: true } }), { ...none, uploaded: { csv: before, at: '2026-10-01T00:00:00Z' } });
expect(d.dataSafety!.uploaded!.changes.length).toBe(1);
});
});
describe('renders as data: URLs', () => {
test('only image paths inside the renders folder', () => {
const dir = mkdtempSync(join(tmpdir(), 'sb-renders-'));
mkdirSync(join(dir, 'en', 'ios69'), { recursive: true });
writeFileSync(join(dir, 'en', 'ios69', '01.png'), Buffer.from([0x89, 0x50, 0x4e, 0x47]));
writeFileSync(join(dir, 'secret.txt'), 'x');
expect(renderImage(dir, 'en/ios69/01.png').dataUrl).toStartWith('data:image/png;base64,');
for (const bad of ['../x.png', 'en/../../x.png', '/etc/x.png', 'secret.txt', 'en/./01.png', '-x.png']) {
if (bad !== '-x.png') expect(RENDER_FILE_RE.test(bad), bad).toBe(false);
expect(renderImage(dir, bad).dataUrl, bad).toBeNull();
}
});
});