|
| 1 | +import { describe, expect, expectTypeOf, test } from 'vitest'; |
| 2 | +import { |
| 3 | + getUnsupportedMacOsSettingMessage, |
| 4 | + isMacOsSettingSupported, |
| 5 | + MACOS_PERMISSION_TARGETS, |
| 6 | + MOBILE_PERMISSION_TARGETS, |
| 7 | + parsePermissionAction, |
| 8 | + parsePermissionTarget, |
| 9 | + PERMISSION_ACTIONS, |
| 10 | + PERMISSION_MODES, |
| 11 | + SETTINGS_INVALID_ARGS_MESSAGE, |
| 12 | + SETTINGS_MACOS_PERMISSION_USAGE, |
| 13 | + SETTINGS_USAGE_OVERRIDE, |
| 14 | + type PermissionAction, |
| 15 | + type PermissionTarget, |
| 16 | +} from './settings.ts'; |
| 17 | + |
| 18 | +// Fixed expected data on purpose (#2614): this file is the witness that a shared permission |
| 19 | +// declaration neither widened nor narrowed what any settings surface already accepted, and that it |
| 20 | +// kept the accepted names in the order `settings` help has always listed them. |
| 21 | +const MOBILE_TARGETS = [ |
| 22 | + 'camera', |
| 23 | + 'microphone', |
| 24 | + 'photos', |
| 25 | + 'contacts', |
| 26 | + 'contacts-limited', |
| 27 | + 'notifications', |
| 28 | + 'calendar', |
| 29 | + 'location', |
| 30 | + 'location-always', |
| 31 | + 'media-library', |
| 32 | + 'motion', |
| 33 | + 'reminders', |
| 34 | + 'siri', |
| 35 | +] as const; |
| 36 | + |
| 37 | +const MACOS_ONLY_TARGETS = ['accessibility', 'screen-recording', 'input-monitoring'] as const; |
| 38 | + |
| 39 | +const SETTINGS_FORMS = [ |
| 40 | + '<wifi|airplane|location> <on|off>', |
| 41 | + 'location set <lat> <lon>', |
| 42 | + 'animations <on|off>', |
| 43 | + 'appearance <light|dark|toggle>', |
| 44 | + 'faceid <match|nonmatch|enroll|unenroll>', |
| 45 | + 'touchid <match|nonmatch|enroll|unenroll>', |
| 46 | + 'fingerprint <match|nonmatch>', |
| 47 | + 'clear-app-state [app-id]', |
| 48 | + 'reset-keychain clear', |
| 49 | + `permission <grant|deny|reset> <${MOBILE_TARGETS.join('|')}> [full|limited]`, |
| 50 | + 'permission <grant|reset> <accessibility|screen-recording|input-monitoring>', |
| 51 | +] as const; |
| 52 | + |
| 53 | +// The whole normalization the parsers promise, written out rather than sampled. |
| 54 | +const NORMALIZATIONS = [ |
| 55 | + (name: string) => name, |
| 56 | + (name: string) => name.toUpperCase(), |
| 57 | + (name: string) => name.charAt(0).toUpperCase() + name.slice(1), |
| 58 | + (name: string) => ` ${name} `, |
| 59 | + (name: string) => `\t${name}\n`, |
| 60 | +] as const; |
| 61 | + |
| 62 | +const REJECTED_TARGETS = [ |
| 63 | + ...MACOS_ONLY_TARGETS, |
| 64 | + 'all', |
| 65 | + 'bluetooth', |
| 66 | + 'camera-x', |
| 67 | + 'camera limited', |
| 68 | + '', |
| 69 | + ' ', |
| 70 | + undefined, |
| 71 | +] as const; |
| 72 | + |
| 73 | +function expectInvalidArgs(run: () => unknown, message: string): void { |
| 74 | + expect(run).toThrow( |
| 75 | + expect.objectContaining({ |
| 76 | + code: 'INVALID_ARGS', |
| 77 | + message: expect.stringContaining(message), |
| 78 | + }), |
| 79 | + ); |
| 80 | +} |
| 81 | + |
| 82 | +describe('the declared permission vocabulary', () => { |
| 83 | + test('holds the names each surface already accepted, in help order', () => { |
| 84 | + expect([...MOBILE_PERMISSION_TARGETS]).toEqual([...MOBILE_TARGETS]); |
| 85 | + expect([...MACOS_PERMISSION_TARGETS]).toEqual([...MACOS_ONLY_TARGETS]); |
| 86 | + expect([...PERMISSION_ACTIONS]).toEqual(['grant', 'deny', 'reset']); |
| 87 | + expect([...PERMISSION_MODES]).toEqual(['full', 'limited']); |
| 88 | + }); |
| 89 | +}); |
| 90 | + |
| 91 | +describe('settings usage and error strings', () => { |
| 92 | + test('help lists every settings form in its documented order', () => { |
| 93 | + expect(SETTINGS_USAGE_OVERRIDE.split(' | ')).toEqual( |
| 94 | + SETTINGS_FORMS.map((form) => `settings ${form}`), |
| 95 | + ); |
| 96 | + }); |
| 97 | + |
| 98 | + test('the invalid-args message lists the same forms, with the last one as an alternative', () => { |
| 99 | + expect(SETTINGS_INVALID_ARGS_MESSAGE).toBe( |
| 100 | + `settings requires ${SETTINGS_FORMS.slice(0, -1).join(', ')}, or ${SETTINGS_FORMS.at(-1)}`, |
| 101 | + ); |
| 102 | + }); |
| 103 | + |
| 104 | + test('the macOS permission form keeps the actions it serves and the names it accepts', () => { |
| 105 | + expect(SETTINGS_MACOS_PERMISSION_USAGE).toBe( |
| 106 | + 'permission <grant|reset> <accessibility|screen-recording|input-monitoring>', |
| 107 | + ); |
| 108 | + }); |
| 109 | + |
| 110 | + test('the macOS guidance names the permission form it supports', () => { |
| 111 | + expect(getUnsupportedMacOsSettingMessage('wifi')).toBe( |
| 112 | + 'Unsupported macOS setting: wifi. macOS supports only settings appearance <light|dark|toggle> ' + |
| 113 | + 'and settings permission <grant|reset> <accessibility|screen-recording|input-monitoring>. ' + |
| 114 | + 'wifi|airplane|location|animations remain unsupported on macOS.', |
| 115 | + ); |
| 116 | + }); |
| 117 | + |
| 118 | + test('only appearance and permission are supported macOS settings', () => { |
| 119 | + expect(isMacOsSettingSupported(' Permission ')).toBe(true); |
| 120 | + expect(isMacOsSettingSupported('appearance')).toBe(true); |
| 121 | + expect(isMacOsSettingSupported('wifi')).toBe(false); |
| 122 | + }); |
| 123 | +}); |
| 124 | + |
| 125 | +describe('parsePermissionTarget', () => { |
| 126 | + test('accepts every mobile target under each normalization', () => { |
| 127 | + for (const target of MOBILE_TARGETS) { |
| 128 | + for (const normalize of NORMALIZATIONS) { |
| 129 | + expect(parsePermissionTarget(normalize(target))).toBe(target); |
| 130 | + } |
| 131 | + } |
| 132 | + }); |
| 133 | + |
| 134 | + test('refuses every name outside the mobile vocabulary, including the macOS-only ones', () => { |
| 135 | + for (const target of REJECTED_TARGETS) { |
| 136 | + expectInvalidArgs( |
| 137 | + () => parsePermissionTarget(target), |
| 138 | + `permission setting requires a target: ${MOBILE_TARGETS.join('|')}`, |
| 139 | + ); |
| 140 | + } |
| 141 | + }); |
| 142 | +}); |
| 143 | + |
| 144 | +describe('parsePermissionAction', () => { |
| 145 | + test('accepts each action under each normalization', () => { |
| 146 | + for (const action of ['grant', 'deny', 'reset']) { |
| 147 | + for (const normalize of NORMALIZATIONS) { |
| 148 | + expect(parsePermissionAction(normalize(action))).toBe(action); |
| 149 | + } |
| 150 | + } |
| 151 | + }); |
| 152 | + |
| 153 | + test('refuses an action outside the vocabulary with the accepted list', () => { |
| 154 | + for (const action of ['allow', 'revoke', '', ' ', 'deny-me']) { |
| 155 | + expectInvalidArgs( |
| 156 | + () => parsePermissionAction(action), |
| 157 | + `Invalid permission action: ${action}. Use grant|deny|reset.`, |
| 158 | + ); |
| 159 | + } |
| 160 | + }); |
| 161 | +}); |
| 162 | + |
| 163 | +// The shared declaration must not move either exported type: these pins are what the public |
| 164 | +// client and the platform owners compile against today. |
| 165 | +describe('permission vocabulary types', () => { |
| 166 | + test('the contract vocabulary stays the mobile subset', () => { |
| 167 | + expectTypeOf<PermissionTarget>().toEqualTypeOf<(typeof MOBILE_TARGETS)[number]>(); |
| 168 | + expectTypeOf<PermissionAction>().toEqualTypeOf<'grant' | 'deny' | 'reset'>(); |
| 169 | + }); |
| 170 | + |
| 171 | + test('the mobile vocabulary does not grow the macOS-only names', () => { |
| 172 | + expectTypeOf<'accessibility'>().not.toMatchTypeOf<PermissionTarget>(); |
| 173 | + expectTypeOf<'screen-recording'>().not.toMatchTypeOf<PermissionTarget>(); |
| 174 | + expectTypeOf<'input-monitoring'>().not.toMatchTypeOf<PermissionTarget>(); |
| 175 | + expectTypeOf<'all'>().not.toMatchTypeOf<PermissionTarget>(); |
| 176 | + }); |
| 177 | +}); |
0 commit comments