scan, privacy answers and credentials from a second game's first release (lessons 35-37): review-prompt needs rating context, not a bare "rate"; account signals skip import lines and module paths, config-only accounts say "from config"; accounts.inApp without deletion code stays a medium 5.1.1(v) finding; privacy-manifest --propose builds NSPrivacyCollectedDataTypes from appPrivacy.types and check warns on an empty list; appPrivacy.types is authoritative in the App Privacy sheet (unlisted = not collected), ad / support / AR types default to not collected without tracking or ad SDKs; Play advertising ID from SDKs, the AD_ID permission, app-ads.txt or the flag, never from a Device ID alone; scan.ignore[].evidence and warnings for entries that match nothing; empty credentials template, placeholder values count as missing (check, doctor)

This commit is contained in:
CRTkafa 2026-10-13 05:39:09 +08:00
parent 86cd79dd45
commit 1366e7e231
34 changed files with 881 additions and 71 deletions

View file

@ -77,7 +77,8 @@ describe('appPrivacy.types (the owner\'s answers)', () => {
expect(a.get('type:Gameplay Content')).toMatchObject({ value: false, certain: true });
expect(a.get('linked:Performance Data')).toMatchObject({ value: false, certain: true });
expect(a.get('tracking:Performance Data')).toMatchObject({ value: null, certain: false }); // not set: (you decide)
expect(a.get('type:Customer Support')).toMatchObject({ value: null, certain: false });
// appPrivacy.types is authoritative: a type it does not list is not collected (not asked, not derived)
expect(a.get('type:Customer Support')).toMatchObject({ value: false, certain: true, source: 'appPrivacy.types (not listed: not collected)' });
// the Advertising Data dialog gets a section of its own
expect(formDef('apple-app-privacy', [...a.values()]).sections.map(s => s.title)).toContain('Advertising Data: purposes');
});
@ -86,7 +87,7 @@ describe('appPrivacy.types (the owner\'s answers)', () => {
const a = answers(c);
expect(a.get('collects')).toMatchObject({ value: true, certain: true });
expect(a.get('type:Hands')?.value).toBe(true);
expect(a.get('type:Device ID')).toMatchObject({ value: null, certain: false });
expect(a.get('type:Device ID')).toMatchObject({ value: false, certain: true });
expect(a.get('linked:Hands')?.certain).toBe(false);
const none = { ...base, dataSafety: undefined, appPrivacy: { types: Object.fromEntries(APPLE_DATA_TYPE_IDS.map(id => [id, { collected: false }])) } } as ShipConfig;
expect([...answers(none).values()]).toEqual([expect.objectContaining({ id: 'collects', value: false, certain: true })]);
@ -136,7 +137,7 @@ describe('check: appPrivacy.types against the iOS project', () => {
r = appPrivacyMismatches({ GAMEPLAY_CONTENT: { collected: true, tracking: false } }, { attString: true, manifest: m(true, []) });
expect(msgs(r)).toContain('warn: Info.plist has NSUserTrackingUsageDescription, but appPrivacy.types marks no collected type as used for tracking');
expect(msgs(r)).toContain('has NSPrivacyTracking true, but appPrivacy.types marks no collected type');
expect(msgs(r)).toContain('appPrivacy.types.GAMEPLAY_CONTENT is collected, but App/PrivacyInfo.xcprivacy has no NSPrivacyCollectedDataTypeGameplayContent');
expect(msgs(r)).toContain('App/PrivacyInfo.xcprivacy has an empty NSPrivacyCollectedDataTypes, but appPrivacy.types marks GAMEPLAY_CONTENT as collected');
expect(appPrivacyMismatches({ DEVICE_ID: { collected: true, tracking: true } }, { attString: true, manifest: m(true, ['NSPrivacyCollectedDataTypeDeviceID']) })).toEqual([]);
});
test('storebound check: a missing accounts.deletionUrl is a Data safety problem (Play 400 otherwise)', async () => {