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