11import { test } from 'vitest' ;
22import assert from 'node:assert/strict' ;
3- import {
4- androidPriorGrantState ,
5- parseAndroidRuntimePermissionGrants ,
6- } from '../permission-grant-state.ts' ;
3+ import { parseAndroidRuntimePermissionGrants } from '../permission-grant-state.ts' ;
74
85// Captured from `adb shell dumpsys package com.callstack.agentdevicelab` on a Pixel 7 / API 36
96// emulator, trimmed to the sections that decide the answer. The indentation is load-bearing:
@@ -12,9 +9,6 @@ import {
129const DUMPSYS = [
1310 'Packages:' ,
1411 ' Package [com.example.app] (5f3a1c2):' ,
15- ' userId=10234' ,
16- ' declared permissions:' ,
17- ' com.example.app.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION: prot=signature' ,
1812 ' install permissions:' ,
1913 ' android.permission.MODIFY_AUDIO_SETTINGS: granted=true' ,
2014 ' android.permission.RECORD_AUDIO: granted=true' ,
@@ -36,45 +30,29 @@ const DUMPSYS = [
3630 ' path: /data/app/~~abc==/com.example.app-def==' ,
3731] . join ( '\n' ) ;
3832
39- test ( 'runtime grants are read from the requested user only' , ( ) => {
40- const user0 = parseAndroidRuntimePermissionGrants ( DUMPSYS , 0 ) ;
41- assert . equal ( androidPriorGrantState ( user0 , 'android.permission.RECORD_AUDIO' ) , 'not_granted' ) ;
42- assert . equal ( androidPriorGrantState ( user0 , 'android.permission.CAMERA' ) , 'granted' ) ;
43-
44- // The same permission, granted for another profile: `pm revoke` acts on the acting user, so
45- // reporting user 10's grant for a user 0 revoke would claim a kill that never happened.
46- const user10 = parseAndroidRuntimePermissionGrants ( DUMPSYS , 10 ) ;
47- assert . equal ( androidPriorGrantState ( user10 , 'android.permission.RECORD_AUDIO' ) , 'granted' ) ;
48- assert . equal ( androidPriorGrantState ( user10 , 'android.permission.CAMERA' ) , 'unknown' ) ;
33+ // `undefined` is the answer for anything the dump does not place inside the requested user's
34+ // runtime block: an install-permission grant (`pm revoke` cannot touch it), another profile's
35+ // grant, or a permission that user never declared. The caller reports that as `unknown`.
36+ test . each ( [
37+ [ 0 , 'android.permission.RECORD_AUDIO' , 'not_granted' , 'acting user overrides the install grant' ] ,
38+ [ 0 , 'android.permission.CAMERA' , 'granted' , 'acting user' ] ,
39+ [ 10 , 'android.permission.RECORD_AUDIO' , 'granted' , 'other profile, only when asked for' ] ,
40+ [ 10 , 'android.permission.CAMERA' , undefined , 'absent for that user' ] ,
41+ [ 0 , 'android.permission.MODIFY_AUDIO_SETTINGS' , undefined , 'install-only' ] ,
42+ [ 0 , 'android.permission.INTERNET' , undefined , 'never mentioned' ] ,
43+ ] as const ) ( 'user %s reads %s as %s (%s)' , ( userId , permission , expected , _why ) => {
44+ assert . equal ( parseAndroidRuntimePermissionGrants ( DUMPSYS , userId ) ?. get ( permission ) , expected ) ;
4945} ) ;
5046
51- test ( 'install permissions never answer for a runtime permission' , ( ) => {
52- // RECORD_AUDIO appears as `granted=true` in the install-permission section and `granted=false`
53- // in user 0's runtime block. A scan that matched `granted=true` anywhere read the wrong one.
54- const grants = parseAndroidRuntimePermissionGrants ( DUMPSYS , 0 ) ;
55- assert . equal ( androidPriorGrantState ( grants , 'android.permission.RECORD_AUDIO' ) , 'not_granted' ) ;
56- assert . equal (
57- androidPriorGrantState ( grants , 'android.permission.MODIFY_AUDIO_SETTINGS' ) ,
58- 'unknown' ,
59- ) ;
60- assert . equal ( androidPriorGrantState ( grants , 'android.permission.INTERNET' ) , 'unknown' ) ;
61- } ) ;
62-
63- test ( 'a user with no runtime-permission block is unknown, not empty' , ( ) => {
64- assert . equal ( parseAndroidRuntimePermissionGrants ( DUMPSYS , 11 ) , undefined ) ;
65- assert . equal (
66- androidPriorGrantState (
67- parseAndroidRuntimePermissionGrants ( DUMPSYS , 11 ) ,
68- 'android.permission.RECORD_AUDIO' ,
69- ) ,
70- 'unknown' ,
71- ) ;
72- } ) ;
73-
74- test ( 'unparseable output is unknown rather than not-granted' , ( ) => {
75- for ( const output of [ '' , 'Packages:\n <none>' , 'Error: package not found' ] ) {
76- assert . equal ( parseAndroidRuntimePermissionGrants ( output , 0 ) , undefined , output ) ;
77- }
47+ // A missing block is not an empty one. Every input here means "the device did not tell us",
48+ // which must stay distinguishable from "the app holds nothing".
49+ test . each ( [
50+ [ 'no runtime block for that user' , DUMPSYS , 11 ] ,
51+ [ 'empty output' , '' , 0 ] ,
52+ [ 'no Packages section' , 'Activity Resolver Table:\n User 0:\n runtime permissions:' , 0 ] ,
53+ [ 'a package section without the user' , 'Packages:\n Package [com.example.app] (abc):' , 0 ] ,
54+ ] as const ) ( '%s reads as unknown' , ( _label , output , userId ) => {
55+ assert . equal ( parseAndroidRuntimePermissionGrants ( output , userId ) , undefined ) ;
7856} ) ;
7957
8058test ( 'an empty runtime block answers not_granted for everything it could have listed' , ( ) => {
@@ -94,23 +72,6 @@ test('an empty runtime block answers not_granted for everything it could have li
9472 assert . equal ( grants ?. size , 0 ) ;
9573} ) ;
9674
97- test ( 'a dump without a Packages section is unknown' , ( ) => {
98- // Every `dumpsys package <pkg>` carries one (it sits after the resolver tables and Key Set
99- // Manager). Its absence means the output is not the dump we can read, so the honest answer
100- // is unknown — never the not_granted that would claim the app was left alone.
101- const grants = parseAndroidRuntimePermissionGrants (
102- [
103- 'Activity Resolver Table:' ,
104- ' Non-Data Actions:' ,
105- ' User 0: installed=true' ,
106- ' runtime permissions:' ,
107- ' android.permission.RECORD_AUDIO: granted=true, flags=[ USER_SET]' ,
108- ] . join ( '\n' ) ,
109- 0 ,
110- ) ;
111- assert . equal ( grants , undefined ) ;
112- } ) ;
113-
11475test ( 'sections after Packages: cannot reopen the scan' , ( ) => {
11576 // `Queries:` repeats `User 0:` with no grants, and `Shared users:` repeats runtime grant lines
11677 // for the shared uid. Merging either would flip RECORD_AUDIO to granted for a user whose own
@@ -127,6 +88,6 @@ test('sections after Packages: cannot reopen the scan', () => {
12788 0 ,
12889 ) ;
12990
130- assert . equal ( androidPriorGrantState ( grants , 'android.permission.RECORD_AUDIO' ) , 'not_granted' ) ;
131- assert . equal ( androidPriorGrantState ( grants , 'android.permission.CAMERA' ) , 'granted' ) ;
91+ assert . equal ( grants ?. get ( 'android.permission.RECORD_AUDIO' ) , 'not_granted' ) ;
92+ assert . equal ( grants ?. get ( 'android.permission.CAMERA' ) , 'granted' ) ;
13293} ) ;
0 commit comments