play data-safety: write the conditional required rows (PSL_ACCOUNT_DELETION_URL from accounts.deletionUrl, PSL_SUPPORT_DATA_DELETION_BY_USER from dataSafety.partialDeletion, PSL_DATA_DELETION_URL); a rule table checks every conditional row in check, declarations and --dry before Play answers 400
This commit is contained in:
parent
c9a7f095c4
commit
97ab76e9c8
14 changed files with 295 additions and 24 deletions
|
|
@ -4,7 +4,8 @@ import { join } from 'node:path';
|
|||
import { parseArgs } from '../cli/lib/args';
|
||||
import { loadConfig, validateConfig } from '../cli/lib/config';
|
||||
import { parseAppStoreListing, parsePlayListing } from '../cli/lib/copy';
|
||||
import { dataSafetyCsv, dataSafetyRows } from '../cli/lib/data-safety';
|
||||
import { DATA_SAFETY_RULES, dataSafetyCsv, dataSafetyIssues, dataSafetyRows } from '../cli/lib/data-safety';
|
||||
import { parseCsv } from '../cli/lib/csv';
|
||||
import { ALPHA3, appleTerritory, fromMoney, nearestPrices, playRegionConfigs, samePrice, toMoney } from '../cli/lib/money';
|
||||
import { maskExpr } from '../cli/icons';
|
||||
import { ffmpegQ } from '../cli/shots/jpeg';
|
||||
|
|
@ -99,6 +100,53 @@ describe('data safety csv', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('data safety: conditional required rows', () => {
|
||||
const acc = { ...{ collectsData: true, encryptedInTransit: true, deletionRequest: true }, accountCreation: ['PSL_ACM_USER_ID_OTHER_AUTH'],
|
||||
data: { PSL_USER_ACCOUNT: { shared: false, collect: ['account' as const] } } };
|
||||
const url = 'https://example.com/delete-account';
|
||||
const get = (rows: string[][], q: string) => rows.find(r => r[0] === q);
|
||||
test('accounts + username: the delete-account URL row carries accounts.deletionUrl', () => {
|
||||
const rows = dataSafetyRows({ ...acc, partialDeletion: false }, { deletionUrl: url });
|
||||
expect(get(rows, 'PSL_ACCOUNT_DELETION_URL')![2]).toBe(url);
|
||||
expect(get(rows, 'PSL_SUPPORT_DATA_DELETION_BY_USER')![2]).toBe('FALSE');
|
||||
expect(get(rows, 'PSL_DATA_DELETION_URL')).toBeUndefined();
|
||||
expect(dataSafetyIssues(rows)).toEqual([]);
|
||||
const yes = dataSafetyRows({ ...acc, partialDeletion: true }, { deletionUrl: url });
|
||||
expect(get(yes, 'PSL_DATA_DELETION_URL')![2]).toBe(url);
|
||||
expect(get(dataSafetyRows({ ...acc, partialDeletion: true, partialDeletionUrl: 'https://example.com/data' }, { deletionUrl: url }), 'PSL_DATA_DELETION_URL')![2]).toBe('https://example.com/data');
|
||||
});
|
||||
test('no accounts: no deletion rows, no issues', () => {
|
||||
const rows = dataSafetyRows({ ...acc, accountCreation: ['PSL_ACM_NONE'] }, { deletionUrl: url });
|
||||
for (const q of ['PSL_ACCOUNT_DELETION_URL', 'PSL_SUPPORT_DATA_DELETION_BY_USER', 'PSL_DATA_DELETION_URL']) expect(get(rows, q)).toBeUndefined();
|
||||
expect(dataSafetyIssues(rows)).toEqual([]);
|
||||
expect(get(dataSafetyRows({ ...acc, accountCreation: undefined }), 'PSL_ACCOUNT_DELETION_URL')).toBeUndefined();
|
||||
});
|
||||
test('missing deletionUrl / partialDeletion: named with the config field to set', () => {
|
||||
const msgs = dataSafetyIssues(dataSafetyRows(acc)).map(i => i.msg);
|
||||
expect(msgs[0]).toBe('missing: PSL_ACCOUNT_DELETION_URL (account creation is not PSL_ACM_NONE; set accounts.deletionUrl, a public https page to request account + data deletion (account-deletion skill))');
|
||||
expect(msgs[1]).toContain('missing: PSL_SUPPORT_DATA_DELETION_BY_USER');
|
||||
expect(msgs[1]).toContain('dataSafety.partialDeletion');
|
||||
expect(dataSafetyIssues(dataSafetyRows({ ...acc, partialDeletion: true, partialDeletionUrl: undefined }, {})).map(i => i.question)).toEqual(['PSL_ACCOUNT_DELETION_URL', 'PSL_DATA_DELETION_URL']);
|
||||
});
|
||||
test('extra replaces a generated question; refused and per-type rows', () => {
|
||||
const rows = dataSafetyRows({ ...acc, partialDeletion: false, extra: [{ question: 'PSL_ACCOUNT_DELETION_URL', value: url }] });
|
||||
expect(rows.filter(r => r[0] === 'PSL_ACCOUNT_DELETION_URL')).toHaveLength(1);
|
||||
expect(dataSafetyIssues(rows)).toEqual([]);
|
||||
const outside = dataSafetyRows({ ...acc, partialDeletion: false, extra: [{ question: 'PSL_HAS_OUTSIDE_APP_ACCOUNTS', value: 'FALSE' }] }, { deletionUrl: url });
|
||||
expect(dataSafetyIssues(outside).map(i => i.msg.split(' (')[0])).toEqual(['not allowed: PSL_HAS_OUTSIDE_APP_ACCOUNTS']);
|
||||
// a type selected but its purpose rows emptied (e.g. a hand-edited export)
|
||||
const cut = dataSafetyRows({ ...acc, partialDeletion: false }, { deletionUrl: url })
|
||||
.map(r => r[0] === 'PSL_DATA_USAGE_RESPONSES:PSL_USER_ACCOUNT:DATA_USAGE_COLLECTION_PURPOSE' ? [r[0], r[1]!, '', r[3]!, r[4]!] : r);
|
||||
expect(dataSafetyIssues(cut).map(i => i.question)).toEqual(['PSL_DATA_USAGE_RESPONSES:PSL_USER_ACCOUNT:DATA_USAGE_COLLECTION_PURPOSE']);
|
||||
});
|
||||
test('the template passes the rules and only uses known question ids', () => {
|
||||
const rows = parseCsv(readFileSync(join(import.meta.dir, '..', 'templates', 'data-safety.csv'), 'utf8'));
|
||||
expect(dataSafetyIssues(rows)).toEqual([]);
|
||||
const known = new Set(DATA_SAFETY_RULES.map(r => r.question));
|
||||
for (const r of rows.slice(1)) expect(known.has(r[0]!) || /^PSL_DATA_TYPES_|^PSL_DATA_USAGE_RESPONSES:PSL_[A-Z_]+:/.test(r[0]!)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('prices', () => {
|
||||
test('toMoney without float noise', () => {
|
||||
expect(toMoney('USD', 2.99)).toEqual({ currencyCode: 'USD', units: '2', nanos: 990000000 });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue