1import rnFixture from '../../plugins/__tests__/fixtures/react-native-project';
2import { format } from '../../utils/XML';
3import * as XML from '../../utils/XML';
4import { AndroidManifest } from '../Manifest';
5import {
6  addBlockedPermissions,
7  ensurePermission,
8  ensurePermissions,
9  getAndroidPermissions,
10  getPermissions,
11  removePermissions,
12  setAndroidPermissions,
13  withInternalBlockedPermissions,
14} from '../Permissions';
15
16async function getFixtureManifestAsync() {
17  const manifest = (await XML.parseXMLAsync(
18    rnFixture['android/app/src/main/AndroidManifest.xml']
19  )) as AndroidManifest;
20
21  removePermissions(manifest, [
22    'android.permission.SYSTEM_ALERT_WINDOW',
23    'android.permission.VIBRATE',
24    'android.permission.READ_EXTERNAL_STORAGE',
25    'android.permission.WRITE_EXTERNAL_STORAGE',
26  ]);
27  return manifest;
28}
29
30describe(withInternalBlockedPermissions, () => {
31  it(`adds blocked permissions`, async () => {
32    const config = withInternalBlockedPermissions({
33      slug: '',
34      name: '',
35      android: {
36        blockedPermissions: ['android.permission.ACCESS_FINE_LOCATION', 'OTHER'],
37      },
38    });
39
40    const { modResults } = await (config as any).mods.android.manifest({
41      modRequest: {},
42      modResults: await getFixtureManifestAsync(),
43    });
44
45    expect(modResults).toEqual({
46      manifest: {
47        $: {
48          'xmlns:android': expect.any(String),
49          // Added tools
50          'xmlns:tools': 'http://schemas.android.com/tools',
51        },
52        'uses-permission': [
53          expect.anything(),
54          // Added two blocked permissions
55          {
56            $: {
57              'android:name': 'android.permission.ACCESS_FINE_LOCATION',
58              'tools:node': 'remove',
59            },
60          },
61          {
62            $: {
63              'android:name': 'android.permission.OTHER',
64              'tools:node': 'remove',
65            },
66          },
67        ],
68        queries: expect.anything(),
69        application: expect.anything(),
70      },
71    });
72  });
73
74  it(`does not add tools if there are no blocked permissions`, async () => {
75    const config = withInternalBlockedPermissions({
76      slug: '',
77      name: '',
78      android: {
79        blockedPermissions: [],
80      },
81    });
82
83    // Doesn't even add the mod
84    expect((config as any).mods).not.toBeDefined();
85  });
86
87  it(`adds blocked permission when using short notation`, async () => {
88    const config = withInternalBlockedPermissions({
89      slug: '',
90      name: '',
91      android: {
92        permissions: ['android.permission.ACCESS_FINE_LOCATION'],
93        blockedPermissions: ['ACCESS_FINE_LOCATION'],
94      },
95    });
96
97    const { modResults } = await (config as any).mods.android.manifest({
98      modRequest: {},
99      modResults: await getFixtureManifestAsync(),
100    });
101
102    expect(modResults).toEqual({
103      manifest: {
104        $: {
105          'xmlns:android': expect.any(String),
106          // Added tools
107          'xmlns:tools': 'http://schemas.android.com/tools',
108        },
109        'uses-permission': [
110          expect.anything(),
111
112          // Added two blocked permissions
113          {
114            $: {
115              'android:name': 'android.permission.ACCESS_FINE_LOCATION',
116              'tools:node': 'remove',
117            },
118          },
119        ],
120        queries: expect.anything(),
121        application: expect.anything(),
122      },
123    });
124  });
125
126  it(`adds blocked permission when using long notation`, async () => {
127    const config = withInternalBlockedPermissions({
128      slug: '',
129      name: '',
130      android: {
131        permissions: ['ACCESS_FINE_LOCATION'],
132        blockedPermissions: ['android.permission.ACCESS_FINE_LOCATION'],
133      },
134    });
135
136    const { modResults } = await (config as any).mods.android.manifest({
137      modRequest: {},
138      modResults: await getFixtureManifestAsync(),
139    });
140
141    expect(modResults).toEqual({
142      manifest: {
143        $: {
144          'xmlns:android': expect.any(String),
145          // Added tools
146          'xmlns:tools': 'http://schemas.android.com/tools',
147        },
148        'uses-permission': [
149          expect.anything(),
150          // Added two blocked permissions
151          {
152            $: {
153              'android:name': 'android.permission.ACCESS_FINE_LOCATION',
154              'tools:node': 'remove',
155            },
156          },
157        ],
158        queries: expect.anything(),
159        application: expect.anything(),
160      },
161    });
162  });
163});
164
165describe(addBlockedPermissions, () => {
166  it(`restricts an existing permission`, () => {
167    expect(
168      addBlockedPermissions(
169        {
170          manifest: {
171            $: {
172              'xmlns:android': '...',
173            },
174            'uses-permission': [
175              {
176                $: { 'android:name': 'dev.expo.foobar' },
177              },
178              {
179                $: { 'android:name': 'dev.expo.foobar-2' },
180              },
181            ],
182          },
183        },
184        ['dev.expo.foobar']
185      ).manifest['uses-permission']
186    ).toStrictEqual([
187      {
188        $: { 'android:name': 'dev.expo.foobar-2' },
189      },
190      {
191        $: { 'android:name': 'dev.expo.foobar', 'tools:node': 'remove' },
192      },
193    ]);
194  });
195
196  it(`restricts a new permission`, () => {
197    expect(
198      addBlockedPermissions(
199        {
200          manifest: {
201            $: {
202              'xmlns:android': '...',
203            },
204            'uses-permission': [],
205          },
206        },
207        ['dev.expo.foobar']
208      ).manifest['uses-permission']
209    ).toStrictEqual([
210      {
211        $: { 'android:name': 'dev.expo.foobar', 'tools:node': 'remove' },
212      },
213    ]);
214  });
215});
216
217describe('Android permissions', () => {
218  it(`returns empty array if no android permissions key is provided`, () => {
219    expect(getAndroidPermissions({})).toMatchObject([]);
220  });
221
222  it(`returns android permissions if array is provided`, () => {
223    expect(
224      getAndroidPermissions({ android: { permissions: ['CAMERA', 'RECORD_AUDIO'] } })
225    ).toMatchObject(['CAMERA', 'RECORD_AUDIO']);
226  });
227
228  it('adds permissions if not present, does not duplicate permissions', async () => {
229    const givenPermissions = [
230      'android.permission.READ_CONTACTS',
231      'com.android.launcher.permission.INSTALL_SHORTCUT',
232      'com.android.launcher.permission.INSTALL_SHORTCUT',
233    ];
234    let androidManifestJson = await getFixtureManifestAsync();
235    androidManifestJson = await setAndroidPermissions(
236      { android: { permissions: givenPermissions } },
237      androidManifestJson
238    );
239
240    const manifestPermissionsJSON = androidManifestJson.manifest['uses-permission'];
241    const manifestPermissions = manifestPermissionsJSON!.map((e) => e.$['android:name']);
242
243    // Account for INTERNET permission in fixture
244    // No duplicates
245    expect(manifestPermissions).toStrictEqual([
246      'android.permission.INTERNET',
247
248      'android.permission.READ_CONTACTS',
249      'com.android.launcher.permission.INSTALL_SHORTCUT',
250    ]);
251    expect(
252      manifestPermissions.filter((e) => e === 'com.android.launcher.permission.INSTALL_SHORTCUT')
253    ).toHaveLength(1);
254  });
255});
256
257describe('Permissions', () => {
258  it(`adds a new permission`, async () => {
259    const manifest = await getFixtureManifestAsync();
260    const didAdd = ensurePermission(manifest, 'EXPO_TEST_PERMISSION');
261    const permissions = getPermissions(manifest);
262    expect(didAdd).toBe(true);
263    expect(permissions).toContain('android.permission.EXPO_TEST_PERMISSION');
264    expect(permissions.length).toBe(2);
265  });
266
267  it(`prevents adding a duplicate permission`, async () => {
268    const manifest = await getFixtureManifestAsync();
269    const didAdd = ensurePermission(manifest, 'INTERNET');
270    const permissions = getPermissions(manifest);
271    expect(didAdd).toBe(false);
272    expect(permissions).toContain('android.permission.INTERNET');
273    expect(permissions.length).toBe(1);
274  });
275
276  it(`ensures multiple permissions`, async () => {
277    const manifest = await getFixtureManifestAsync();
278    const permissionsToAdd = ['VALUE_1', 'VALUE_2'];
279    const results = ensurePermissions(manifest, permissionsToAdd);
280    expect(results).toMatchSnapshot();
281    expect(Object.values(results)).toStrictEqual([true, true]);
282
283    expect(getPermissions(manifest).length).toBe(3);
284  });
285
286  it(`removes permissions by name`, async () => {
287    const manifest = await getFixtureManifestAsync();
288    expect(ensurePermission(manifest, 'VALUE_TO_REMOVE_1')).toBe(true);
289    expect(ensurePermission(manifest, 'VALUE_TO_REMOVE_2')).toBe(true);
290    expect(getPermissions(manifest).length).toBe(3);
291
292    removePermissions(manifest, ['VALUE_TO_REMOVE_1', 'VALUE_TO_REMOVE_2']);
293    expect(getPermissions(manifest).length).toBe(1);
294  });
295
296  it(`removes all permissions`, async () => {
297    const manifest = await getFixtureManifestAsync();
298    expect(ensurePermission(manifest, 'VALUE_TO_REMOVE_1')).toBe(true);
299    expect(ensurePermission(manifest, 'VALUE_TO_REMOVE_2')).toBe(true);
300    expect(getPermissions(manifest).length).toBe(3);
301
302    removePermissions(manifest);
303
304    expect(getPermissions(manifest).length).toBe(0);
305  });
306
307  it(`can write with a pretty format`, async () => {
308    const manifest = await getFixtureManifestAsync();
309    expect(ensurePermission(manifest, 'NEW_PERMISSION_1')).toBe(true);
310    expect(ensurePermission(manifest, 'NEW_PERMISSION_2')).toBe(true);
311    expect(getPermissions(manifest).length).toBe(3);
312
313    expect(format(manifest)).toMatchSnapshot();
314  });
315});
316