xref: /expo/apps/test-suite/tests/FileSystem.js (revision 427ff67e)
1'use strict';
2
3import { Asset } from 'expo-asset';
4import * as FS from 'expo-file-system';
5import Constants from 'expo-constants';
6import { Platform } from '@unimodules/core';
7
8export const name = 'FileSystem';
9
10export async function test({ describe, expect, it, ...t }) {
11  describe('FileSystem', () => {
12    const throws = async run => {
13      let error = null;
14      try {
15        await run();
16      } catch (e) {
17        error = e;
18      }
19      expect(error).toBeTruthy();
20    };
21
22    if (Constants.appOwnership === 'expo') {
23      describe('managed workflow', () => {
24        it('throws out-of-scope exceptions', async () => {
25          const p = FS.documentDirectory;
26
27          await throws(() => FS.getInfoAsync(p + '../hello/world'));
28          await throws(() => FS.readAsStringAsync(p + '../hello/world'));
29          await throws(() => FS.writeAsStringAsync(p + '../hello/world', ''));
30          await throws(() => FS.deleteAsync(p + '../hello/world'));
31          await throws(() => FS.deleteAsync(p));
32          await throws(() => FS.deleteAsync(FS.cacheDirectory));
33          await throws(() => FS.moveAsync({ from: p + '../a/b', to: 'c' }));
34          await throws(() => FS.moveAsync({ from: 'c', to: p + '../a/b' }));
35          await throws(() => FS.copyAsync({ from: p + '../a/b', to: 'c' }));
36          await throws(() => FS.copyAsync({ from: 'c', to: p + '../a/b' }));
37          await throws(() => FS.makeDirectoryAsync(p + '../hello/world'));
38          await throws(() => FS.readDirectoryAsync(p + '../hello/world'));
39          await throws(() => FS.downloadAsync('http://www.google.com', p + '../hello/world'));
40          await throws(() => FS.readDirectoryAsync(p + '../'));
41          await throws(() => FS.downloadAsync('http://www.google.com', p + '../hello/world'));
42        });
43      });
44    }
45
46    if (Platform.OS === 'web') {
47      // Web doesn't support FileSystem
48      return;
49    }
50
51    it(
52      'delete(idempotent) -> !exists -> download(md5, uri) -> exists ' + '-> delete -> !exists',
53      async () => {
54        const localUri = FS.documentDirectory + 'download1.png';
55
56        const assertExists = async expectedToExist => {
57          let { exists } = await FS.getInfoAsync(localUri);
58          if (expectedToExist) {
59            expect(exists).toBeTruthy();
60          } else {
61            expect(exists).not.toBeTruthy();
62          }
63        };
64
65        await FS.deleteAsync(localUri, { idempotent: true });
66        await assertExists(false);
67
68        const { md5, headers } = await FS.downloadAsync(
69          'https://s3-us-west-1.amazonaws.com/test-suite-data/avatar2.png',
70          localUri,
71          { md5: true }
72        );
73        expect(md5).toBe('1e02045c10b8f1145edc7c8375998f87');
74        await assertExists(true);
75        expect(headers['Content-Type']).toBe('image/png');
76
77        await FS.deleteAsync(localUri);
78        await assertExists(false);
79      },
80      9000
81    );
82
83    it('Can read/write Base64', async () => {
84      const asset = await Asset.fromModule(require('../assets/icons/app.png'));
85      await asset.downloadAsync();
86
87      for (let startingPosition = 0; startingPosition < 3; startingPosition++) {
88        const options = {
89          encoding: FS.EncodingType.Base64,
90          position: startingPosition,
91          length: startingPosition + 1,
92        };
93
94        const b64 = await FS.readAsStringAsync(asset.localUri, options);
95        expect(b64).toBeDefined();
96        expect(typeof b64).toBe('string');
97        expect(b64.length % 4).toBe(0);
98
99        const localUri = FS.documentDirectory + 'b64.png';
100
101        await FS.writeAsStringAsync(localUri, b64, { encoding: FS.EncodingType.Base64 });
102
103        expect(await FS.readAsStringAsync(localUri, { encoding: FS.EncodingType.Base64 })).toBe(
104          b64
105        );
106      }
107    });
108
109    it('delete(idempotent) -> delete[error]', async () => {
110      const localUri = FS.documentDirectory + 'willDelete.png';
111
112      await FS.deleteAsync(localUri, { idempotent: true });
113
114      let error;
115      try {
116        await FS.deleteAsync(localUri);
117      } catch (e) {
118        error = e;
119      }
120      expect(error.message).toMatch(/not.*found/);
121    });
122
123    it('download(md5, uri) -> read -> delete -> !exists -> read[error]', async () => {
124      const localUri = FS.documentDirectory + 'download1.txt';
125
126      const { md5 } = await FS.downloadAsync(
127        'https://s3-us-west-1.amazonaws.com/test-suite-data/text-file.txt',
128        localUri,
129        { md5: true }
130      );
131      expect(md5).toBe('86d73d2f11e507365f7ea8e7ec3cc4cb');
132
133      const string = await FS.readAsStringAsync(localUri);
134      expect(string).toBe('hello, world\nthis is a test file\n');
135
136      await FS.deleteAsync(localUri, { idempotent: true });
137
138      let error;
139      try {
140        await FS.readAsStringAsync(localUri);
141      } catch (e) {
142        error = e;
143      }
144      expect(error).toBeTruthy();
145    }, 9000);
146
147    it('delete(idempotent) -> !exists -> write -> read -> write -> read', async () => {
148      const localUri = FS.documentDirectory + 'write1.txt';
149
150      await FS.deleteAsync(localUri, { idempotent: true });
151
152      const { exists } = await FS.getInfoAsync(localUri);
153      expect(exists).not.toBeTruthy();
154
155      const writeAndVerify = async expected => {
156        await FS.writeAsStringAsync(localUri, expected);
157        const string = await FS.readAsStringAsync(localUri);
158        expect(string).toBe(expected);
159      };
160
161      await writeAndVerify('hello, world');
162      await writeAndVerify('hello, world!!!!!!');
163    });
164
165    it('delete(new) -> 2 * [write -> move -> !exists(orig) -> read(new)]', async () => {
166      const from = FS.documentDirectory + 'from.txt';
167      const to = FS.documentDirectory + 'to.txt';
168      const contents = ['contents 1', 'contents 2'];
169
170      await FS.deleteAsync(to, { idempotent: true });
171
172      // Move twice to make sure we can overwrite
173      for (let i = 0; i < 2; ++i) {
174        await FS.writeAsStringAsync(from, contents[i]);
175
176        await FS.moveAsync({ from, to });
177
178        const { exists } = await FS.getInfoAsync(from);
179        expect(exists).not.toBeTruthy();
180
181        expect(await FS.readAsStringAsync(to)).toBe(contents[i]);
182      }
183    });
184
185    it('delete(new) -> 2 * [write -> copy -> exists(orig) -> read(new)]', async () => {
186      const from = FS.documentDirectory + 'from.txt';
187      const to = FS.documentDirectory + 'to.txt';
188      const contents = ['contents 1', 'contents 2'];
189
190      await FS.deleteAsync(to, { idempotent: true });
191
192      // Copy twice to make sure we can overwrite
193      for (let i = 0; i < 2; ++i) {
194        await FS.writeAsStringAsync(from, contents[i]);
195
196        await FS.copyAsync({ from, to });
197
198        const { exists } = await FS.getInfoAsync(from);
199        expect(exists).toBeTruthy();
200
201        expect(await FS.readAsStringAsync(to)).toBe(contents[i]);
202      }
203    });
204
205    it(
206      'delete(dir) -> write(dir/file)[error] -> mkdir(dir) ->' +
207        'mkdir(dir)[error] -> write(dir/file) -> read',
208      async () => {
209        let error;
210        const path = FS.documentDirectory + 'dir/file';
211        const dir = FS.documentDirectory + 'dir';
212        const contents = 'hello, world';
213
214        await FS.deleteAsync(dir, { idempotent: true });
215
216        error = null;
217        try {
218          await FS.writeAsStringAsync(path, contents);
219        } catch (e) {
220          error = e;
221        }
222        expect(error).toBeTruthy();
223
224        await FS.makeDirectoryAsync(dir);
225
226        error = null;
227        try {
228          await FS.makeDirectoryAsync(dir);
229        } catch (e) {
230          error = e;
231        }
232        expect(error).toBeTruthy();
233
234        await FS.writeAsStringAsync(path, contents);
235
236        expect(await FS.readAsStringAsync(path)).toBe(contents);
237      }
238    );
239
240    it(
241      'delete(dir) -> write(dir/dir2/file)[error] -> ' +
242        'mkdir(dir/dir2, intermediates) -> ' +
243        'mkdir(dir/dir2, intermediates) -> write(dir/dir2/file) -> read',
244      async () => {
245        let error;
246        const path = FS.documentDirectory + 'dir/dir2/file';
247        const dir = FS.documentDirectory + 'dir/dir2';
248        const contents = 'hello, world';
249
250        await FS.deleteAsync(dir, { idempotent: true });
251
252        error = null;
253        try {
254          await FS.writeAsStringAsync(path, contents);
255        } catch (e) {
256          error = e;
257        }
258        expect(error).toBeTruthy();
259
260        await FS.makeDirectoryAsync(dir, {
261          intermediates: true,
262        });
263
264        error = null;
265        try {
266          await FS.makeDirectoryAsync(dir);
267        } catch (e) {
268          error = e;
269        }
270        expect(error).toBeTruthy();
271
272        error = null;
273        try {
274          await FS.makeDirectoryAsync(dir, {
275            intermediates: true,
276          });
277        } catch (e) {
278          error = e;
279        }
280        expect(error).toBe(null);
281
282        await FS.writeAsStringAsync(path, contents);
283
284        expect(await FS.readAsStringAsync(path)).toBe(contents);
285      }
286    );
287
288    /*
289    This test fails in CI because of an exception being thrown by deleteAsync in the nativeModule.
290    I traced it down to the FileUtils.forceDelete call here:
291    https://github.com/expo/expo/blob/bcd136b096df84e0b0f72a15acbda45491de8201/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemModule.java#L294
292    it(
293      'delete(dir, idempotent) -> make tree -> check contents ' +
294        '-> check directory listings' +
295        '-> move -> check directory listings' +
296        '-> copy -> check directory listings',
297      async () => {
298        let error;
299        const dir = FS.documentDirectory + 'dir';
300
301        await FS.deleteAsync(dir, { idempotent: true });
302
303        await FS.makeDirectoryAsync(dir + '/child1', {
304          intermediates: true,
305        });
306        await FS.makeDirectoryAsync(dir + '/child2', {
307          intermediates: true,
308        });
309
310        await FS.writeAsStringAsync(dir + '/file1', 'contents1');
311        await FS.writeAsStringAsync(dir + '/file2', 'contents2');
312
313        await FS.writeAsStringAsync(dir + '/child1/file3', 'contents3');
314
315        await FS.writeAsStringAsync(dir + '/child2/file4', 'contents4');
316        await FS.writeAsStringAsync(dir + '/child2/file5', 'contents5');
317
318        const checkContents = async (path, contents) =>
319          expect(await FS.readAsStringAsync(path)).toBe(contents);
320
321        await checkContents(dir + '/file1', 'contents1');
322        await checkContents(dir + '/file2', 'contents2');
323        await checkContents(dir + '/child1/file3', 'contents3');
324        await checkContents(dir + '/child2/file4', 'contents4');
325        await checkContents(dir + '/child2/file5', 'contents5');
326
327        const checkDirectory = async (path, expected) => {
328          const list = await FS.readDirectoryAsync(path);
329          expect(list.sort()).toEqual(expected.sort());
330        };
331
332        const checkRoot = async root => {
333          await checkDirectory(root, ['file1', 'file2', 'child1', 'child2']);
334          await checkDirectory(root + '/child1', ['file3']);
335          await checkDirectory(root + '/child2', ['file4', 'file5']);
336
337          error = null;
338          try {
339            await checkDirectory(root + '/file1', ['nope']);
340          } catch (e) {
341            error = e;
342          }
343          expect(error).toBeTruthy();
344        };
345
346        await checkRoot(dir);
347
348        await FS.deleteAsync(FS.documentDirectory + 'moved', {
349          idempotent: true,
350        });
351        await FS.moveAsync({ from: dir, to: FS.documentDirectory + 'moved' });
352        await checkRoot(FS.documentDirectory + 'moved');
353        await FS.copyAsync({
354          from: FS.documentDirectory + 'moved',
355          to: FS.documentDirectory + 'copied',
356        });
357        await checkRoot(FS.documentDirectory + 'copied');
358      }
359    );
360    */
361
362    it('delete(idempotent) -> download(md5) -> getInfo(size)', async () => {
363      const localUri = FS.documentDirectory + 'download1.png';
364
365      await FS.deleteAsync(localUri, { idempotent: true });
366
367      const { md5 } = await FS.downloadAsync(
368        'https://s3-us-west-1.amazonaws.com/test-suite-data/avatar2.png',
369        localUri,
370        { md5: true }
371      );
372      expect(md5).toBe('1e02045c10b8f1145edc7c8375998f87');
373
374      const { size, modificationTime } = await FS.getInfoAsync(localUri);
375      expect(size).toBe(3230);
376      const nowTime = 0.001 * new Date().getTime();
377      expect(nowTime - modificationTime).toBeLessThan(3600);
378
379      await FS.deleteAsync(localUri);
380    }, 30000);
381
382    it('missing parameters', async () => {
383      const p = FS.documentDirectory + 'test';
384
385      await throws(() => FS.moveAsync({ from: p }));
386      await throws(() => FS.moveAsync({ to: p }));
387      await throws(() => FS.copyAsync({ from: p }));
388      await throws(() => FS.copyAsync({ to: p }));
389    });
390
391    it('can read root directories', async () => {
392      await FS.readDirectoryAsync(FS.documentDirectory);
393      await FS.readDirectoryAsync(FS.cacheDirectory);
394    });
395
396    it('download(network failure)', async () => {
397      const localUri = FS.documentDirectory + 'download1.png';
398
399      const assertExists = async expectedToExist => {
400        let { exists } = await FS.getInfoAsync(localUri);
401        if (expectedToExist) {
402          expect(exists).toBeTruthy();
403        } else {
404          expect(exists).not.toBeTruthy();
405        }
406      };
407
408      await FS.deleteAsync(localUri, { idempotent: true });
409      await assertExists(false);
410
411      let error;
412      try {
413        await FS.downloadAsync('https://nonexistent-subdomain.expo.io', localUri, { md5: true });
414      } catch (e) {
415        error = e;
416      }
417      expect(error).toBeTruthy();
418      await assertExists(false);
419      await FS.deleteAsync(localUri, { idempotent: true });
420    }, 30000);
421
422    it('download(404)', async () => {
423      const localUri = FS.documentDirectory + 'download1.png';
424
425      const assertExists = async expectedToExist => {
426        let { exists } = await FS.getInfoAsync(localUri);
427        if (expectedToExist) {
428          expect(exists).toBeTruthy();
429        } else {
430          expect(exists).not.toBeTruthy();
431        }
432      };
433
434      await FS.deleteAsync(localUri, { idempotent: true });
435      await assertExists(false);
436
437      const { status } = await FS.downloadAsync('https://expo.io/404', localUri, {
438        md5: true,
439      });
440      await assertExists(true);
441      expect(status).toBe(404);
442
443      await FS.deleteAsync(localUri);
444      await assertExists(false);
445    }, 30000);
446
447    it('download(nonexistent local path)', async () => {
448      try {
449        const remoteUrl = 'https://s3-us-west-1.amazonaws.com/test-suite-data/avatar2.png';
450        const localUri = FS.documentDirectory + 'doesnt/exists/download1.png';
451        await FS.downloadAsync(remoteUrl, localUri);
452      } catch (err) {
453        expect(err.message).toMatch(/Directory for .* doesn't exist/);
454      }
455    }, 30000);
456
457    it('mkdir(multi-level) + download(multi-level local path)', async () => {
458      const remoteUrl = 'https://s3-us-west-1.amazonaws.com/test-suite-data/avatar2.png';
459      const localDirUri = FS.documentDirectory + 'foo/bar/baz';
460      const localFileUri = localDirUri + 'download1.png';
461
462      await FS.makeDirectoryAsync(localDirUri, { intermediates: true });
463
464      await FS.downloadAsync(remoteUrl, localFileUri);
465    }, 30000);
466  });
467}
468