test_package_privacy.py (4467B)
1 from __future__ import annotations 2 3 import copy 4 import sys 5 import unittest 6 from pathlib import Path 7 8 SCRIPTS = Path(__file__).resolve().parent 9 if str(SCRIPTS) not in sys.path: 10 sys.path.insert(0, str(SCRIPTS)) 11 12 import package_contract as contract # noqa: E402 13 import package_privacy as privacy # noqa: E402 14 15 16 class PrivacyContractTests(unittest.TestCase): 17 def test_real_manifest_and_permission_contact_inputs_match_the_product( 18 self, 19 ) -> None: 20 root = SCRIPTS.parent 21 contract._validate_privacy( 22 contract._read_plist(root / "Tera/Resources/PrivacyInfo.xcprivacy") 23 ) 24 contract._validate_app_plist(contract._read_plist(root / "Tera/Info.plist")) 25 26 def test_tracking_and_linkage_cannot_hide_behind_numeric_boolean_aliases( 27 self, 28 ) -> None: 29 for numeric in [0, 0.0, 1, 1.0]: 30 for target in ["tracking", "linked", "entry_tracking"]: 31 with self.subTest(numeric=numeric, target=target): 32 value = privacy.manifest() 33 if target == "tracking": 34 value["NSPrivacyTracking"] = numeric 35 elif target == "linked": 36 value["NSPrivacyCollectedDataTypes"][0][ 37 "NSPrivacyCollectedDataTypeLinked" 38 ] = numeric 39 else: 40 value["NSPrivacyCollectedDataTypes"][0][ 41 "NSPrivacyCollectedDataTypeTracking" 42 ] = numeric 43 with self.assertRaises(contract.PackageContractError): 44 contract._validate_privacy(value) 45 46 def test_missing_data_type_reason_or_tracking_declaration_fails_closed( 47 self, 48 ) -> None: 49 base = privacy.manifest() 50 for key in base: 51 with self.subTest(key=key): 52 value = copy.deepcopy(base) 53 del value[key] 54 with self.assertRaises(contract.PackageContractError): 55 contract._validate_privacy(value) 56 for inventory in ["NSPrivacyCollectedDataTypes", "NSPrivacyAccessedAPITypes"]: 57 for index in range(len(base[inventory])): 58 with self.subTest(inventory=inventory, index=index): 59 value = copy.deepcopy(base) 60 value[inventory].pop(index) 61 with self.assertRaises(contract.PackageContractError): 62 contract._validate_privacy(value) 63 64 def test_duplicate_foreign_purpose_or_unreviewed_api_reason_fails_closed( 65 self, 66 ) -> None: 67 for inventory in ["NSPrivacyCollectedDataTypes", "NSPrivacyAccessedAPITypes"]: 68 value = privacy.manifest() 69 value[inventory].append(copy.deepcopy(value[inventory][0])) 70 with self.assertRaises(contract.PackageContractError): 71 contract._validate_privacy(value) 72 for kind, key, replacement in [ 73 ( 74 "NSPrivacyCollectedDataTypes", 75 "NSPrivacyCollectedDataTypePurposes", 76 ["NSPrivacyCollectedDataTypePurposeAnalytics"], 77 ), 78 ( 79 "NSPrivacyAccessedAPITypes", 80 "NSPrivacyAccessedAPITypeReasons", 81 ["3B52.1"], 82 ), 83 ]: 84 value = privacy.manifest() 85 value[kind][0][key] = replacement 86 with self.assertRaises(contract.PackageContractError): 87 contract._validate_privacy(value) 88 89 def test_stale_permission_and_foreign_contact_cannot_pass_nonempty_string_checks( 90 self, 91 ) -> None: 92 root = SCRIPTS.parent 93 base = contract._read_plist(root / "Tera/Info.plist") 94 for key, bad in [ 95 ("NSCameraUsageDescription", "Add a farm photo"), 96 ("TeraSupportEmail", "operator@example.invalid"), 97 ("TeraSupportEmail", None), 98 ]: 99 value = {**base, key: bad} 100 with self.assertRaises(contract.PackageContractError): 101 contract._validate_app_plist(value) 102 103 def test_unsupported_or_nonfinite_values_return_a_stable_rejection(self) -> None: 104 for value in [b"private", float("nan"), float("inf")]: 105 document = privacy.manifest() 106 document["unexpected"] = value 107 with self.assertRaises(contract.PackageContractError): 108 contract._validate_privacy(document)