diff --git a/packages/storage/e2e/StorageReference.e2e.js b/packages/storage/e2e/StorageReference.e2e.js deleted file mode 100644 index 18c7046f07..0000000000 --- a/packages/storage/e2e/StorageReference.e2e.js +++ /dev/null @@ -1,681 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -const { PATH, seed, WRITE_ONLY_NAME } = require('./helpers'); - -describe('storage() -> StorageReference', function () { - before(async function () { - await seed(PATH); - }); - - describe('toString()', function () { - it('returns the correct bucket path to the file', function () { - const app = firebase.app(); - firebase - .storage() - .ref('/uploadNope.jpeg') - .toString() - .should.equal(`gs://${app.options.storageBucket}/uploadNope.jpeg`); - }); - }); - - describe('properties', function () { - describe('fullPath', function () { - it('returns the full path as a string', function () { - firebase.storage().ref('/foo/uploadNope.jpeg').fullPath.should.equal('foo/uploadNope.jpeg'); - firebase.storage().ref('foo/uploadNope.jpeg').fullPath.should.equal('foo/uploadNope.jpeg'); - }); - }); - - describe('storage', function () { - it('returns the instance of storage', function () { - firebase.storage().ref('/foo/uploadNope.jpeg').storage.ref.should.be.a.Function(); - }); - }); - - describe('bucket', function () { - it('returns the storage bucket as a string', function () { - const app = firebase.app(); - firebase - .storage() - .ref('/foo/uploadNope.jpeg') - .bucket.should.equal(app.options.storageBucket); - }); - }); - - describe('name', function () { - it('returns the file name as a string', function () { - const ref = firebase.storage().ref('/foo/uploadNope.jpeg'); - ref.name.should.equal('uploadNope.jpeg'); - }); - }); - - describe('parent', function () { - it('returns the parent directory as a reference', function () { - firebase.storage().ref('/foo/uploadNope.jpeg').parent.fullPath.should.equal('foo'); - }); - - it('returns null if already at root', function () { - const ref = firebase.storage().ref('/'); - should.equal(ref.parent, null); - }); - }); - - describe('root', function () { - it('returns a reference to the root of the bucket', function () { - firebase.storage().ref('/foo/uploadNope.jpeg').root.fullPath.should.equal('/'); - }); - }); - }); - - describe('child()', function () { - it('returns a reference to a child path', function () { - const parentRef = firebase.storage().ref('/foo'); - const childRef = parentRef.child('someFile.json'); - childRef.fullPath.should.equal('foo/someFile.json'); - }); - }); - - describe('delete()', function () { - it('should delete a file', async function () { - const storageReference = firebase.storage().ref(`${PATH}/deleteMe.txt`); - await storageReference.putString('Delete File'); - await storageReference.delete(); - - try { - await storageReference.getMetadata(); - return Promise.reject(new Error('Did not throw')); - } catch (error) { - error.code.should.equal('storage/object-not-found'); - error.message.should.equal( - '[storage/object-not-found] No object exists at the desired reference.', - ); - return Promise.resolve(); - } - }); - - it('throws error if file does not exist', async function () { - const storageReference = firebase.storage().ref(`${PATH}/iDoNotExist.txt`); - - try { - await storageReference.delete(); - return Promise.reject(new Error('Did not throw')); - } catch (error) { - error.code.should.equal('storage/object-not-found'); - error.message.should.equal( - '[storage/object-not-found] No object exists at the desired reference.', - ); - return Promise.resolve(); - } - }); - - it('throws error if no write permission', async function () { - const storageReference = firebase.storage().ref('/uploadNope.jpeg'); - - try { - await storageReference.delete(); - return Promise.reject(new Error('Did not throw')); - } catch (error) { - error.code.should.equal('storage/unauthorized'); - error.message.should.equal( - '[storage/unauthorized] User is not authorized to perform the desired action.', - ); - return Promise.resolve(); - } - }); - }); - - describe('getDownloadURL', function () { - it('should return a download url for a file', async function () { - // This is frequently flaky in CI - but works sometimes. Skipping only in CI for now. - // Disabled for iOS pending: https://github.com/firebase/firebase-ios-sdk/pull/10370 - if (!isCI && device.getPlatform() !== 'ios') { - const storageReference = firebase.storage().ref(`${PATH}/list/file1.txt`); - const downloadUrl = await storageReference.getDownloadURL(); - downloadUrl.should.be.a.String(); - downloadUrl.should.containEql('file1.txt'); - downloadUrl.should.containEql(firebase.app().options.projectId); - } else { - this.skip(); - } - }); - - it('throws error if file does not exist', async function () { - const storageReference = firebase.storage().ref(`${PATH}/iDoNotExist.txt`); - - try { - await storageReference.getDownloadURL(); - return Promise.reject(new Error('Did not throw')); - } catch (error) { - error.code.should.equal('storage/object-not-found'); - error.message.should.equal( - '[storage/object-not-found] No object exists at the desired reference.', - ); - return Promise.resolve(); - } - }); - - it('throws error if no read permission', async function () { - const storageReference = firebase.storage().ref(WRITE_ONLY_NAME); - - try { - await storageReference.getDownloadURL(); - return Promise.reject(new Error('Did not throw')); - } catch (error) { - error.code.should.equal('storage/unauthorized'); - error.message.should.equal( - '[storage/unauthorized] User is not authorized to perform the desired action.', - ); - return Promise.resolve(); - } - }); - }); - - describe('getMetadata', function () { - it('should return a metadata for a file', async function () { - const storageReference = firebase.storage().ref(`${PATH}/list/file1.txt`); - const metadata = await storageReference.getMetadata(); - metadata.generation.should.be.a.String(); - metadata.fullPath.should.equal(`${PATH}/list/file1.txt`); - if (device.getPlatform() === 'android') { - metadata.name.should.equal('file1.txt'); - } else { - // FIXME on ios file comes through as fully-qualified - // https://github.com/firebase/firebase-ios-sdk/issues/9849#issuecomment-1159819958 - metadata.name.should.equal(`${PATH}/list/file1.txt`); - } - metadata.size.should.be.a.Number(); - should.equal(metadata.size > 0, true); - metadata.updated.should.be.a.String(); - metadata.timeCreated.should.be.a.String(); - metadata.contentEncoding.should.be.a.String(); - metadata.contentDisposition.should.be.a.String(); - metadata.contentType.should.equal('text/plain'); - metadata.bucket.should.equal(`${firebase.app().options.projectId}.appspot.com`); - metadata.metageneration.should.be.a.String(); - metadata.md5Hash.should.be.a.String(); - // TODO against cloud storage cacheControl comes back null/undefined by default. Emulator has a difference - // https://github.com/firebase/firebase-tools/issues/3398#issuecomment-1159821364 - // should.equal(metadata.cacheControl, undefined); - should.equal(metadata.contentLanguage, null); - // should.equal(metadata.customMetadata, null); - }); - }); - - describe('list', function () { - it('should return list results', async function () { - const storageReference = firebase.storage().ref(`${PATH}/list`); - const result = await storageReference.list(); - - result.constructor.name.should.eql('StorageListResult'); - result.should.have.property('nextPageToken'); - - result.items.should.be.Array(); - result.items.length.should.be.greaterThan(0); - result.items[0].constructor.name.should.eql('StorageReference'); - - result.prefixes.should.be.Array(); - result.prefixes.length.should.be.greaterThan(0); - result.prefixes[0].constructor.name.should.eql('StorageReference'); - }); - - it('throws if options is not an object', function () { - try { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - storageReference.list(123); - return Promise.reject(new Error('Did not throw')); - } catch (error) { - error.message.should.containEql("'options' expected an object value"); - return Promise.resolve(); - } - }); - - describe('maxResults', function () { - it('should limit with maxResults are passed', async function () { - const storageReference = firebase.storage().ref(`${PATH}/list`); - const result = await storageReference.list({ - maxResults: 1, - }); - - result.nextPageToken.should.be.String(); - - result.items.should.be.Array(); - result.items.length.should.eql(1); - result.items[0].constructor.name.should.eql('StorageReference'); - - result.prefixes.should.be.Array(); - // todo length? - }); - - it('throws if maxResults is not a number', function () { - try { - const storageReference = firebase.storage().ref(`${PATH}/list`); - storageReference.list({ - maxResults: '123', - }); - return Promise.reject(new Error('Did not throw')); - } catch (error) { - error.message.should.containEql("'options.maxResults' expected a number value"); - return Promise.resolve(); - } - }); - - it('throws if maxResults is not a valid number', function () { - try { - const storageReference = firebase.storage().ref(`${PATH}/list`); - storageReference.list({ - maxResults: 2000, - }); - return Promise.reject(new Error('Did not throw')); - } catch (error) { - error.message.should.containEql( - "'options.maxResults' expected a number value between 1-1000", - ); - return Promise.resolve(); - } - }); - }); - - describe('pageToken', function () { - it('throws if pageToken is not a string', function () { - try { - const storageReference = firebase.storage().ref(`${PATH}/list`); - storageReference.list({ - pageToken: 123, - }); - return Promise.reject(new Error('Did not throw')); - } catch (error) { - error.message.should.containEql("'options.pageToken' expected a string value"); - return Promise.resolve(); - } - }); - - it('should return and use a page token', async function () { - const storageReference = firebase.storage().ref(`${PATH}/list`); - const result1 = await storageReference.list({ - maxResults: 1, - }); - - const item1 = result1.items[0].fullPath; - - const result2 = await storageReference.list({ - maxResults: 1, - pageToken: result1.nextPageToken, - }); - - const item2 = result2.items[0].fullPath; - - if (item1 === item2) { - throw new Error('Expected item results to be different.'); - } - }); - }); - }); - - describe('listAll', function () { - it('should return all results', async function () { - const storageReference = firebase.storage().ref(`${PATH}/list`); - const result = await storageReference.listAll(); - - should.equal(result.nextPageToken, null); - - result.items.should.be.Array(); - result.items.length.should.be.greaterThan(0); - result.items[0].constructor.name.should.eql('StorageReference'); - - result.prefixes.should.be.Array(); - result.prefixes.length.should.be.greaterThan(0); - result.prefixes[0].constructor.name.should.eql('StorageReference'); - }); - - it('should not crash if the user is not allowed to list the directory', async function () { - const storageReference = firebase.storage().ref('/forbidden'); - try { - await storageReference.listAll(); - return Promise.reject(new Error('listAll on a forbidden directory succeeded')); - } catch (error) { - error.code.should.equal('storage/unauthorized'); - error.message.should.equal( - '[storage/unauthorized] User is not authorized to perform the desired action.', - ); - return Promise.resolve(); - } - }); - }); - - describe('updateMetadata', function () { - it('should return the updated metadata for a file', async function () { - const storageReference = firebase.storage().ref(`${PATH}/list/file1.txt`); - let metadata = await storageReference.updateMetadata({ - cacheControl: 'cache-control', - contentDisposition: 'content-disposition', - contentEncoding: 'application/octet-stream', - contentLanguage: 'de', - contentType: 'content-type-a', - customMetadata: { - a: 'b', - y: 'z', - }, - }); - - // Things that are set automagically for us - metadata.generation.should.be.a.String(); - metadata.fullPath.should.equal(`${PATH}/list/file1.txt`); - if (device.getPlatform() === 'android') { - metadata.name.should.equal('file1.txt'); - } else { - // FIXME on ios file comes through as fully-qualified - // https://github.com/firebase/firebase-ios-sdk/issues/9849#issuecomment-1159819958 - metadata.name.should.equal(`${PATH}/list/file1.txt`); - } - metadata.size.should.be.a.Number(); - should.equal(metadata.size > 0, true); - metadata.updated.should.be.a.String(); - metadata.timeCreated.should.be.a.String(); - metadata.metageneration.should.be.a.String(); - metadata.md5Hash.should.be.a.String(); - metadata.bucket.should.equal(`${firebase.app().options.projectId}.appspot.com`); - - // Things we just updated - metadata.cacheControl.should.equals('cache-control'); - metadata.contentDisposition.should.equal('content-disposition'); - metadata.contentEncoding.should.equal('application/octet-stream'); - metadata.contentLanguage.should.equal('de'); - metadata.contentType.should.equal('content-type-a'); - metadata.customMetadata.should.be.an.Object(); - metadata.customMetadata.a.should.equal('b'); - metadata.customMetadata.y.should.equal('z'); - Object.keys(metadata.customMetadata).length.should.equal(2); - - // Now let's make sure removing metadata works - metadata = await storageReference.updateMetadata({ - contentType: null, - cacheControl: null, - contentDisposition: null, - contentEncoding: null, - contentLanguage: null, - customMetadata: null, - }); - - // Things that are set automagically for us and are not updatable - metadata.generation.should.be.a.String(); - metadata.fullPath.should.equal(`${PATH}/list/file1.txt`); - if (device.getPlatform() === 'android') { - metadata.name.should.equal('file1.txt'); - } else { - // FIXME on ios file comes through as fully-qualified - // https://github.com/firebase/firebase-ios-sdk/issues/9849#issuecomment-1159819958 - metadata.name.should.equal(`${PATH}/list/file1.txt`); - } - metadata.size.should.be.a.Number(); - should.equal(metadata.size > 0, true); - metadata.updated.should.be.a.String(); - metadata.timeCreated.should.be.a.String(); - metadata.metageneration.should.be.a.String(); - metadata.md5Hash.should.be.a.String(); - metadata.bucket.should.equal(`${firebase.app().options.projectId}.appspot.com`); - - // Things that we may set (or remove) - should.equal(metadata.cacheControl, undefined); - should.equal(metadata.contentDisposition, undefined); - should.equal(metadata.contentEncoding, 'identity'); - should.equal(metadata.contentLanguage, undefined); - should.equal(metadata.contentType, undefined); - should.equal(metadata.customMetadata, undefined); - }); - - it('should set update or remove customMetadata properties correctly', async function () { - const storageReference = firebase.storage().ref(`${PATH}/list/file1.txt`); - let metadata = await storageReference.updateMetadata({ - contentType: 'application/octet-stream', - customMetadata: { - keepMe: 'please', - removeMeFirstTime: 'please', - removeMeSecondTime: 'please', - }, - }); - - Object.keys(metadata.customMetadata).length.should.equal(3); - metadata.customMetadata.keepMe.should.equal('please'); - metadata.customMetadata.removeMeFirstTime.should.equal('please'); - metadata.customMetadata.removeMeSecondTime.should.equal('please'); - - metadata = await storageReference.updateMetadata({ - contentType: 'application/octet-stream', - customMetadata: { - keepMe: 'please', - removeMeFirstTime: null, - removeMeSecondTime: 'please', - }, - }); - - Object.keys(metadata.customMetadata).length.should.equal(2); - metadata.customMetadata.keepMe.should.equal('please'); - metadata.customMetadata.removeMeSecondTime.should.equal('please'); - - metadata = await storageReference.updateMetadata({ - contentType: 'application/octet-stream', - customMetadata: { - keepMe: 'please', - }, - }); - - Object.keys(metadata.customMetadata).length.should.equal(1); - metadata.customMetadata.keepMe.should.equal('please'); - }); - - it('should error if updateMetadata includes md5hash', async function () { - const storageReference = firebase.storage().ref(`${PATH}/list/file1.txt`); - try { - await storageReference.updateMetadata({ - md5hash: '0xDEADBEEF', - }); - return Promise.reject(new Error('Did not throw on invalid updateMetadata')); - } catch (e) { - e.message.should.containEql('md5hash may only be set on upload, not on updateMetadata'); - return Promise.resolve(); - } - }); - }); - - describe('putFile', function () { - it('errors if file path is not a string', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - storageReference.putFile(1337); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql('expects a string value'); - return Promise.resolve(); - } - }); - - it('errors if metadata is not an object', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - storageReference.putFile('foo', 123); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql('must be an object value'); - return Promise.resolve(); - } - }); - - it('errors if metadata contains an unsupported property', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - storageReference.putFile('foo', { foo: true }); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql("unknown property 'foo'"); - return Promise.resolve(); - } - }); - - it('errors if metadata property value is not a string or null value', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - storageReference.putFile('foo', { contentType: true }); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql('should be a string or null value'); - return Promise.resolve(); - } - }); - - it('errors if metadata.customMetadata is not an object', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - storageReference.putFile('foo', { customMetadata: true }); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql( - 'customMetadata must be an object of keys and string values', - ); - return Promise.resolve(); - } - }); - - // TODO check an metaData:md5hash property passes through correcty on putFile - }); - - describe('putString', function () { - it('errors if metadata is not an object', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - storageReference.putString('foo', 'raw', 123); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql('must be an object value'); - return Promise.resolve(); - } - }); - - it('errors if metadata contains an unsupported property', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - storageReference.putString('foo', 'raw', { foo: true }); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql("unknown property 'foo'"); - return Promise.resolve(); - } - }); - - it('errors if metadata property value is not a string or null value', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - storageReference.putString('foo', 'raw', { contentType: true }); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql('should be a string or null value'); - return Promise.resolve(); - } - }); - - it('errors if metadata.customMetadata is not an object', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - storageReference.putString('foo', 'raw', { customMetadata: true }); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql( - 'customMetadata must be an object of keys and string values', - ); - return Promise.resolve(); - } - }); - - it('allows valid metadata properties for upload', async function () { - const storageReference = firebase.storage().ref(`${PATH}/metadataTest.txt`); - await storageReference.putString('foo', 'raw', { - contentType: 'text/plain', - md5hash: '123412341234', - cacheControl: 'true', - contentDisposition: 'disposed', - contentEncoding: 'application/octet-stream', - contentLanguage: 'de', - customMetadata: { - customMetadata1: 'metadata1value', - }, - }); - }); - }); - - describe('put', function () { - it('errors if metadata is not an object', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - storageReference.put(new jet.context.window.ArrayBuffer(), 123); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql('must be an object value'); - return Promise.resolve(); - } - }); - - it('errors if metadata contains an unsupported property', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - storageReference.put(new jet.context.window.ArrayBuffer(), { foo: true }); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql("unknown property 'foo'"); - return Promise.resolve(); - } - }); - - it('errors if metadata property value is not a string or null value', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - storageReference.put(new jet.context.window.ArrayBuffer(), { contentType: true }); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql('should be a string or null value'); - return Promise.resolve(); - } - }); - - it('errors if metadata.customMetadata is not an object', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - storageReference.put(new jet.context.window.ArrayBuffer(), { customMetadata: true }); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql( - 'customMetadata must be an object of keys and string values', - ); - return Promise.resolve(); - } - }); - - it('allows valid metadata properties for upload', async function () { - const storageReference = firebase.storage().ref(`${PATH}/metadataTest.jpeg`); - await storageReference.put(new jet.context.window.ArrayBuffer(), { - contentType: 'image/jpg', - md5hash: '123412341234', - cacheControl: 'true', - contentDisposition: 'disposed', - contentEncoding: 'application/octet-stream', - contentLanguage: 'de', - customMetadata: { - customMetadata1: 'metadata1value', - }, - }); - }); - }); -}); diff --git a/packages/storage/e2e/StorageReference.modular.e2e.js b/packages/storage/e2e/StorageReference.modular.e2e.js new file mode 100644 index 0000000000..d88f563855 --- /dev/null +++ b/packages/storage/e2e/StorageReference.modular.e2e.js @@ -0,0 +1,1364 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +const { PATH, seed, WRITE_ONLY_NAME } = require('./helpers'); + +describe('storage() -> StorageReference', function () { + describe('storage() -> StorageReference modular', function () { + before(async function () { + await seed(PATH); + }); + describe('firebase v8 compatibility', function () { + describe('toString()', function () { + it('returns the correct bucket path to the file', function () { + const app = firebase.app(); + firebase + .storage() + .ref('/uploadNope.jpeg') + .toString() + .should.equal(`gs://${app.options.storageBucket}/uploadNope.jpeg`); + }); + }); + + describe('properties', function () { + describe('fullPath', function () { + it('returns the full path as a string', function () { + firebase + .storage() + .ref('/foo/uploadNope.jpeg') + .fullPath.should.equal('foo/uploadNope.jpeg'); + firebase + .storage() + .ref('foo/uploadNope.jpeg') + .fullPath.should.equal('foo/uploadNope.jpeg'); + }); + }); + + describe('storage', function () { + it('returns the instance of storage', function () { + firebase.storage().ref('/foo/uploadNope.jpeg').storage.ref.should.be.a.Function(); + }); + }); + + describe('bucket', function () { + it('returns the storage bucket as a string', function () { + const app = firebase.app(); + firebase + .storage() + .ref('/foo/uploadNope.jpeg') + .bucket.should.equal(app.options.storageBucket); + }); + }); + + describe('name', function () { + it('returns the file name as a string', function () { + const ref = firebase.storage().ref('/foo/uploadNope.jpeg'); + ref.name.should.equal('uploadNope.jpeg'); + }); + }); + + describe('parent', function () { + it('returns the parent directory as a reference', function () { + firebase.storage().ref('/foo/uploadNope.jpeg').parent.fullPath.should.equal('foo'); + }); + + it('returns null if already at root', function () { + const ref = firebase.storage().ref('/'); + should.equal(ref.parent, null); + }); + }); + + describe('root', function () { + it('returns a reference to the root of the bucket', function () { + firebase.storage().ref('/foo/uploadNope.jpeg').root.fullPath.should.equal('/'); + }); + }); + }); + + describe('child()', function () { + it('returns a reference to a child path', function () { + const parentRef = firebase.storage().ref('/foo'); + const childRef = parentRef.child('someFile.json'); + childRef.fullPath.should.equal('foo/someFile.json'); + }); + }); + + describe('delete()', function () { + it('should delete a file', async function () { + const storageReference = firebase.storage().ref(`${PATH}/deleteMe.txt`); + await storageReference.putString('Delete File'); + await storageReference.delete(); + + try { + await storageReference.getMetadata(); + return Promise.reject(new Error('Did not throw')); + } catch (error) { + error.code.should.equal('storage/object-not-found'); + error.message.should.equal( + '[storage/object-not-found] No object exists at the desired reference.', + ); + return Promise.resolve(); + } + }); + + it('throws error if file does not exist', async function () { + const storageReference = firebase.storage().ref(`${PATH}/iDoNotExist.txt`); + + try { + await storageReference.delete(); + return Promise.reject(new Error('Did not throw')); + } catch (error) { + error.code.should.equal('storage/object-not-found'); + error.message.should.equal( + '[storage/object-not-found] No object exists at the desired reference.', + ); + return Promise.resolve(); + } + }); + + it('throws error if no write permission', async function () { + const storageReference = firebase.storage().ref('/uploadNope.jpeg'); + + try { + await storageReference.delete(); + return Promise.reject(new Error('Did not throw')); + } catch (error) { + error.code.should.equal('storage/unauthorized'); + error.message.should.equal( + '[storage/unauthorized] User is not authorized to perform the desired action.', + ); + return Promise.resolve(); + } + }); + }); + + describe('getDownloadURL', function () { + it('should return a download url for a file', async function () { + // This is frequently flaky in CI - but works sometimes. Skipping only in CI for now. + // Disabled for iOS pending: https://github.com/firebase/firebase-ios-sdk/pull/10370 + if (!isCI && device.getPlatform() !== 'ios') { + const storageReference = firebase.storage().ref(`${PATH}/list/file1.txt`); + const downloadUrl = await storageReference.getDownloadURL(); + downloadUrl.should.be.a.String(); + downloadUrl.should.containEql('file1.txt'); + downloadUrl.should.containEql(firebase.app().options.projectId); + } else { + this.skip(); + } + }); + it('throws error if file does not exist', async function () { + const storageReference = firebase.storage().ref(`${PATH}/iDoNotExist.txt`); + try { + await storageReference.getDownloadURL(); + return Promise.reject(new Error('Did not throw')); + } catch (error) { + error.code.should.equal('storage/object-not-found'); + error.message.should.equal( + '[storage/object-not-found] No object exists at the desired reference.', + ); + return Promise.resolve(); + } + }); + it('throws error if no read permission', async function () { + const storageReference = firebase.storage().ref(WRITE_ONLY_NAME); + try { + await storageReference.getDownloadURL(); + return Promise.reject(new Error('Did not throw')); + } catch (error) { + error.code.should.equal('storage/unauthorized'); + error.message.should.equal( + '[storage/unauthorized] User is not authorized to perform the desired action.', + ); + return Promise.resolve(); + } + }); + }); + describe('getMetadata', function () { + it('should return a metadata for a file', async function () { + const storageReference = firebase.storage().ref(`${PATH}/list/file1.txt`); + const metadata = await storageReference.getMetadata(); + metadata.generation.should.be.a.String(); + metadata.fullPath.should.equal(`${PATH}/list/file1.txt`); + if (device.getPlatform() === 'android') { + metadata.name.should.equal('file1.txt'); + } else { + // FIXME on ios file comes through as fully-qualified + // https://github.com/firebase/firebase-ios-sdk/issues/9849#issuecomment-1159819958 + metadata.name.should.equal(`${PATH}/list/file1.txt`); + } + metadata.size.should.be.a.Number(); + should.equal(metadata.size > 0, true); + metadata.updated.should.be.a.String(); + metadata.timeCreated.should.be.a.String(); + metadata.contentEncoding.should.be.a.String(); + metadata.contentDisposition.should.be.a.String(); + metadata.contentType.should.equal('text/plain'); + metadata.bucket.should.equal(`${firebase.app().options.projectId}.appspot.com`); + metadata.metageneration.should.be.a.String(); + metadata.md5Hash.should.be.a.String(); + // TODO against cloud storage cacheControl comes back null/undefined by default. Emulator has a difference + // https://github.com/firebase/firebase-tools/issues/3398#issuecomment-1159821364 + // should.equal(metadata.cacheControl, undefined); + should.equal(metadata.contentLanguage, null); + // should.equal(metadata.customMetadata, null); + }); + }); + describe('list', function () { + it('should return list results', async function () { + const storageReference = firebase.storage().ref(`${PATH}/list`); + const result = await storageReference.list(); + result.constructor.name.should.eql('StorageListResult'); + result.should.have.property('nextPageToken'); + result.items.should.be.Array(); + result.items.length.should.be.greaterThan(0); + result.items[0].constructor.name.should.eql('StorageReference'); + result.prefixes.should.be.Array(); + result.prefixes.length.should.be.greaterThan(0); + result.prefixes[0].constructor.name.should.eql('StorageReference'); + }); + it('throws if options is not an object', function () { + try { + const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); + storageReference.list(123); + return Promise.reject(new Error('Did not throw')); + } catch (error) { + error.message.should.containEql("'options' expected an object value"); + return Promise.resolve(); + } + }); + describe('maxResults', function () { + it('should limit with maxResults are passed', async function () { + const storageReference = firebase.storage().ref(`${PATH}/list`); + const result = await storageReference.list({ + maxResults: 1, + }); + result.nextPageToken.should.be.String(); + result.items.should.be.Array(); + result.items.length.should.eql(1); + result.items[0].constructor.name.should.eql('StorageReference'); + result.prefixes.should.be.Array(); + // todo length? + }); + it('throws if maxResults is not a number', function () { + try { + const storageReference = firebase.storage().ref(`${PATH}/list`); + storageReference.list({ + maxResults: '123', + }); + return Promise.reject(new Error('Did not throw')); + } catch (error) { + error.message.should.containEql("'options.maxResults' expected a number value"); + return Promise.resolve(); + } + }); + it('throws if maxResults is not a valid number', function () { + try { + const storageReference = firebase.storage().ref(`${PATH}/list`); + storageReference.list({ + maxResults: 2000, + }); + return Promise.reject(new Error('Did not throw')); + } catch (error) { + error.message.should.containEql( + "'options.maxResults' expected a number value between 1-1000", + ); + return Promise.resolve(); + } + }); + }); + describe('pageToken', function () { + it('throws if pageToken is not a string', function () { + try { + const storageReference = firebase.storage().ref(`${PATH}/list`); + storageReference.list({ + pageToken: 123, + }); + return Promise.reject(new Error('Did not throw')); + } catch (error) { + error.message.should.containEql("'options.pageToken' expected a string value"); + return Promise.resolve(); + } + }); + it('should return and use a page token', async function () { + const storageReference = firebase.storage().ref(`${PATH}/list`); + const result1 = await storageReference.list({ + maxResults: 1, + }); + const item1 = result1.items[0].fullPath; + const result2 = await storageReference.list({ + maxResults: 1, + pageToken: result1.nextPageToken, + }); + const item2 = result2.items[0].fullPath; + if (item1 === item2) { + throw new Error('Expected item results to be different.'); + } + }); + }); + }); + describe('listAll', function () { + it('should return all results', async function () { + const storageReference = firebase.storage().ref(`${PATH}/list`); + const result = await storageReference.listAll(); + should.equal(result.nextPageToken, null); + result.items.should.be.Array(); + result.items.length.should.be.greaterThan(0); + result.items[0].constructor.name.should.eql('StorageReference'); + result.prefixes.should.be.Array(); + result.prefixes.length.should.be.greaterThan(0); + result.prefixes[0].constructor.name.should.eql('StorageReference'); + }); + it('should not crash if the user is not allowed to list the directory', async function () { + const storageReference = firebase.storage().ref('/forbidden'); + try { + await storageReference.listAll(); + return Promise.reject(new Error('listAll on a forbidden directory succeeded')); + } catch (error) { + error.code.should.equal('storage/unauthorized'); + error.message.should.equal( + '[storage/unauthorized] User is not authorized to perform the desired action.', + ); + return Promise.resolve(); + } + }); + }); + describe('updateMetadata', function () { + it('should return the updated metadata for a file', async function () { + const storageReference = firebase.storage().ref(`${PATH}/list/file1.txt`); + let metadata = await storageReference.updateMetadata({ + cacheControl: 'cache-control', + contentDisposition: 'content-disposition', + contentEncoding: 'application/octet-stream', + contentLanguage: 'de', + contentType: 'content-type-a', + customMetadata: { + a: 'b', + y: 'z', + }, + }); + // Things that are set automagically for us + metadata.generation.should.be.a.String(); + metadata.fullPath.should.equal(`${PATH}/list/file1.txt`); + if (device.getPlatform() === 'android') { + metadata.name.should.equal('file1.txt'); + } else { + // FIXME on ios file comes through as fully-qualified + // https://github.com/firebase/firebase-ios-sdk/issues/9849#issuecomment-1159819958 + metadata.name.should.equal(`${PATH}/list/file1.txt`); + } + metadata.size.should.be.a.Number(); + should.equal(metadata.size > 0, true); + metadata.updated.should.be.a.String(); + metadata.timeCreated.should.be.a.String(); + metadata.metageneration.should.be.a.String(); + metadata.md5Hash.should.be.a.String(); + metadata.bucket.should.equal(`${firebase.app().options.projectId}.appspot.com`); + // Things we just updated + metadata.cacheControl.should.equals('cache-control'); + metadata.contentDisposition.should.equal('content-disposition'); + metadata.contentEncoding.should.equal('application/octet-stream'); + metadata.contentLanguage.should.equal('de'); + metadata.contentType.should.equal('content-type-a'); + metadata.customMetadata.should.be.an.Object(); + metadata.customMetadata.a.should.equal('b'); + metadata.customMetadata.y.should.equal('z'); + Object.keys(metadata.customMetadata).length.should.equal(2); + // Now let's make sure removing metadata works + metadata = await storageReference.updateMetadata({ + contentType: null, + cacheControl: null, + contentDisposition: null, + contentEncoding: null, + contentLanguage: null, + customMetadata: null, + }); + // Things that are set automagically for us and are not updatable + metadata.generation.should.be.a.String(); + metadata.fullPath.should.equal(`${PATH}/list/file1.txt`); + if (device.getPlatform() === 'android') { + metadata.name.should.equal('file1.txt'); + } else { + // FIXME on ios file comes through as fully-qualified + // https://github.com/firebase/firebase-ios-sdk/issues/9849#issuecomment-1159819958 + metadata.name.should.equal(`${PATH}/list/file1.txt`); + } + metadata.size.should.be.a.Number(); + should.equal(metadata.size > 0, true); + metadata.updated.should.be.a.String(); + metadata.timeCreated.should.be.a.String(); + metadata.metageneration.should.be.a.String(); + metadata.md5Hash.should.be.a.String(); + metadata.bucket.should.equal(`${firebase.app().options.projectId}.appspot.com`); + // Things that we may set (or remove) + should.equal(metadata.cacheControl, undefined); + should.equal(metadata.contentDisposition, undefined); + should.equal(metadata.contentEncoding, 'identity'); + should.equal(metadata.contentLanguage, undefined); + should.equal(metadata.contentType, undefined); + should.equal(metadata.customMetadata, undefined); + }); + it('should set update or remove customMetadata properties correctly', async function () { + const storageReference = firebase.storage().ref(`${PATH}/list/file1.txt`); + let metadata = await storageReference.updateMetadata({ + contentType: 'application/octet-stream', + customMetadata: { + keepMe: 'please', + removeMeFirstTime: 'please', + removeMeSecondTime: 'please', + }, + }); + Object.keys(metadata.customMetadata).length.should.equal(3); + metadata.customMetadata.keepMe.should.equal('please'); + metadata.customMetadata.removeMeFirstTime.should.equal('please'); + metadata.customMetadata.removeMeSecondTime.should.equal('please'); + metadata = await storageReference.updateMetadata({ + contentType: 'application/octet-stream', + customMetadata: { + keepMe: 'please', + removeMeFirstTime: null, + removeMeSecondTime: 'please', + }, + }); + Object.keys(metadata.customMetadata).length.should.equal(2); + metadata.customMetadata.keepMe.should.equal('please'); + metadata.customMetadata.removeMeSecondTime.should.equal('please'); + metadata = await storageReference.updateMetadata({ + contentType: 'application/octet-stream', + customMetadata: { + keepMe: 'please', + }, + }); + Object.keys(metadata.customMetadata).length.should.equal(1); + metadata.customMetadata.keepMe.should.equal('please'); + }); + it('should error if updateMetadata includes md5hash', async function () { + const storageReference = firebase.storage().ref(`${PATH}/list/file1.txt`); + try { + await storageReference.updateMetadata({ + md5hash: '0xDEADBEEF', + }); + return Promise.reject(new Error('Did not throw on invalid updateMetadata')); + } catch (e) { + e.message.should.containEql('md5hash may only be set on upload, not on updateMetadata'); + return Promise.resolve(); + } + }); + }); + describe('putFile', function () { + it('errors if file path is not a string', async function () { + const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); + try { + storageReference.putFile(1337); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql('expects a string value'); + return Promise.resolve(); + } + }); + it('errors if metadata is not an object', async function () { + const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); + try { + storageReference.putFile('foo', 123); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql('must be an object value'); + return Promise.resolve(); + } + }); + it('errors if metadata contains an unsupported property', async function () { + const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); + try { + storageReference.putFile('foo', { foo: true }); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql("unknown property 'foo'"); + return Promise.resolve(); + } + }); + it('errors if metadata property value is not a string or null value', async function () { + const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); + try { + storageReference.putFile('foo', { contentType: true }); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql('should be a string or null value'); + return Promise.resolve(); + } + }); + it('errors if metadata.customMetadata is not an object', async function () { + const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); + try { + storageReference.putFile('foo', { customMetadata: true }); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql( + 'customMetadata must be an object of keys and string values', + ); + return Promise.resolve(); + } + }); + // TODO check an metaData:md5hash property passes through correcty on putFile + }); + describe('putString', function () { + it('errors if metadata is not an object', async function () { + const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); + try { + storageReference.putString('foo', 'raw', 123); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql('must be an object value'); + return Promise.resolve(); + } + }); + it('errors if metadata contains an unsupported property', async function () { + const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); + try { + storageReference.putString('foo', 'raw', { foo: true }); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql("unknown property 'foo'"); + return Promise.resolve(); + } + }); + it('errors if metadata property value is not a string or null value', async function () { + const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); + try { + storageReference.putString('foo', 'raw', { contentType: true }); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql('should be a string or null value'); + return Promise.resolve(); + } + }); + it('errors if metadata.customMetadata is not an object', async function () { + const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); + try { + storageReference.putString('foo', 'raw', { customMetadata: true }); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql( + 'customMetadata must be an object of keys and string values', + ); + return Promise.resolve(); + } + }); + it('allows valid metadata properties for upload', async function () { + const storageReference = firebase.storage().ref(`${PATH}/metadataTest.txt`); + await storageReference.putString('foo', 'raw', { + contentType: 'text/plain', + md5hash: '123412341234', + cacheControl: 'true', + contentDisposition: 'disposed', + contentEncoding: 'application/octet-stream', + contentLanguage: 'de', + customMetadata: { + customMetadata1: 'metadata1value', + }, + }); + }); + }); + describe('put', function () { + it('errors if metadata is not an object', async function () { + const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); + try { + storageReference.put(new jet.context.window.ArrayBuffer(), 123); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql('must be an object value'); + return Promise.resolve(); + } + }); + it('errors if metadata contains an unsupported property', async function () { + const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); + try { + storageReference.put(new jet.context.window.ArrayBuffer(), { foo: true }); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql("unknown property 'foo'"); + return Promise.resolve(); + } + }); + it('errors if metadata property value is not a string or null value', async function () { + const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); + try { + storageReference.put(new jet.context.window.ArrayBuffer(), { contentType: true }); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql('should be a string or null value'); + return Promise.resolve(); + } + }); + it('errors if metadata.customMetadata is not an object', async function () { + const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); + try { + storageReference.put(new jet.context.window.ArrayBuffer(), { customMetadata: true }); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql( + 'customMetadata must be an object of keys and string values', + ); + return Promise.resolve(); + } + }); + it('allows valid metadata properties for upload', async function () { + const storageReference = firebase.storage().ref(`${PATH}/metadataTest.jpeg`); + await storageReference.put(new jet.context.window.ArrayBuffer(), { + contentType: 'image/jpg', + md5hash: '123412341234', + cacheControl: 'true', + contentDisposition: 'disposed', + contentEncoding: 'application/octet-stream', + contentLanguage: 'de', + customMetadata: { + customMetadata1: 'metadata1value', + }, + }); + }); + }); + }); + }); + + describe('StorageReference modular', function () { + before(async function () { + await seed(PATH); + }); + + describe('toString()', function () { + it('returns the correct bucket path to the file', function () { + const { getStorage, ref, toString } = storageModular; + const storageReference = ref(getStorage(), `/uploadNope.jpeg`); + const app = firebase.app(); + + toString(storageReference).should.equal( + `gs://${app.options.storageBucket}/uploadNope.jpeg`, + ); + }); + }); + + describe('properties', function () { + describe('fullPath', function () { + it('returns the full path as a string', function () { + const { getStorage, ref } = storageModular; + const storageReference = ref(getStorage(), '/foo/uploadNope.jpeg'); + const storageReference2 = ref(getStorage(), 'foo/uploadNope.jpeg'); + storageReference.fullPath.should.equal('foo/uploadNope.jpeg'); + storageReference2.fullPath.should.equal('foo/uploadNope.jpeg'); + }); + }); + + describe('storage', function () { + it('returns the instance of storage', function () { + const { getStorage, ref } = storageModular; + const storageReference = ref(getStorage(), '/foo/uploadNope.jpeg'); + storageReference.storage.ref.should.be.a.Function(); + }); + }); + + describe('bucket', function () { + it('returns the storage bucket as a string', function () { + const { getStorage, ref } = storageModular; + const storageReference = ref(getStorage(), '/foo/uploadNope.jpeg'); + const app = firebase.app(); + storageReference.bucket.should.equal(app.options.storageBucket); + }); + }); + + describe('name', function () { + it('returns the file name as a string', function () { + const { getStorage, ref } = storageModular; + const storageReference = ref(getStorage(), '/foo/uploadNope.jpeg'); + + storageReference.name.should.equal('uploadNope.jpeg'); + }); + }); + + describe('parent', function () { + it('returns the parent directory as a reference', function () { + const { getStorage, ref } = storageModular; + const storageReference = ref(getStorage(), '/foo/uploadNope.jpeg'); + storageReference.parent.fullPath.should.equal('foo'); + }); + + it('returns null if already at root', function () { + const { getStorage, ref } = storageModular; + const storageReference = ref(getStorage(), '/'); + + should.equal(storageReference.parent, null); + }); + }); + + describe('root', function () { + it('returns a reference to the root of the bucket', function () { + const { getStorage, ref } = storageModular; + const storageReference = ref(getStorage(), '/foo/uploadNope.jpeg'); + storageReference.root.fullPath.should.equal('/'); + }); + }); + }); + + describe('child()', function () { + it('returns a reference to a child path', function () { + const { getStorage, ref, child } = storageModular; + const storageReference = ref(getStorage(), '/foo'); + + const childRef = child(storageReference, 'someFile.json'); + childRef.fullPath.should.equal('foo/someFile.json'); + }); + }); + + describe('delete()', function () { + it('should delete a file', async function () { + const { getStorage, ref, uploadString, deleteObject, getMetadata } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/deleteMe.txt`); + + await uploadString(storageReference, 'Delete File'); + await deleteObject(storageReference); + + try { + await getMetadata(storageReference); + return Promise.reject(new Error('Did not throw')); + } catch (error) { + error.code.should.equal('storage/object-not-found'); + error.message.should.equal( + '[storage/object-not-found] No object exists at the desired reference.', + ); + return Promise.resolve(); + } + }); + + it('throws error if file does not exist', async function () { + const { getStorage, ref, deleteObject } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/iDoNotExist.txt`); + + try { + await deleteObject(storageReference); + return Promise.reject(new Error('Did not throw')); + } catch (error) { + error.code.should.equal('storage/object-not-found'); + error.message.should.equal( + '[storage/object-not-found] No object exists at the desired reference.', + ); + return Promise.resolve(); + } + }); + + it('throws error if no write permission', async function () { + const { getStorage, ref, deleteObject } = storageModular; + const storageReference = ref(getStorage(), '/uploadNope.jpeg'); + + try { + await deleteObject(storageReference); + return Promise.reject(new Error('Did not throw')); + } catch (error) { + error.code.should.equal('storage/unauthorized'); + error.message.should.equal( + '[storage/unauthorized] User is not authorized to perform the desired action.', + ); + return Promise.resolve(); + } + }); + }); + + describe('getDownloadURL', function () { + it('should return a download url for a file', async function () { + const { getStorage, ref, getDownloadURL } = storageModular; + // This is frequently flaky in CI - but works sometimes. Skipping only in CI for now. + // Disabled for iOS pending: https://github.com/firebase/firebase-ios-sdk/pull/10370 + if (!isCI && device.getPlatform() !== 'ios') { + const storageReference = ref(getStorage(), `${PATH}/list/file1.txt`); + const downloadUrl = await getDownloadURL(storageReference); + downloadUrl.should.be.a.String(); + downloadUrl.should.containEql('file1.txt'); + downloadUrl.should.containEql(firebase.app().options.projectId); + } else { + this.skip(); + } + }); + + it('throws error if file does not exist', async function () { + const { getStorage, ref, getDownloadURL } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/iDoNotExist.txt`); + + try { + await getDownloadURL(storageReference); + return Promise.reject(new Error('Did not throw')); + } catch (error) { + error.code.should.equal('storage/object-not-found'); + error.message.should.equal( + '[storage/object-not-found] No object exists at the desired reference.', + ); + return Promise.resolve(); + } + }); + + it('throws error if no read permission', async function () { + const { getStorage, ref, getDownloadURL } = storageModular; + const storageReference = ref(getStorage(), WRITE_ONLY_NAME); + + try { + await getDownloadURL(storageReference); + return Promise.reject(new Error('Did not throw')); + } catch (error) { + error.code.should.equal('storage/unauthorized'); + error.message.should.equal( + '[storage/unauthorized] User is not authorized to perform the desired action.', + ); + return Promise.resolve(); + } + }); + }); + + describe('getMetadata', function () { + it('should return a metadata for a file', async function () { + const { getStorage, ref, getMetadata } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/list/file1.txt`); + const metadata = await getMetadata(storageReference); + metadata.generation.should.be.a.String(); + metadata.fullPath.should.equal(`${PATH}/list/file1.txt`); + if (device.getPlatform() === 'android') { + metadata.name.should.equal('file1.txt'); + } else { + // FIXME on ios file comes through as fully-qualified + // https://github.com/firebase/firebase-ios-sdk/issues/9849#issuecomment-1159819958 + metadata.name.should.equal(`${PATH}/list/file1.txt`); + } + metadata.size.should.be.a.Number(); + should.equal(metadata.size > 0, true); + metadata.updated.should.be.a.String(); + metadata.timeCreated.should.be.a.String(); + metadata.contentEncoding.should.be.a.String(); + metadata.contentDisposition.should.be.a.String(); + metadata.contentType.should.equal('text/plain'); + metadata.bucket.should.equal(`${firebase.app().options.projectId}.appspot.com`); + metadata.metageneration.should.be.a.String(); + metadata.md5Hash.should.be.a.String(); + // TODO against cloud storage cacheControl comes back null/undefined by default. Emulator has a difference + // https://github.com/firebase/firebase-tools/issues/3398#issuecomment-1159821364 + // should.equal(metadata.cacheControl, undefined); + should.equal(metadata.contentLanguage, null); + // should.equal(metadata.customMetadata, null); + }); + }); + + describe('list', function () { + it('should return list results', async function () { + const { getStorage, ref } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/list`); + + const result = await storageReference.list(); + + result.constructor.name.should.eql('StorageListResult'); + result.should.have.property('nextPageToken'); + + result.items.should.be.Array(); + result.items.length.should.be.greaterThan(0); + result.items[0].constructor.name.should.eql('StorageReference'); + + result.prefixes.should.be.Array(); + result.prefixes.length.should.be.greaterThan(0); + result.prefixes[0].constructor.name.should.eql('StorageReference'); + }); + + it('throws if options is not an object', function () { + try { + const { getStorage, ref, list } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/ok.jpeg`); + list(storageReference, 123); + return Promise.reject(new Error('Did not throw')); + } catch (error) { + error.message.should.containEql("'options' expected an object value"); + return Promise.resolve(); + } + }); + + describe('maxResults', function () { + it('should limit with maxResults are passed', async function () { + const { getStorage, ref, list } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/list`); + + const result = await list(storageReference, { + maxResults: 1, + }); + + result.nextPageToken.should.be.String(); + + result.items.should.be.Array(); + result.items.length.should.eql(1); + result.items[0].constructor.name.should.eql('StorageReference'); + + result.prefixes.should.be.Array(); + // todo length? + }); + + it('throws if maxResults is not a number', function () { + try { + const { getStorage, ref, list } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/list`); + + list(storageReference, { + maxResults: '123', + }); + return Promise.reject(new Error('Did not throw')); + } catch (error) { + error.message.should.containEql("'options.maxResults' expected a number value"); + return Promise.resolve(); + } + }); + + it('throws if maxResults is not a valid number', function () { + try { + const { getStorage, ref, list } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/list`); + list(storageReference, { + maxResults: 2000, + }); + return Promise.reject(new Error('Did not throw')); + } catch (error) { + error.message.should.containEql( + "'options.maxResults' expected a number value between 1-1000", + ); + return Promise.resolve(); + } + }); + }); + + describe('pageToken', function () { + it('throws if pageToken is not a string', function () { + try { + const { getStorage, ref, list } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/list`); + list(storageReference, { + pageToken: 123, + }); + return Promise.reject(new Error('Did not throw')); + } catch (error) { + error.message.should.containEql("'options.pageToken' expected a string value"); + return Promise.resolve(); + } + }); + + it('should return and use a page token', async function () { + const { getStorage, ref, list } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/list`); + const result1 = await list(storageReference, { + maxResults: 1, + }); + + const item1 = result1.items[0].fullPath; + + const result2 = await list(storageReference, { + maxResults: 1, + pageToken: result1.nextPageToken, + }); + + const item2 = result2.items[0].fullPath; + + if (item1 === item2) { + throw new Error('Expected item results to be different.'); + } + }); + }); + }); + + describe('listAll', function () { + it('should return all results', async function () { + const { getStorage, ref, listAll } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/list`); + const result = await listAll(storageReference); + + should.equal(result.nextPageToken, null); + + result.items.should.be.Array(); + result.items.length.should.be.greaterThan(0); + result.items[0].constructor.name.should.eql('StorageReference'); + + result.prefixes.should.be.Array(); + result.prefixes.length.should.be.greaterThan(0); + result.prefixes[0].constructor.name.should.eql('StorageReference'); + }); + + it('should not crash if the user is not allowed to list the directory', async function () { + const { getStorage, ref, listAll } = storageModular; + const storageReference = ref(getStorage(), '/forbidden'); + + try { + await listAll(storageReference); + return Promise.reject(new Error('listAll on a forbidden directory succeeded')); + } catch (error) { + error.code.should.equal('storage/unauthorized'); + error.message.should.equal( + '[storage/unauthorized] User is not authorized to perform the desired action.', + ); + return Promise.resolve(); + } + }); + }); + + describe('updateMetadata', function () { + it('should return the updated metadata for a file', async function () { + const { getStorage, ref, updateMetadata } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/list/file1.txt`); + + let metadata = await updateMetadata(storageReference, { + cacheControl: 'cache-control', + contentDisposition: 'content-disposition', + contentEncoding: 'application/octet-stream', + contentLanguage: 'de', + contentType: 'content-type-a', + customMetadata: { + a: 'b', + y: 'z', + }, + }); + + // Things that are set automagically for us + metadata.generation.should.be.a.String(); + metadata.fullPath.should.equal(`${PATH}/list/file1.txt`); + if (device.getPlatform() === 'android') { + metadata.name.should.equal('file1.txt'); + } else { + // FIXME on ios file comes through as fully-qualified + // https://github.com/firebase/firebase-ios-sdk/issues/9849#issuecomment-1159819958 + metadata.name.should.equal(`${PATH}/list/file1.txt`); + } + metadata.size.should.be.a.Number(); + should.equal(metadata.size > 0, true); + metadata.updated.should.be.a.String(); + metadata.timeCreated.should.be.a.String(); + metadata.metageneration.should.be.a.String(); + metadata.md5Hash.should.be.a.String(); + metadata.bucket.should.equal(`${firebase.app().options.projectId}.appspot.com`); + + // Things we just updated + metadata.cacheControl.should.equals('cache-control'); + metadata.contentDisposition.should.equal('content-disposition'); + metadata.contentEncoding.should.equal('application/octet-stream'); + metadata.contentLanguage.should.equal('de'); + metadata.contentType.should.equal('content-type-a'); + metadata.customMetadata.should.be.an.Object(); + metadata.customMetadata.a.should.equal('b'); + metadata.customMetadata.y.should.equal('z'); + Object.keys(metadata.customMetadata).length.should.equal(2); + + // Now let's make sure removing metadata works + metadata = await updateMetadata(storageReference, { + contentType: null, + cacheControl: null, + contentDisposition: null, + contentEncoding: null, + contentLanguage: null, + customMetadata: null, + }); + + // Things that are set automagically for us and are not updatable + metadata.generation.should.be.a.String(); + metadata.fullPath.should.equal(`${PATH}/list/file1.txt`); + if (device.getPlatform() === 'android') { + metadata.name.should.equal('file1.txt'); + } else { + // FIXME on ios file comes through as fully-qualified + // https://github.com/firebase/firebase-ios-sdk/issues/9849#issuecomment-1159819958 + metadata.name.should.equal(`${PATH}/list/file1.txt`); + } + metadata.size.should.be.a.Number(); + should.equal(metadata.size > 0, true); + metadata.updated.should.be.a.String(); + metadata.timeCreated.should.be.a.String(); + metadata.metageneration.should.be.a.String(); + metadata.md5Hash.should.be.a.String(); + metadata.bucket.should.equal(`${firebase.app().options.projectId}.appspot.com`); + + // Things that we may set (or remove) + should.equal(metadata.cacheControl, undefined); + should.equal(metadata.contentDisposition, undefined); + should.equal(metadata.contentEncoding, 'identity'); + should.equal(metadata.contentLanguage, undefined); + should.equal(metadata.contentType, undefined); + should.equal(metadata.customMetadata, undefined); + }); + + it('should set update or remove customMetadata properties correctly', async function () { + const { getStorage, ref, updateMetadata } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/list/file1.txt`); + + let metadata = await updateMetadata(storageReference, { + contentType: 'application/octet-stream', + customMetadata: { + keepMe: 'please', + removeMeFirstTime: 'please', + removeMeSecondTime: 'please', + }, + }); + + Object.keys(metadata.customMetadata).length.should.equal(3); + metadata.customMetadata.keepMe.should.equal('please'); + metadata.customMetadata.removeMeFirstTime.should.equal('please'); + metadata.customMetadata.removeMeSecondTime.should.equal('please'); + + metadata = await updateMetadata(storageReference, { + contentType: 'application/octet-stream', + customMetadata: { + keepMe: 'please', + removeMeFirstTime: null, + removeMeSecondTime: 'please', + }, + }); + + Object.keys(metadata.customMetadata).length.should.equal(2); + metadata.customMetadata.keepMe.should.equal('please'); + metadata.customMetadata.removeMeSecondTime.should.equal('please'); + + metadata = await updateMetadata(storageReference, { + contentType: 'application/octet-stream', + customMetadata: { + keepMe: 'please', + }, + }); + + Object.keys(metadata.customMetadata).length.should.equal(1); + metadata.customMetadata.keepMe.should.equal('please'); + }); + + it('should error if updateMetadata includes md5hash', async function () { + const { getStorage, ref, updateMetadata } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/list/file1.txt`); + + try { + await updateMetadata(storageReference, { + md5hash: '0xDEADBEEF', + }); + return Promise.reject(new Error('Did not throw on invalid updateMetadata')); + } catch (e) { + e.message.should.containEql('md5hash may only be set on upload, not on updateMetadata'); + return Promise.resolve(); + } + }); + }); + + describe('putFile', function () { + it('errors if file path is not a string', async function () { + const { getStorage, ref, putFile } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/ok.jpeg`); + + try { + putFile(storageReference, 1337); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql('expects a string value'); + return Promise.resolve(); + } + }); + + it('errors if metadata is not an object', async function () { + const { getStorage, ref, putFile } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/ok.jpeg`); + try { + putFile(storageReference, 'foo', 123); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql('must be an object value'); + return Promise.resolve(); + } + }); + + it('errors if metadata contains an unsupported property', async function () { + const { getStorage, ref, putFile } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/ok.jpeg`); + try { + putFile(storageReference, 'foo', { foo: true }); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql("unknown property 'foo'"); + return Promise.resolve(); + } + }); + + it('errors if metadata property value is not a string or null value', async function () { + const { getStorage, ref, putFile } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/ok.jpeg`); + try { + putFile(storageReference, 'foo', { contentType: true }); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql('should be a string or null value'); + return Promise.resolve(); + } + }); + + it('errors if metadata.customMetadata is not an object', async function () { + const { getStorage, ref, putFile } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/ok.jpeg`); + try { + putFile(storageReference, 'foo', { customMetadata: true }); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql( + 'customMetadata must be an object of keys and string values', + ); + return Promise.resolve(); + } + }); + + // TODO check an metaData:md5hash property passes through correcty on putFile + }); + + describe('putString', function () { + it('errors if metadata is not an object', async function () { + const { getStorage, ref, uploadString } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/ok.jpeg`); + try { + uploadString(storageReference, 'foo', 'raw', 123); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql('must be an object value'); + return Promise.resolve(); + } + }); + + it('errors if metadata contains an unsupported property', async function () { + const { getStorage, ref, uploadString } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/ok.jpeg`); + try { + uploadString(storageReference, 'foo', 'raw', { foo: true }); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql("unknown property 'foo'"); + return Promise.resolve(); + } + }); + + it('errors if metadata property value is not a string or null value', async function () { + const { getStorage, ref, uploadString } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/ok.jpeg`); + try { + uploadString(storageReference, 'foo', 'raw', { contentType: true }); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql('should be a string or null value'); + return Promise.resolve(); + } + }); + + it('errors if metadata.customMetadata is not an object', async function () { + const { getStorage, ref, uploadString } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/ok.jpeg`); + try { + uploadString(storageReference, 'foo', 'raw', { customMetadata: true }); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql( + 'customMetadata must be an object of keys and string values', + ); + return Promise.resolve(); + } + }); + + it('allows valid metadata properties for upload', async function () { + const { getStorage, ref, uploadString } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/metadataTest.txt`); + + await uploadString(storageReference, 'foo', 'raw', { + contentType: 'text/plain', + md5hash: '123412341234', + cacheControl: 'true', + contentDisposition: 'disposed', + contentEncoding: 'application/octet-stream', + contentLanguage: 'de', + customMetadata: { + customMetadata1: 'metadata1value', + }, + }); + }); + }); + + describe('put', function () { + it('errors if metadata is not an object', async function () { + const { getStorage, ref, uploadBytesResumable } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/ok.jpeg`); + + try { + uploadBytesResumable(storageReference, new jet.context.window.ArrayBuffer(), 123); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql('must be an object value'); + return Promise.resolve(); + } + }); + + it('errors if metadata contains an unsupported property', async function () { + const { getStorage, ref, uploadBytesResumable } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/ok.jpeg`); + try { + uploadBytesResumable(storageReference, new jet.context.window.ArrayBuffer(), { + foo: true, + }); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql("unknown property 'foo'"); + return Promise.resolve(); + } + }); + + it('errors if metadata property value is not a string or null value', async function () { + const { getStorage, ref, uploadBytesResumable } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/ok.jpeg`); + try { + uploadBytesResumable(storageReference, new jet.context.window.ArrayBuffer(), { + contentType: true, + }); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql('should be a string or null value'); + return Promise.resolve(); + } + }); + + it('errors if metadata.customMetadata is not an object', async function () { + const { getStorage, ref, uploadBytesResumable } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/ok.jpeg`); + try { + uploadBytesResumable(storageReference, new jet.context.window.ArrayBuffer(), { + customMetadata: true, + }); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql( + 'customMetadata must be an object of keys and string values', + ); + return Promise.resolve(); + } + }); + + it('allows valid metadata properties for upload', async function () { + const { getStorage, ref, uploadBytesResumable } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/metadataTest.jpeg`); + + await uploadBytesResumable(storageReference, new jet.context.window.ArrayBuffer(), { + contentType: 'image/jpg', + md5hash: '123412341234', + cacheControl: 'true', + contentDisposition: 'disposed', + contentEncoding: 'application/octet-stream', + contentLanguage: 'de', + customMetadata: { + customMetadata1: 'metadata1value', + }, + }); + }); + }); + }); +}); diff --git a/packages/storage/e2e/StorageStatics.e2e.js b/packages/storage/e2e/StorageStatics.e2e.js deleted file mode 100644 index df0b35e9a0..0000000000 --- a/packages/storage/e2e/StorageStatics.e2e.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -describe('storage()', function () { - describe('statics', function () { - // TODO test statics correctly exported - }); -}); diff --git a/packages/storage/e2e/StorageTask.e2e.js b/packages/storage/e2e/StorageTask.e2e.js deleted file mode 100644 index 8427268b74..0000000000 --- a/packages/storage/e2e/StorageTask.e2e.js +++ /dev/null @@ -1,852 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -const { PATH, seed, WRITE_ONLY_NAME } = require('./helpers'); - -function snapshotProperties(snapshot) { - snapshot.should.have.property('state'); - snapshot.should.have.property('metadata'); - snapshot.should.have.property('ref'); - snapshot.should.have.property('task'); - snapshot.should.have.property('totalBytes'); - snapshot.should.have.property('bytesTransferred'); -} - -describe('storage() -> StorageTask', function () { - before(async function () { - await seed(PATH); - }); - - describe('writeToFile()', function () { - // TODO - followup - the storage emulator currently inverts not-found / permission error conditions - // this one returns the permission denied against live storage, but object not found against emulator - xit('errors if permission denied', async function () { - try { - await firebase - .storage() - .ref('/not.jpg') - .writeToFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/not.jpg`); - return Promise.reject(new Error('No permission denied error')); - } catch (error) { - error.code.should.equal('storage/unauthorized'); - error.message.includes('not authorized').should.be.true(); - return Promise.resolve(); - } - }); - - it('downloads a file', async function () { - const meta = await firebase - .storage() - .ref(`${PATH}/list/file1.txt`) - .writeToFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/file1.txt`); - - meta.state.should.eql(firebase.storage.TaskState.SUCCESS); - meta.bytesTransferred.should.eql(meta.totalBytes); - }); - }); - - describe('putString()', function () { - it('uploads a raw string', async function () { - const jsonDerulo = JSON.stringify({ foo: 'bar' }); - - const uploadTaskSnapshot = await firebase - .storage() - .ref(`${PATH}/putString.json`) - .putString(jsonDerulo, firebase.storage.StringFormat.RAW, { - contentType: 'application/json', - }); - - uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); - uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); - uploadTaskSnapshot.metadata.should.be.an.Object(); - }); - - it('uploads a data_url formatted string', async function () { - const dataUrl = 'data:application/json;base64,eyJmb28iOiJiYXNlNjQifQ=='; - const uploadTaskSnapshot = await firebase - .storage() - .ref(`${PATH}/putStringDataURL.json`) - .putString(dataUrl, firebase.storage.StringFormat.DATA_URL); - - uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); - uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); - uploadTaskSnapshot.metadata.should.be.an.Object(); - }); - - it('uploads a url encoded data_url formatted string', async function () { - const dataUrl = 'data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E'; - const uploadTaskSnapshot = await firebase - .storage() - .ref(`${PATH}/helloWorld.html`) - .putString(dataUrl, firebase.storage.StringFormat.DATA_URL); - - uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); - uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); - uploadTaskSnapshot.metadata.should.be.an.Object(); - }); - - it('when using data_url it still sets the content type if metadata is provided', async function () { - const dataUrl = 'data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E'; - - const uploadTaskSnapshot = await firebase - .storage() - .ref(`${PATH}/helloWorld.html`) - .putString(dataUrl, firebase.storage.StringFormat.DATA_URL, { - // TODO(salakar) automate test metadata is preserved when auto setting mediatype - customMetadata: { - hello: 'world', - }, - }); - - uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); - uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); - uploadTaskSnapshot.metadata.should.be.an.Object(); - }); - - it('uploads a base64 string', async function () { - const base64String = 'eyJmb28iOiJiYXNlNjQifQ=='; - - const uploadTaskSnapshot = await firebase - .storage() - .ref(`${PATH}/putStringBase64.json`) - .putString(base64String, firebase.storage.StringFormat.BASE64, { - contentType: 'application/json', - }); - - uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); - uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); - uploadTaskSnapshot.metadata.should.be.an.Object(); - }); - - it('uploads a base64url string', async function () { - const base64UrlString = 'eyJmb28iOiJiYXNlNjQifQ'; - - const uploadTaskSnapshot = await firebase - .storage() - .ref(`${PATH}/putStringBase64Url.json`) - .putString(base64UrlString, firebase.storage.StringFormat.BASE64URL, { - contentType: 'application/json', - }); - - uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); - uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); - uploadTaskSnapshot.metadata.should.be.an.Object(); - }); - - it('throws an error on invalid data_url', async function () { - const dataUrl = ''; - try { - await firebase - .storage() - .ref('/a.b') - .putString(dataUrl, firebase.storage.StringFormat.DATA_URL); - return Promise.reject(new Error('Did not throw!')); - } catch (error) { - error.message.should.containEql('invalid data_url string provided'); - return Promise.resolve(); - } - }); - - it('throws if string arg is not a valid string', async function () { - try { - await firebase.storage().ref('/a.b').putString(1, 'base64'); - return Promise.reject(new Error('Did not throw!')); - } catch (error) { - error.message.should.containEql("'string' expects a string value"); - return Promise.resolve(); - } - }); - - it('throws an error on invalid string format', async function () { - try { - await firebase.storage().ref('/a.b').putString('fooby', 'abc'); - return Promise.reject(new Error('Did not throw!')); - } catch (error) { - error.message.should.containEql("'format' provided is invalid, must be one of"); - return Promise.resolve(); - } - }); - - it('throws an error if metadata is not an object', async function () { - try { - await firebase.storage().ref('/a.b').putString('fooby', 'raw', 1234); - return Promise.reject(new Error('Did not throw!')); - } catch (error) { - error.message.should.containEql('must be an object value if provided'); - return Promise.resolve(); - } - }); - }); - - describe('put()', function () { - it('uploads a Blob', async function () { - const jsonDerulo = JSON.stringify({ foo: 'bar' }); - - const bob = new jet.context.Blob([jsonDerulo], { - type: 'application/json', - }); - - const uploadTaskSnapshot = await firebase - .storage() - .ref(`${PATH}/putStringBlob.json`) - .put(bob); - - uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); - uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); - uploadTaskSnapshot.metadata.should.be.an.Object(); - }); - - it('uploads an ArrayBuffer', async function () { - const jsonDerulo = JSON.stringify({ foo: 'bar' }); - - const arrayBuffer = new jet.context.window.ArrayBuffer(jsonDerulo.length); - const arrayBufferView = new jet.context.window.Uint8Array(arrayBuffer); - - for (let i = 0, strLen = jsonDerulo.length; i < strLen; i++) { - arrayBufferView[i] = jsonDerulo.charCodeAt(i); - } - - const uploadTaskSnapshot = await firebase - .storage() - .ref(`${PATH}/putStringArrayBuffer.json`) - .put(arrayBuffer, { - contentType: 'application/json', - }); - - uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); - uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); - uploadTaskSnapshot.metadata.should.be.an.Object(); - }); - - it('uploads an Uint8Array', async function () { - const jsonDerulo = JSON.stringify({ foo: 'bar' }); - - const arrayBuffer = new jet.context.window.ArrayBuffer(jsonDerulo.length); - const unit8Array = new jet.context.window.Uint8Array(arrayBuffer); - - for (let i = 0, strLen = jsonDerulo.length; i < strLen; i++) { - unit8Array[i] = jsonDerulo.charCodeAt(i); - } - - const uploadTaskSnapshot = await firebase - .storage() - .ref(`${PATH}/putStringUint8Array.json`) - .put(unit8Array, { - contentType: 'application/json', - }); - - uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); - uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); - uploadTaskSnapshot.metadata.should.be.an.Object(); - }); - - it('should have access to the snapshot values outside of the Task thennable', async function () { - const jsonDerulo = JSON.stringify({ foo: 'bar' }); - - const bob = new jet.context.Blob([jsonDerulo], { - type: 'application/json', - }); - - const uploadTaskSnapshot = firebase.storage().ref(`${PATH}/putStringBlob.json`).put(bob); - - await uploadTaskSnapshot; - - const snapshot = uploadTaskSnapshot.snapshot; - - snapshotProperties(snapshot); - }); - }); - - describe('upload tasks', function () { - // before(async function () { - // // TODO we need some semi-large assets to upload and download I think? - // await firebase - // .storage() - // .ref('/ok.jpeg') - // .writeToFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/ok.jpeg`); - // await firebase - // .storage() - // .ref('/cat.gif') - // .writeToFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/cat.gif`); - // await firebase - // .storage() - // .ref('/hei.heic') - // .writeToFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/hei.heic`); - // }); - - // TODO followup - works against live storage but emulator inverts permission / not found errors - xit('errors if permission denied', async function () { - try { - await firebase - .storage() - .ref('/uploadNope.jpeg') - .putFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/ok.jpeg`); - return Promise.reject(new Error('No permission denied error')); - } catch (error) { - error.code.should.equal('storage/unauthorized'); - error.message.includes('not authorized').should.be.true(); - return Promise.resolve(); - } - }); - - // TODO followup - works against live storage but emulator inverts permission / not found errors - xit('supports thenable .catch()', async function () { - const out = await firebase - .storage() - .ref('/uploadNope.jpeg') - .putFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/ok.jpeg`) - .catch(error => { - error.code.should.equal('storage/unauthorized'); - error.message.includes('not authorized').should.be.true(); - return 1; - }); - should.equal(out, 1); - }); - - // TODO we don't have files seeded on the device, but could do so from test helpers - xit('uploads files with contentType detection', async function () { - let uploadTaskSnapshot = await firebase - .storage() - .ref(`${PATH}/uploadOk.jpeg`) - .putFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/ok.jpeg`); - - uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); - uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); - uploadTaskSnapshot.metadata.should.be.an.Object(); - uploadTaskSnapshot.metadata.contentType.should.equal('image/jpeg'); - - uploadTaskSnapshot = await firebase - .storage() - .ref('/uploadCat.gif') - // uri decode test - .putFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}%2Fcat.gif`); - - uploadTaskSnapshot.metadata.should.be.an.Object(); - uploadTaskSnapshot.metadata.contentType.should.equal('image/gif'); - - if (device.getPlatform() === 'ios') { - uploadTaskSnapshot = await firebase - .storage() - .ref('/uploadHei.heic') - .putFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/hei.heic`); - - uploadTaskSnapshot.metadata.should.be.an.Object(); - uploadTaskSnapshot.metadata.contentType.should.equal('image/heic'); - } - }); - - it('uploads a file without read permission', async function () { - const uploadTaskSnapshot = await firebase - .storage() - .ref(WRITE_ONLY_NAME) - .putString('Just a string to put in a file for upload'); - - uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); - uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); - uploadTaskSnapshot.metadata.should.be.an.Object(); - }); - - it('should have access to the snapshot values outside of the Task thennable', async function () { - const uploadTaskSnapshot = firebase - .storage() - .ref(`${PATH}/putStringBlob.json`) - .putString('Just a string to put in a file for upload'); - - await uploadTaskSnapshot; - - const snapshot = uploadTaskSnapshot.snapshot; - - snapshotProperties(snapshot); - }); - - it('should have access to the snapshot values outside of the event subscriber', async function () { - const uploadTaskSnapshot = firebase - .storage() - .ref(`${PATH}/putStringBlob.json`) - .putString('Just a string to put in a file for upload'); - - const { resolve, promise } = Promise.defer(); - - uploadTaskSnapshot.on('state_changed', { - next: snapshot => { - if (snapshot.state === firebase.storage.TaskState.SUCCESS) { - snapshotProperties(snapshot); - resolve(); - } - }, - }); - await promise; - }); - }); - - describe('on()', function () { - before(async function () { - await firebase - .storage() - .ref(`${PATH}/ok.jpeg`) - .putString('Just a string to put in a file for upload'); - }); - - it('throws an Error if event is invalid', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - const task = storageReference.putFile('abc'); - task.on('foo'); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql( - "event argument must be a string with a value of 'state_changed'", - ); - return Promise.resolve(); - } - }); - - it('throws an Error if nextOrObserver is invalid', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - const task = storageReference.putFile('abc'); - task.on('state_changed', 'not a fn'); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql("'nextOrObserver' must be a Function, an Object or Null"); - return Promise.resolve(); - } - }); - - it('observer calls error callback', async function () { - const ref = firebase.storage().ref(`${PATH}/uploadOk.jpeg`); - const { resolve, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/notFoundFooFile.bar`; - const task = ref.putFile(path); - - task.on('state_changed', { - error: error => { - error.code.should.containEql('storage/file-not-found'); - resolve(); - }, - }); - - try { - await task; - } catch (error) { - error.code.should.containEql('storage/file-not-found'); - } - - await promise; - }); - - it('observer: calls next callback', async function () { - const ref = firebase.storage().ref(`${PATH}/ok.jpeg`); - const { resolve, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; - const task = ref.writeToFile(path); - - task.on('state_changed', { - next: snapshot => { - if (snapshot.state === firebase.storage.TaskState.SUCCESS) { - resolve(); - } - }, - }); - - await task; - await promise; - }); - - it('observer: calls completion callback', async function () { - const ref = firebase.storage().ref(`${PATH}/ok.jpeg`); - const { resolve, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; - const task = ref.writeToFile(path); - - task.on('state_changed', { - complete: snapshot => { - snapshot.state.should.equal(firebase.storage.TaskState.SUCCESS); - resolve(); - }, - }); - - await task; - await promise; - }); - - it('calls error callback', async function () { - const ref = firebase.storage().ref(`${PATH}/uploadOk.jpeg`); - const { resolve, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/notFoundFooFile.bar`; - const task = ref.putFile(path); - - task.on( - 'state_changed', - null, - error => { - error.code.should.containEql('storage/file-not-found'); - resolve(); - }, - null, - ); - - try { - await task; - } catch (error) { - error.code.should.containEql('storage/file-not-found'); - } - - await promise; - }); - - it('calls next callback', async function () { - const ref = firebase.storage().ref(`${PATH}/ok.jpeg`); - const { resolve, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; - const task = ref.writeToFile(path); - - task.on('state_changed', snapshot => { - if (snapshot.state === firebase.storage.TaskState.SUCCESS) { - resolve(); - } - }); - - await task; - await promise; - }); - - it('calls completion callback', async function () { - const ref = firebase.storage().ref(`${PATH}/ok.jpeg`); - const { resolve, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; - const task = ref.writeToFile(path); - - task.on('state_changed', null, null, snapshot => { - snapshot.state.should.equal(firebase.storage.TaskState.SUCCESS); - resolve(); - }); - - await task; - await promise; - }); - - it('returns a subscribe fn', async function () { - const ref = firebase.storage().ref(`${PATH}/ok.jpeg`); - const { resolve, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; - const task = ref.writeToFile(path); - - const subscribe = task.on('state_changed'); - - subscribe(null, null, snapshot => { - if (snapshot.state === firebase.storage.TaskState.SUCCESS) { - resolve(); - } - }); - - await task; - await promise; - }); - - it('returns a subscribe fn supporting observer usage syntax', async function () { - const ref = firebase.storage().ref(`${PATH}/ok.jpeg`); - const { resolve, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; - const task = ref.writeToFile(path); - - const subscribe = task.on('state_changed'); - - subscribe({ - complete: snapshot => { - if (snapshot.state === firebase.storage.TaskState.SUCCESS) { - resolve(); - } - }, - }); - - await task; - await promise; - }); - - it('listens to download state', async function () { - const ref = firebase.storage().ref(`${PATH}/ok.jpeg`); - const { resolve, reject, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.gif`; - - const unsubscribe = ref.writeToFile(path).on( - 'state_changed', - snapshot => { - if (snapshot.state === firebase.storage.TaskState.SUCCESS) { - resolve(); - } - }, - error => { - unsubscribe(); - reject(error); - }, - ); - - await promise; - }); - - it('listens to upload state', async function () { - const { resolve, reject, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.gif`; - const ref = firebase.storage().ref(`${PATH}/uploadOk.jpeg`); - - const unsubscribe = ref.putFile(path).on( - 'state_changed', - snapshot => { - if (snapshot.state === firebase.storage.TaskState.SUCCESS) { - resolve(); - } - }, - error => { - unsubscribe(); - reject(error); - }, - ); - - await promise; - }); - }); - - // TODO get files staged for emulator testing - xdescribe('pause() resume()', function () { - it('successfully pauses and resumes an upload', async function testRunner() { - this.timeout(100 * 1000); - - await firebase - .storage() - .ref(device.getPlatform() === 'ios' ? '/smallFileTest.png' : '/cat.gif') - .writeToFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/pauseUpload_test1.gif`); - - const ref = firebase.storage().ref('/uploadCat.gif'); - const { resolve, reject, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/pauseUpload_test1.gif`; - const uploadTask = ref.putFile(path); - - let hadRunningStatus = false; - let hadPausedStatus = false; - let hadResumedStatus = false; - - uploadTask.on( - 'state_changed', - snapshot => { - // 1) pause when we receive first running event - if (snapshot.state === firebase.storage.TaskState.RUNNING && !hadRunningStatus) { - hadRunningStatus = true; - if (device.getPlatform() === 'android') { - uploadTask.pause(); - } else { - // TODO (salakar) submit bug report to Firebase iOS SDK repo (pausing immediately after the first progress event will fail the upload with an unknown error) - setTimeout(() => { - uploadTask.pause(); - }, 750); - } - } - - // 2) resume when we receive first paused event - if (snapshot.state === firebase.storage.TaskState.PAUSED) { - hadPausedStatus = true; - uploadTask.resume(); - } - - // 3) track that we resumed on 2nd running status whilst paused - if ( - snapshot.state === firebase.storage.TaskState.RUNNING && - hadRunningStatus && - hadPausedStatus && - !hadResumedStatus - ) { - hadResumedStatus = true; - } - - // 4) finally confirm we received all statuses - if (snapshot.state === firebase.storage.TaskState.SUCCESS) { - should.equal(hadRunningStatus, true); - should.equal(hadPausedStatus, true); - should.equal(hadResumedStatus, true); - resolve(); - } - }, - error => { - reject(error); - }, - ); - - await promise; - }); - - it('successfully pauses and resumes a download', async function () { - const ref = firebase - .storage() - .ref(device.getPlatform() === 'ios' ? '/1mbTestFile.gif' : '/cat.gif'); - - const { resolve, reject, promise } = Promise.defer(); - - // random file name as Android does not allow overriding if file already exists - const path = `${ - firebase.utils.FilePath.DOCUMENT_DIRECTORY - }/invertase/pauseDownload${Math.round(Math.random() * 1000)}.gif`; - const downloadTask = ref.writeToFile(path); - - let hadRunningStatus = false; - let hadPausedStatus = false; - let hadResumedStatus = false; - - downloadTask.on( - 'state_changed', - snapshot => { - // TODO(salakar) validate snapshot props - // 1) pause when we receive first running event - if (snapshot.state === firebase.storage.TaskState.RUNNING && !hadRunningStatus) { - hadRunningStatus = true; - downloadTask.pause(); - } - - // 2) resume when we receive first paused event - if (snapshot.state === firebase.storage.TaskState.PAUSED) { - hadPausedStatus = true; - downloadTask.resume(); - } - - // 3) track that we resumed on 2nd running status whilst paused - if ( - snapshot.state === firebase.storage.TaskState.RUNNING && - hadRunningStatus && - hadPausedStatus && - !hadResumedStatus - ) { - hadResumedStatus = true; - } - - // 4) finally confirm we received all statuses - if (snapshot.state === firebase.storage.TaskState.SUCCESS) { - should.equal(hadRunningStatus, true); - should.equal(hadPausedStatus, true); - should.equal(hadResumedStatus, true); - resolve(); - } - }, - error => { - reject(error); - }, - ); - - await promise; - }); - }); - - describe('cancel()', function () { - // TODO stage a file big enough to test upload cancel - xit('successfully cancels an upload', async function () { - await firebase - .storage() - .ref('/1mbTestFile.gif') - .writeToFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/pauseUpload.gif`); - - const ref = firebase.storage().ref('/successful-cancel.jpg'); - - //Upload and cancel - const { resolve, reject, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/pauseUpload.gif`; - const uploadTask = ref.putFile(path); - - let hadRunningStatus = false; - let hadCancelledStatus = false; - - uploadTask.on( - 'state_changed', - snapshot => { - // TODO(salakar) validate snapshot props - // 1) cancel it when we receive first running event - if (snapshot.state === firebase.storage.TaskState.RUNNING && !hadRunningStatus) { - hadRunningStatus = true; - uploadTask.cancel(); - } - - // 2) confirm cancellation - if (snapshot.state === firebase.storage.TaskState.CANCELLED) { - should.equal(hadRunningStatus, true); - hadCancelledStatus = true; - } - - if (snapshot.state === firebase.storage.TaskState.ERROR) { - throw new Error('Should not error if cancelled?'); - } - - if (snapshot.state === firebase.storage.TaskState.SUCCESS) { - reject(new Error('UploadTask did not cancel!')); - } - }, - error => { - should.equal(hadRunningStatus, true); - should.equal(hadCancelledStatus, true); - error.code.should.equal('storage/cancelled'); - error.message.should.containEql('User cancelled the operation.'); - resolve(); - }, - ); - - await promise; - }); - }); - - // TODO stage a file big enough to cancel a download - xit('successfully cancels a download', async function () { - await Utils.sleep(10000); - const ref = firebase.storage().ref('/1mbTestFile.gif'); - const { resolve, reject, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/testUploadFile.jpg`; - const downloadTask = ref.writeToFile(path); - - let hadRunningStatus = false; - let hadCancelledStatus = false; - - downloadTask.on( - 'state_changed', - snapshot => { - // TODO(salakar) validate snapshot props - // 1) cancel it when we receive first running event - if (snapshot.state === firebase.storage.TaskState.RUNNING && !hadRunningStatus) { - hadRunningStatus = true; - downloadTask.cancel(); - } - - // 2) confirm cancellation - if (snapshot.state === firebase.storage.TaskState.CANCELLED) { - should.equal(hadRunningStatus, true); - hadCancelledStatus = true; - } - - if (snapshot.state === firebase.storage.TaskState.ERROR) { - throw new Error('Should not error if cancelled?'); - } - - if (snapshot.state === firebase.storage.TaskState.SUCCESS) { - reject(new Error('DownloadTask did not cancel!')); - } - }, - error => { - should.equal(hadRunningStatus, true); - should.equal(hadCancelledStatus, true); - error.code.should.equal('storage/cancelled'); - error.message.should.containEql('User cancelled the operation.'); - resolve(); - }, - ); - - await promise; - }); -}); diff --git a/packages/storage/e2e/StorageTask.modular.e2e.js b/packages/storage/e2e/StorageTask.modular.e2e.js new file mode 100644 index 0000000000..aed61be07b --- /dev/null +++ b/packages/storage/e2e/StorageTask.modular.e2e.js @@ -0,0 +1,1623 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +const { PATH, seed, WRITE_ONLY_NAME } = require('./helpers'); + +function snapshotProperties(snapshot) { + snapshot.should.have.property('state'); + snapshot.should.have.property('metadata'); + snapshot.should.have.property('ref'); + snapshot.should.have.property('task'); + snapshot.should.have.property('totalBytes'); + snapshot.should.have.property('bytesTransferred'); +} + +describe('storage() -> StorageTask', function () { + describe('storage() -> StorageTask modular', function () { + describe('firebase v8 compatibility', function () { + before(async function () { + await seed(PATH); + }); + describe('writeToFile()', function () { + // TODO - followup - the storage emulator currently inverts not-found / permission error conditions + // this one returns the permission denied against live storage, but object not found against emulator + xit('errors if permission denied', async function () { + try { + await firebase + .storage() + .ref('/not.jpg') + .writeToFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/not.jpg`); + return Promise.reject(new Error('No permission denied error')); + } catch (error) { + error.code.should.equal('storage/unauthorized'); + error.message.includes('not authorized').should.be.true(); + return Promise.resolve(); + } + }); + it('downloads a file', async function () { + const meta = await firebase + .storage() + .ref(`${PATH}/list/file1.txt`) + .writeToFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/file1.txt`); + meta.state.should.eql(firebase.storage.TaskState.SUCCESS); + meta.bytesTransferred.should.eql(meta.totalBytes); + }); + }); + describe('putString()', function () { + it('uploads a raw string', async function () { + const jsonDerulo = JSON.stringify({ foo: 'bar' }); + const uploadTaskSnapshot = await firebase + .storage() + .ref(`${PATH}/putString.json`) + .putString(jsonDerulo, firebase.storage.StringFormat.RAW, { + contentType: 'application/json', + }); + uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); + uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); + uploadTaskSnapshot.metadata.should.be.an.Object(); + }); + it('uploads a data_url formatted string', async function () { + const dataUrl = 'data:application/json;base64,eyJmb28iOiJiYXNlNjQifQ=='; + const uploadTaskSnapshot = await firebase + .storage() + .ref(`${PATH}/putStringDataURL.json`) + .putString(dataUrl, firebase.storage.StringFormat.DATA_URL); + uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); + uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); + uploadTaskSnapshot.metadata.should.be.an.Object(); + }); + it('uploads a url encoded data_url formatted string', async function () { + const dataUrl = 'data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E'; + const uploadTaskSnapshot = await firebase + .storage() + .ref(`${PATH}/helloWorld.html`) + .putString(dataUrl, firebase.storage.StringFormat.DATA_URL); + uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); + uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); + uploadTaskSnapshot.metadata.should.be.an.Object(); + }); + it('when using data_url it still sets the content type if metadata is provided', async function () { + const dataUrl = 'data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E'; + const uploadTaskSnapshot = await firebase + .storage() + .ref(`${PATH}/helloWorld.html`) + .putString(dataUrl, firebase.storage.StringFormat.DATA_URL, { + // TODO(salakar) automate test metadata is preserved when auto setting mediatype + customMetadata: { + hello: 'world', + }, + }); + uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); + uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); + uploadTaskSnapshot.metadata.should.be.an.Object(); + }); + it('uploads a base64 string', async function () { + const base64String = 'eyJmb28iOiJiYXNlNjQifQ=='; + const uploadTaskSnapshot = await firebase + .storage() + .ref(`${PATH}/putStringBase64.json`) + .putString(base64String, firebase.storage.StringFormat.BASE64, { + contentType: 'application/json', + }); + uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); + uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); + uploadTaskSnapshot.metadata.should.be.an.Object(); + }); + it('uploads a base64url string', async function () { + const base64UrlString = 'eyJmb28iOiJiYXNlNjQifQ'; + const uploadTaskSnapshot = await firebase + .storage() + .ref(`${PATH}/putStringBase64Url.json`) + .putString(base64UrlString, firebase.storage.StringFormat.BASE64URL, { + contentType: 'application/json', + }); + uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); + uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); + uploadTaskSnapshot.metadata.should.be.an.Object(); + }); + it('throws an error on invalid data_url', async function () { + const dataUrl = ''; + try { + await firebase + .storage() + .ref('/a.b') + .putString(dataUrl, firebase.storage.StringFormat.DATA_URL); + return Promise.reject(new Error('Did not throw!')); + } catch (error) { + error.message.should.containEql('invalid data_url string provided'); + return Promise.resolve(); + } + }); + it('throws if string arg is not a valid string', async function () { + try { + await firebase.storage().ref('/a.b').putString(1, 'base64'); + return Promise.reject(new Error('Did not throw!')); + } catch (error) { + error.message.should.containEql("'string' expects a string value"); + return Promise.resolve(); + } + }); + it('throws an error on invalid string format', async function () { + try { + await firebase.storage().ref('/a.b').putString('fooby', 'abc'); + return Promise.reject(new Error('Did not throw!')); + } catch (error) { + error.message.should.containEql("'format' provided is invalid, must be one of"); + return Promise.resolve(); + } + }); + it('throws an error if metadata is not an object', async function () { + try { + await firebase.storage().ref('/a.b').putString('fooby', 'raw', 1234); + return Promise.reject(new Error('Did not throw!')); + } catch (error) { + error.message.should.containEql('must be an object value if provided'); + return Promise.resolve(); + } + }); + }); + describe('put()', function () { + it('uploads a Blob', async function () { + const jsonDerulo = JSON.stringify({ foo: 'bar' }); + const bob = new jet.context.Blob([jsonDerulo], { + type: 'application/json', + }); + const uploadTaskSnapshot = await firebase + .storage() + .ref(`${PATH}/putStringBlob.json`) + .put(bob); + uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); + uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); + uploadTaskSnapshot.metadata.should.be.an.Object(); + }); + it('uploads an ArrayBuffer', async function () { + const jsonDerulo = JSON.stringify({ foo: 'bar' }); + const arrayBuffer = new jet.context.window.ArrayBuffer(jsonDerulo.length); + const arrayBufferView = new jet.context.window.Uint8Array(arrayBuffer); + for (let i = 0, strLen = jsonDerulo.length; i < strLen; i++) { + arrayBufferView[i] = jsonDerulo.charCodeAt(i); + } + const uploadTaskSnapshot = await firebase + .storage() + .ref(`${PATH}/putStringArrayBuffer.json`) + .put(arrayBuffer, { + contentType: 'application/json', + }); + uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); + uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); + uploadTaskSnapshot.metadata.should.be.an.Object(); + }); + it('uploads an Uint8Array', async function () { + const jsonDerulo = JSON.stringify({ foo: 'bar' }); + const arrayBuffer = new jet.context.window.ArrayBuffer(jsonDerulo.length); + const unit8Array = new jet.context.window.Uint8Array(arrayBuffer); + for (let i = 0, strLen = jsonDerulo.length; i < strLen; i++) { + unit8Array[i] = jsonDerulo.charCodeAt(i); + } + const uploadTaskSnapshot = await firebase + .storage() + .ref(`${PATH}/putStringUint8Array.json`) + .put(unit8Array, { + contentType: 'application/json', + }); + uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); + uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); + uploadTaskSnapshot.metadata.should.be.an.Object(); + }); + it('should have access to the snapshot values outside of the Task thennable', async function () { + const jsonDerulo = JSON.stringify({ foo: 'bar' }); + const bob = new jet.context.Blob([jsonDerulo], { + type: 'application/json', + }); + const uploadTaskSnapshot = firebase.storage().ref(`${PATH}/putStringBlob.json`).put(bob); + await uploadTaskSnapshot; + const snapshot = uploadTaskSnapshot.snapshot; + snapshotProperties(snapshot); + }); + }); + describe('upload tasks', function () { + // before(async function () { + // // TODO we need some semi-large assets to upload and download I think? + // await firebase + // .storage() + // .ref('/ok.jpeg') + // .writeToFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/ok.jpeg`); + // await firebase + // .storage() + // .ref('/cat.gif') + // .writeToFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/cat.gif`); + // await firebase + // .storage() + // .ref('/hei.heic') + // .writeToFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/hei.heic`); + // }); + // TODO followup - works against live storage but emulator inverts permission / not found errors + xit('errors if permission denied', async function () { + try { + await firebase + .storage() + .ref('/uploadNope.jpeg') + .putFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/ok.jpeg`); + return Promise.reject(new Error('No permission denied error')); + } catch (error) { + error.code.should.equal('storage/unauthorized'); + error.message.includes('not authorized').should.be.true(); + return Promise.resolve(); + } + }); + // TODO followup - works against live storage but emulator inverts permission / not found errors + xit('supports thenable .catch()', async function () { + const out = await firebase + .storage() + .ref('/uploadNope.jpeg') + .putFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/ok.jpeg`) + .catch(error => { + error.code.should.equal('storage/unauthorized'); + error.message.includes('not authorized').should.be.true(); + return 1; + }); + should.equal(out, 1); + }); + // TODO we don't have files seeded on the device, but could do so from test helpers + xit('uploads files with contentType detection', async function () { + let uploadTaskSnapshot = await firebase + .storage() + .ref(`${PATH}/uploadOk.jpeg`) + .putFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/ok.jpeg`); + uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); + uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); + uploadTaskSnapshot.metadata.should.be.an.Object(); + uploadTaskSnapshot.metadata.contentType.should.equal('image/jpeg'); + uploadTaskSnapshot = await firebase + .storage() + .ref('/uploadCat.gif') + // uri decode test + .putFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}%2Fcat.gif`); + uploadTaskSnapshot.metadata.should.be.an.Object(); + uploadTaskSnapshot.metadata.contentType.should.equal('image/gif'); + if (device.getPlatform() === 'ios') { + uploadTaskSnapshot = await firebase + .storage() + .ref('/uploadHei.heic') + .putFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/hei.heic`); + uploadTaskSnapshot.metadata.should.be.an.Object(); + uploadTaskSnapshot.metadata.contentType.should.equal('image/heic'); + } + }); + it('uploads a file without read permission', async function () { + const uploadTaskSnapshot = await firebase + .storage() + .ref(WRITE_ONLY_NAME) + .putString('Just a string to put in a file for upload'); + uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); + uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); + uploadTaskSnapshot.metadata.should.be.an.Object(); + }); + it('should have access to the snapshot values outside of the Task thennable', async function () { + const uploadTaskSnapshot = firebase + .storage() + .ref(`${PATH}/putStringBlob.json`) + .putString('Just a string to put in a file for upload'); + await uploadTaskSnapshot; + const snapshot = uploadTaskSnapshot.snapshot; + snapshotProperties(snapshot); + }); + it('should have access to the snapshot values outside of the event subscriber', async function () { + const uploadTaskSnapshot = firebase + .storage() + .ref(`${PATH}/putStringBlob.json`) + .putString('Just a string to put in a file for upload'); + const { resolve, promise } = Promise.defer(); + uploadTaskSnapshot.on('state_changed', { + next: snapshot => { + if (snapshot.state === firebase.storage.TaskState.SUCCESS) { + snapshotProperties(snapshot); + resolve(); + } + }, + }); + await promise; + }); + }); + describe('on()', function () { + before(async function () { + await firebase + .storage() + .ref(`${PATH}/ok.jpeg`) + .putString('Just a string to put in a file for upload'); + }); + it('throws an Error if event is invalid', async function () { + const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); + try { + const task = storageReference.putFile('abc'); + task.on('foo'); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql( + "event argument must be a string with a value of 'state_changed'", + ); + return Promise.resolve(); + } + }); + it('throws an Error if nextOrObserver is invalid', async function () { + const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); + try { + const task = storageReference.putFile('abc'); + task.on('state_changed', 'not a fn'); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql( + "'nextOrObserver' must be a Function, an Object or Null", + ); + return Promise.resolve(); + } + }); + it('observer calls error callback', async function () { + const ref = firebase.storage().ref(`${PATH}/uploadOk.jpeg`); + const { resolve, promise } = Promise.defer(); + const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/notFoundFooFile.bar`; + const task = ref.putFile(path); + task.on('state_changed', { + error: error => { + error.code.should.containEql('storage/file-not-found'); + resolve(); + }, + }); + try { + await task; + } catch (error) { + error.code.should.containEql('storage/file-not-found'); + } + await promise; + }); + it('observer: calls next callback', async function () { + const ref = firebase.storage().ref(`${PATH}/ok.jpeg`); + const { resolve, promise } = Promise.defer(); + const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; + const task = ref.writeToFile(path); + task.on('state_changed', { + next: snapshot => { + if (snapshot.state === firebase.storage.TaskState.SUCCESS) { + resolve(); + } + }, + }); + await task; + await promise; + }); + it('observer: calls completion callback', async function () { + const ref = firebase.storage().ref(`${PATH}/ok.jpeg`); + const { resolve, promise } = Promise.defer(); + const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; + const task = ref.writeToFile(path); + task.on('state_changed', { + complete: snapshot => { + snapshot.state.should.equal(firebase.storage.TaskState.SUCCESS); + resolve(); + }, + }); + await task; + await promise; + }); + it('calls error callback', async function () { + const ref = firebase.storage().ref(`${PATH}/uploadOk.jpeg`); + const { resolve, promise } = Promise.defer(); + const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/notFoundFooFile.bar`; + const task = ref.putFile(path); + task.on( + 'state_changed', + null, + error => { + error.code.should.containEql('storage/file-not-found'); + resolve(); + }, + null, + ); + try { + await task; + } catch (error) { + error.code.should.containEql('storage/file-not-found'); + } + await promise; + }); + it('calls next callback', async function () { + const ref = firebase.storage().ref(`${PATH}/ok.jpeg`); + const { resolve, promise } = Promise.defer(); + const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; + const task = ref.writeToFile(path); + task.on('state_changed', snapshot => { + if (snapshot.state === firebase.storage.TaskState.SUCCESS) { + resolve(); + } + }); + await task; + await promise; + }); + it('calls completion callback', async function () { + const ref = firebase.storage().ref(`${PATH}/ok.jpeg`); + const { resolve, promise } = Promise.defer(); + const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; + const task = ref.writeToFile(path); + task.on('state_changed', null, null, snapshot => { + snapshot.state.should.equal(firebase.storage.TaskState.SUCCESS); + resolve(); + }); + await task; + await promise; + }); + it('returns a subscribe fn', async function () { + const ref = firebase.storage().ref(`${PATH}/ok.jpeg`); + const { resolve, promise } = Promise.defer(); + const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; + const task = ref.writeToFile(path); + const subscribe = task.on('state_changed'); + subscribe(null, null, snapshot => { + if (snapshot.state === firebase.storage.TaskState.SUCCESS) { + resolve(); + } + }); + await task; + await promise; + }); + it('returns a subscribe fn supporting observer usage syntax', async function () { + const ref = firebase.storage().ref(`${PATH}/ok.jpeg`); + const { resolve, promise } = Promise.defer(); + const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; + const task = ref.writeToFile(path); + const subscribe = task.on('state_changed'); + subscribe({ + complete: snapshot => { + if (snapshot.state === firebase.storage.TaskState.SUCCESS) { + resolve(); + } + }, + }); + await task; + await promise; + }); + it('listens to download state', async function () { + const ref = firebase.storage().ref(`${PATH}/ok.jpeg`); + const { resolve, reject, promise } = Promise.defer(); + const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.gif`; + const unsubscribe = ref.writeToFile(path).on( + 'state_changed', + snapshot => { + if (snapshot.state === firebase.storage.TaskState.SUCCESS) { + resolve(); + } + }, + error => { + unsubscribe(); + reject(error); + }, + ); + await promise; + }); + it('listens to upload state', async function () { + const { resolve, reject, promise } = Promise.defer(); + const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.gif`; + const ref = firebase.storage().ref(`${PATH}/uploadOk.jpeg`); + const unsubscribe = ref.putFile(path).on( + 'state_changed', + snapshot => { + if (snapshot.state === firebase.storage.TaskState.SUCCESS) { + resolve(); + } + }, + error => { + unsubscribe(); + reject(error); + }, + ); + await promise; + }); + }); + // TODO get files staged for emulator testing + xdescribe('pause() resume()', function () { + it('successfully pauses and resumes an upload', async function testRunner() { + this.timeout(100 * 1000); + await firebase + .storage() + .ref(device.getPlatform() === 'ios' ? '/smallFileTest.png' : '/cat.gif') + .writeToFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/pauseUpload_test1.gif`); + const ref = firebase.storage().ref('/uploadCat.gif'); + const { resolve, reject, promise } = Promise.defer(); + const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/pauseUpload_test1.gif`; + const uploadTask = ref.putFile(path); + let hadRunningStatus = false; + let hadPausedStatus = false; + let hadResumedStatus = false; + uploadTask.on( + 'state_changed', + snapshot => { + // 1) pause when we receive first running event + if (snapshot.state === firebase.storage.TaskState.RUNNING && !hadRunningStatus) { + hadRunningStatus = true; + if (device.getPlatform() === 'android') { + uploadTask.pause(); + } else { + // TODO (salakar) submit bug report to Firebase iOS SDK repo (pausing immediately after the first progress event will fail the upload with an unknown error) + setTimeout(() => { + uploadTask.pause(); + }, 750); + } + } + // 2) resume when we receive first paused event + if (snapshot.state === firebase.storage.TaskState.PAUSED) { + hadPausedStatus = true; + uploadTask.resume(); + } + // 3) track that we resumed on 2nd running status whilst paused + if ( + snapshot.state === firebase.storage.TaskState.RUNNING && + hadRunningStatus && + hadPausedStatus && + !hadResumedStatus + ) { + hadResumedStatus = true; + } + // 4) finally confirm we received all statuses + if (snapshot.state === firebase.storage.TaskState.SUCCESS) { + should.equal(hadRunningStatus, true); + should.equal(hadPausedStatus, true); + should.equal(hadResumedStatus, true); + resolve(); + } + }, + error => { + reject(error); + }, + ); + await promise; + }); + it('successfully pauses and resumes a download', async function () { + const ref = firebase + .storage() + .ref(device.getPlatform() === 'ios' ? '/1mbTestFile.gif' : '/cat.gif'); + const { resolve, reject, promise } = Promise.defer(); + // random file name as Android does not allow overriding if file already exists + const path = `${ + firebase.utils.FilePath.DOCUMENT_DIRECTORY + }/invertase/pauseDownload${Math.round(Math.random() * 1000)}.gif`; + const downloadTask = ref.writeToFile(path); + let hadRunningStatus = false; + let hadPausedStatus = false; + let hadResumedStatus = false; + downloadTask.on( + 'state_changed', + snapshot => { + // TODO(salakar) validate snapshot props + // 1) pause when we receive first running event + if (snapshot.state === firebase.storage.TaskState.RUNNING && !hadRunningStatus) { + hadRunningStatus = true; + downloadTask.pause(); + } + // 2) resume when we receive first paused event + if (snapshot.state === firebase.storage.TaskState.PAUSED) { + hadPausedStatus = true; + downloadTask.resume(); + } + // 3) track that we resumed on 2nd running status whilst paused + if ( + snapshot.state === firebase.storage.TaskState.RUNNING && + hadRunningStatus && + hadPausedStatus && + !hadResumedStatus + ) { + hadResumedStatus = true; + } + // 4) finally confirm we received all statuses + if (snapshot.state === firebase.storage.TaskState.SUCCESS) { + should.equal(hadRunningStatus, true); + should.equal(hadPausedStatus, true); + should.equal(hadResumedStatus, true); + resolve(); + } + }, + error => { + reject(error); + }, + ); + await promise; + }); + }); + describe('cancel()', function () { + // TODO stage a file big enough to test upload cancel + xit('successfully cancels an upload', async function () { + await firebase + .storage() + .ref('/1mbTestFile.gif') + .writeToFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/pauseUpload.gif`); + const ref = firebase.storage().ref('/successful-cancel.jpg'); + //Upload and cancel + const { resolve, reject, promise } = Promise.defer(); + const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/pauseUpload.gif`; + const uploadTask = ref.putFile(path); + let hadRunningStatus = false; + let hadCancelledStatus = false; + uploadTask.on( + 'state_changed', + snapshot => { + // TODO(salakar) validate snapshot props + // 1) cancel it when we receive first running event + if (snapshot.state === firebase.storage.TaskState.RUNNING && !hadRunningStatus) { + hadRunningStatus = true; + uploadTask.cancel(); + } + // 2) confirm cancellation + if (snapshot.state === firebase.storage.TaskState.CANCELLED) { + should.equal(hadRunningStatus, true); + hadCancelledStatus = true; + } + if (snapshot.state === firebase.storage.TaskState.ERROR) { + throw new Error('Should not error if cancelled?'); + } + if (snapshot.state === firebase.storage.TaskState.SUCCESS) { + reject(new Error('UploadTask did not cancel!')); + } + }, + error => { + should.equal(hadRunningStatus, true); + should.equal(hadCancelledStatus, true); + error.code.should.equal('storage/cancelled'); + error.message.should.containEql('User cancelled the operation.'); + resolve(); + }, + ); + await promise; + }); + }); + // TODO stage a file big enough to cancel a download + xit('successfully cancels a download', async function () { + await Utils.sleep(10000); + const ref = firebase.storage().ref('/1mbTestFile.gif'); + const { resolve, reject, promise } = Promise.defer(); + const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/testUploadFile.jpg`; + const downloadTask = ref.writeToFile(path); + let hadRunningStatus = false; + let hadCancelledStatus = false; + downloadTask.on( + 'state_changed', + snapshot => { + // TODO(salakar) validate snapshot props + // 1) cancel it when we receive first running event + if (snapshot.state === firebase.storage.TaskState.RUNNING && !hadRunningStatus) { + hadRunningStatus = true; + downloadTask.cancel(); + } + // 2) confirm cancellation + if (snapshot.state === firebase.storage.TaskState.CANCELLED) { + should.equal(hadRunningStatus, true); + hadCancelledStatus = true; + } + if (snapshot.state === firebase.storage.TaskState.ERROR) { + throw new Error('Should not error if cancelled?'); + } + if (snapshot.state === firebase.storage.TaskState.SUCCESS) { + reject(new Error('DownloadTask did not cancel!')); + } + }, + error => { + should.equal(hadRunningStatus, true); + should.equal(hadCancelledStatus, true); + error.code.should.equal('storage/cancelled'); + error.message.should.containEql('User cancelled the operation.'); + resolve(); + }, + ); + await promise; + }); + }); + }); + + describe('StorageTask modular', function () { + before(async function () { + await seed(PATH); + }); + + describe('writeToFile()', function () { + // TODO - followup - the storage emulator currently inverts not-found / permission error conditions + // this one returns the permission denied against live storage, but object not found against emulator + xit('errors if permission denied', async function () { + const { getStorage, ref } = storageModular; + try { + await ref(getStorage(), '/not.jpg').writeToFile( + `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/not.jpg`, + ); + return Promise.reject(new Error('No permission denied error')); + } catch (error) { + error.code.should.equal('storage/unauthorized'); + error.message.includes('not authorized').should.be.true(); + return Promise.resolve(); + } + }); + + it('downloads a file', async function () { + const { getStorage, ref } = storageModular; + + const meta = await ref(getStorage(), `${PATH}/list/file1.txt`).writeToFile( + `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/file1.txt`, + ); + + meta.state.should.eql(firebase.storage.TaskState.SUCCESS); + meta.bytesTransferred.should.eql(meta.totalBytes); + }); + }); + + describe('putString()', function () { + it('uploads a raw string', async function () { + const { getStorage, ref, uploadString } = storageModular; + + const jsonDerulo = JSON.stringify({ foo: 'bar' }); + + const uploadTaskSnapshot = await uploadString( + ref(getStorage(), `${PATH}/putString.json`), + jsonDerulo, + firebase.storage.StringFormat.RAW, + { + contentType: 'application/json', + }, + ); + + uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); + uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); + uploadTaskSnapshot.metadata.should.be.an.Object(); + }); + + it('uploads a data_url formatted string', async function () { + const { getStorage, ref, uploadString } = storageModular; + + const dataUrl = 'data:application/json;base64,eyJmb28iOiJiYXNlNjQifQ=='; + + const uploadTaskSnapshot = await uploadString( + ref(getStorage(), `${PATH}/putStringDataURL.json`), + dataUrl, + firebase.storage.StringFormat.DATA_URL, + ); + + uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); + uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); + uploadTaskSnapshot.metadata.should.be.an.Object(); + }); + + it('uploads a url encoded data_url formatted string', async function () { + const { getStorage, ref, uploadString } = storageModular; + + const dataUrl = 'data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E'; + + const uploadTaskSnapshot = await uploadString( + ref(getStorage(), `${PATH}/helloWorld.html`), + dataUrl, + firebase.storage.StringFormat.DATA_URL, + ); + + uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); + uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); + uploadTaskSnapshot.metadata.should.be.an.Object(); + }); + + it('when using data_url it still sets the content type if metadata is provided', async function () { + const { getStorage, ref, uploadString } = storageModular; + const dataUrl = 'data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E'; + + const uploadTaskSnapshot = await uploadString( + ref(getStorage(), `${PATH}/helloWorld.html`), + dataUrl, + firebase.storage.StringFormat.DATA_URL, + { + // TODO(salakar) automate test metadata is preserved when auto setting mediatype + customMetadata: { + hello: 'world', + }, + }, + ); + + uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); + uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); + uploadTaskSnapshot.metadata.should.be.an.Object(); + }); + + it('uploads a base64 string', async function () { + const { getStorage, ref, uploadString } = storageModular; + const base64String = 'eyJmb28iOiJiYXNlNjQifQ=='; + + const uploadTaskSnapshot = await uploadString( + ref(getStorage(), `${PATH}/putStringBase64.json`), + base64String, + firebase.storage.StringFormat.BASE64, + { + contentType: 'application/json', + }, + ); + + uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); + uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); + uploadTaskSnapshot.metadata.should.be.an.Object(); + }); + + it('uploads a base64url string', async function () { + const { getStorage, ref, uploadString } = storageModular; + const base64UrlString = 'eyJmb28iOiJiYXNlNjQifQ'; + + const uploadTaskSnapshot = await uploadString( + ref(getStorage(), `${PATH}/putStringBase64Url.json`), + base64UrlString, + firebase.storage.StringFormat.BASE64URL, + { + contentType: 'application/json', + }, + ); + + uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); + uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); + uploadTaskSnapshot.metadata.should.be.an.Object(); + }); + + it('throws an error on invalid data_url', async function () { + const { getStorage, ref, uploadString } = storageModular; + const dataUrl = ''; + try { + await uploadString( + ref(getStorage(), '/a.b'), + dataUrl, + firebase.storage.StringFormat.DATA_URL, + ); + + return Promise.reject(new Error('Did not throw!')); + } catch (error) { + error.message.should.containEql('invalid data_url string provided'); + return Promise.resolve(); + } + }); + + it('throws if string arg is not a valid string', async function () { + const { getStorage, ref, uploadString } = storageModular; + try { + await uploadString(ref(getStorage(), '/a.b'), 1, 'base64'); + return Promise.reject(new Error('Did not throw!')); + } catch (error) { + error.message.should.containEql("'string' expects a string value"); + return Promise.resolve(); + } + }); + + it('throws an error on invalid string format', async function () { + const { getStorage, ref, uploadString } = storageModular; + try { + await uploadString(ref(getStorage(), '/a.b'), 'fooby', 'abc'); + return Promise.reject(new Error('Did not throw!')); + } catch (error) { + error.message.should.containEql("'format' provided is invalid, must be one of"); + return Promise.resolve(); + } + }); + + it('throws an error if metadata is not an object', async function () { + const { getStorage, ref, uploadString } = storageModular; + try { + await uploadString(ref(getStorage(), '/a.b'), 'fooby', 'raw', 1234); + return Promise.reject(new Error('Did not throw!')); + } catch (error) { + error.message.should.containEql('must be an object value if provided'); + return Promise.resolve(); + } + }); + }); + + describe('put()', function () { + it('uploads a Blob', async function () { + const { getStorage, ref, uploadBytesResumable } = storageModular; + const jsonDerulo = JSON.stringify({ foo: 'bar' }); + + const bob = new jet.context.Blob([jsonDerulo], { + type: 'application/json', + }); + + const uploadTaskSnapshot = await uploadBytesResumable( + ref(getStorage(), `${PATH}/putStringBlob.json`), + bob, + ); + + uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); + uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); + uploadTaskSnapshot.metadata.should.be.an.Object(); + }); + + it('uploads an ArrayBuffer', async function () { + const { getStorage, ref, uploadBytesResumable } = storageModular; + const jsonDerulo = JSON.stringify({ foo: 'bar' }); + + const arrayBuffer = new jet.context.window.ArrayBuffer(jsonDerulo.length); + const arrayBufferView = new jet.context.window.Uint8Array(arrayBuffer); + + for (let i = 0, strLen = jsonDerulo.length; i < strLen; i++) { + arrayBufferView[i] = jsonDerulo.charCodeAt(i); + } + const uploadTaskSnapshot = await uploadBytesResumable( + ref(getStorage(), `${PATH}/putStringArrayBuffer.json`), + arrayBuffer, + { + contentType: 'application/json', + }, + ); + + uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); + uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); + uploadTaskSnapshot.metadata.should.be.an.Object(); + }); + + it('uploads an Uint8Array', async function () { + const { getStorage, ref, uploadBytesResumable } = storageModular; + const jsonDerulo = JSON.stringify({ foo: 'bar' }); + + const arrayBuffer = new jet.context.window.ArrayBuffer(jsonDerulo.length); + const unit8Array = new jet.context.window.Uint8Array(arrayBuffer); + + for (let i = 0, strLen = jsonDerulo.length; i < strLen; i++) { + unit8Array[i] = jsonDerulo.charCodeAt(i); + } + + const uploadTaskSnapshot = await uploadBytesResumable( + ref(getStorage(), `${PATH}/putStringUint8Array.json`), + unit8Array, + { + contentType: 'application/json', + }, + ); + + uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); + uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); + uploadTaskSnapshot.metadata.should.be.an.Object(); + }); + + it('should have access to the snapshot values outside of the Task thennable', async function () { + const { getStorage, ref, uploadBytesResumable } = storageModular; + const jsonDerulo = JSON.stringify({ foo: 'bar' }); + + const bob = new jet.context.Blob([jsonDerulo], { + type: 'application/json', + }); + + const uploadTaskSnapshot = uploadBytesResumable( + ref(getStorage(), `${PATH}/putStringBlob.json`), + bob, + ); + await uploadTaskSnapshot; + + const snapshot = uploadTaskSnapshot.snapshot; + + snapshotProperties(snapshot); + }); + }); + + describe('upload tasks', function () { + // before(async function () { + // // TODO we need some semi-large assets to upload and download I think? + // await firebase + // .storage() + // .ref('/ok.jpeg') + // .writeToFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/ok.jpeg`); + // await firebase + // .storage() + // .ref('/cat.gif') + // .writeToFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/cat.gif`); + // await firebase + // .storage() + // .ref('/hei.heic') + // .writeToFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/hei.heic`); + // }); + + // TODO followup - works against live storage but emulator inverts permission / not found errors + xit('errors if permission denied', async function () { + const { getStorage, ref, putFile } = storageModular; + try { + await putFile( + ref(getStorage(), '/uploadNope.json'), + `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/ok.jpeg`, + ); + + return Promise.reject(new Error('No permission denied error')); + } catch (error) { + error.code.should.equal('storage/unauthorized'); + error.message.includes('not authorized').should.be.true(); + return Promise.resolve(); + } + }); + + // TODO followup - works against live storage but emulator inverts permission / not found errors + xit('supports thenable .catch()', async function () { + const { getStorage, ref, putFile } = storageModular; + + await putFile( + ref(getStorage(), '/uploadNope.json'), + `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/ok.jpeg`, + ).catch(error => { + error.code.should.equal('storage/unauthorized'); + error.message.includes('not authorized').should.be.true(); + return 1; + }); + should.equal(out, 1); + }); + + // TODO we don't have files seeded on the device, but could do so from test helpers + xit('uploads files with contentType detection', async function () { + const { getStorage, ref, putFile } = storageModular; + + let uploadTaskSnapshot = await putFile( + ref(getStorage(), `${PATH}/uploadOk.jpeg`), + `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/ok.jpeg`, + ); + + uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); + uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); + uploadTaskSnapshot.metadata.should.be.an.Object(); + uploadTaskSnapshot.metadata.contentType.should.equal('image/jpeg'); + + uploadTaskSnapshot = await putFile( + ref(getStorage(), '/uploadCat.gif'), + `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}%2Fcat.gif`, + ); + + uploadTaskSnapshot.metadata.should.be.an.Object(); + uploadTaskSnapshot.metadata.contentType.should.equal('image/gif'); + + if (device.getPlatform() === 'ios') { + uploadTaskSnapshot = await putFile( + ref(getStorage(), '/uploadHei.heic'), + `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/hei.heic`, + ); + + uploadTaskSnapshot.metadata.should.be.an.Object(); + uploadTaskSnapshot.metadata.contentType.should.equal('image/heic'); + } + }); + + it('uploads a file without read permission', async function () { + const { getStorage, ref, uploadString } = storageModular; + const uploadTaskSnapshot = await uploadString( + ref(getStorage(), WRITE_ONLY_NAME), + 'Just a string to put in a file for upload', + ); + + uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); + uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); + uploadTaskSnapshot.metadata.should.be.an.Object(); + }); + + it('should have access to the snapshot values outside of the Task thennable', async function () { + const { getStorage, ref, uploadString } = storageModular; + + const uploadTaskSnapshot = uploadString( + ref(getStorage(), `${PATH}/putStringBlob.json`), + 'Just a string to put in a file for upload', + ); + + await uploadTaskSnapshot; + const snapshot = uploadTaskSnapshot.snapshot; + snapshotProperties(snapshot); + }); + + it('should have access to the snapshot values outside of the event subscriber', async function () { + const { getStorage, ref, uploadString } = storageModular; + + const uploadTaskSnapshot = uploadString( + ref(getStorage(), `${PATH}/putStringBlob.json`), + 'Just a string to put in a file for upload', + ); + + const { resolve, promise } = Promise.defer(); + uploadTaskSnapshot.on('state_changed', { + next: snapshot => { + if (snapshot.state === firebase.storage.TaskState.SUCCESS) { + snapshotProperties(snapshot); + resolve(); + } + }, + }); + await promise; + }); + }); + + describe('on()', function () { + before(async function () { + const { getStorage, ref, uploadString } = storageModular; + + await uploadString( + ref(getStorage(), `${PATH}/ok.json`), + 'Just a string to put in a file for upload', + ); + }); + + it('throws an Error if event is invalid', async function () { + const { getStorage, ref, putFile } = storageModular; + + const storageReference = ref(getStorage(), `${PATH}/ok.jpeg`); + try { + const task = putFile(storageReference, 'abc'); + + task.on('foo'); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql( + "event argument must be a string with a value of 'state_changed'", + ); + return Promise.resolve(); + } + }); + + it('throws an Error if nextOrObserver is invalid', async function () { + const { getStorage, ref, putFile } = storageModular; + const storageReference = ref(getStorage(), `${PATH}/ok.jpeg`); + try { + const task = putFile(storageReference, 'abc'); + task.on('state_changed', 'not a fn'); + return Promise.reject(new Error('Did not error!')); + } catch (error) { + error.message.should.containEql("'nextOrObserver' must be a Function, an Object or Null"); + return Promise.resolve(); + } + }); + + it('observer calls error callback', async function () { + const { getStorage, ref, putFile } = storageModular; + const storageRef = ref(getStorage(), `${PATH}/uploadOk.jpeg`); + const { resolve, promise } = Promise.defer(); + const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/notFoundFooFile.bar`; + const task = putFile(storageRef, path); + + task.on('state_changed', { + error: error => { + error.code.should.containEql('storage/file-not-found'); + resolve(); + }, + }); + + try { + await task; + } catch (error) { + error.code.should.containEql('storage/file-not-found'); + } + + await promise; + }); + + it('observer: calls next callback', async function () { + const { getStorage, ref, writeToFile } = storageModular; + + const storageRef = ref(getStorage(), `${PATH}/ok.jpeg`); + const { resolve, promise } = Promise.defer(); + const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; + const task = writeToFile(storageRef, path); + task.on('state_changed', { + next: snapshot => { + if (snapshot.state === firebase.storage.TaskState.SUCCESS) { + resolve(); + } + }, + }); + await task; + await promise; + }); + + it('observer: calls completion callback', async function () { + const { getStorage, ref, writeToFile } = storageModular; + const storageRef = ref(getStorage(), `${PATH}/ok.jpeg`); + + const { resolve, promise } = Promise.defer(); + const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; + const task = writeToFile(storageRef, path); + + task.on('state_changed', { + complete: snapshot => { + snapshot.state.should.equal(firebase.storage.TaskState.SUCCESS); + resolve(); + }, + }); + + await task; + await promise; + }); + + it('calls error callback', async function () { + const { getStorage, ref, putFile } = storageModular; + const storageRef = ref(getStorage(), `${PATH}/uploadOk.jpeg`); + const { resolve, promise } = Promise.defer(); + const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/notFoundFooFile.bar`; + const task = putFile(storageRef, path); + + task.on( + 'state_changed', + null, + error => { + error.code.should.containEql('storage/file-not-found'); + resolve(); + }, + null, + ); + + try { + await task; + } catch (error) { + error.code.should.containEql('storage/file-not-found'); + } + + await promise; + }); + + it('calls next callback', async function () { + const { getStorage, ref, writeToFile } = storageModular; + const storageRef = ref(getStorage(), `${PATH}/ok.jpeg`); + + const { resolve, promise } = Promise.defer(); + const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; + const task = writeToFile(storageRef, path); + + task.on('state_changed', snapshot => { + if (snapshot.state === firebase.storage.TaskState.SUCCESS) { + resolve(); + } + }); + + await task; + await promise; + }); + + it('calls completion callback', async function () { + const { getStorage, ref, writeToFile } = storageModular; + const storageRef = ref(getStorage(), `${PATH}/ok.jpeg`); + const { resolve, promise } = Promise.defer(); + const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; + const task = writeToFile(storageRef, path); + + task.on('state_changed', null, null, snapshot => { + snapshot.state.should.equal(firebase.storage.TaskState.SUCCESS); + resolve(); + }); + + await task; + await promise; + }); + + it('returns a subscribe fn', async function () { + const { getStorage, ref, writeToFile } = storageModular; + const storageRef = ref(getStorage(), `${PATH}/ok.jpeg`); + const { resolve, promise } = Promise.defer(); + const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; + const task = writeToFile(storageRef, path); + + const subscribe = task.on('state_changed'); + + subscribe(null, null, snapshot => { + if (snapshot.state === firebase.storage.TaskState.SUCCESS) { + resolve(); + } + }); + + await task; + await promise; + }); + + it('returns a subscribe fn supporting observer usage syntax', async function () { + const { getStorage, ref, writeToFile } = storageModular; + const storageRef = ref(getStorage(), `${PATH}/ok.jpeg`); + const { resolve, promise } = Promise.defer(); + const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; + const task = writeToFile(storageRef, path); + + const subscribe = task.on('state_changed'); + + subscribe({ + complete: snapshot => { + if (snapshot.state === firebase.storage.TaskState.SUCCESS) { + resolve(); + } + }, + }); + + await task; + await promise; + }); + + it('listens to download state', async function () { + const { getStorage, ref, writeToFile } = storageModular; + const storageRef = ref(getStorage(), `${PATH}/ok.jpeg`); + const { resolve, reject, promise } = Promise.defer(); + const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.gif`; + + const unsubscribe = writeToFile(storageRef, path).on( + 'state_changed', + snapshot => { + if (snapshot.state === firebase.storage.TaskState.SUCCESS) { + resolve(); + } + }, + error => { + unsubscribe(); + reject(error); + }, + ); + + await promise; + }); + + it('listens to upload state', async function () { + const { getStorage, ref, putFile } = storageModular; + const storageRef = ref(getStorage(), `${PATH}/ok.jpeg`); + const { resolve, reject, promise } = Promise.defer(); + const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.gif`; + + const unsubscribe = putFile(storageRef, path).on( + 'state_changed', + snapshot => { + if (snapshot.state === firebase.storage.TaskState.SUCCESS) { + resolve(); + } + }, + error => { + unsubscribe(); + reject(error); + }, + ); + + await promise; + }); + }); + + // TODO get files staged for emulator testing + xdescribe('pause() resume()', function () { + it('successfully pauses and resumes an upload', async function testRunner() { + this.timeout(100 * 1000); + const { getStorage, ref, writeToFile, putFile } = storageModular; + const storageRef = ref( + getStorage(), + device.getPlatform() === 'ios' ? '/smallFileTest.png' : '/cat.gif', + ); + + await writeToFile( + storageRef, + `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/pauseUpload_test1.gif`, + ); + + const ref2 = ref(getStorage(), '/uploadCat.gif'); + + const { resolve, reject, promise } = Promise.defer(); + const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/pauseUpload_test1.gif`; + const uploadTask = putFile(ref2, path); + + let hadRunningStatus = false; + let hadPausedStatus = false; + let hadResumedStatus = false; + + uploadTask.on( + 'state_changed', + snapshot => { + // 1) pause when we receive first running event + if (snapshot.state === firebase.storage.TaskState.RUNNING && !hadRunningStatus) { + hadRunningStatus = true; + if (device.getPlatform() === 'android') { + uploadTask.pause(); + } else { + // TODO (salakar) submit bug report to Firebase iOS SDK repo (pausing immediately after the first progress event will fail the upload with an unknown error) + setTimeout(() => { + uploadTask.pause(); + }, 750); + } + } + + // 2) resume when we receive first paused event + if (snapshot.state === firebase.storage.TaskState.PAUSED) { + hadPausedStatus = true; + uploadTask.resume(); + } + + // 3) track that we resumed on 2nd running status whilst paused + if ( + snapshot.state === firebase.storage.TaskState.RUNNING && + hadRunningStatus && + hadPausedStatus && + !hadResumedStatus + ) { + hadResumedStatus = true; + } + + // 4) finally confirm we received all statuses + if (snapshot.state === firebase.storage.TaskState.SUCCESS) { + should.equal(hadRunningStatus, true); + should.equal(hadPausedStatus, true); + should.equal(hadResumedStatus, true); + resolve(); + } + }, + error => { + reject(error); + }, + ); + + await promise; + }); + + it('successfully pauses and resumes a download', async function () { + const { getStorage, ref, writeToFile } = storageModular; + const storageRef = ref( + getStorage(), + device.getPlatform() === 'ios' ? '/1mbTestFile.gif' : '/cat.gif', + ); + + const { resolve, reject, promise } = Promise.defer(); + + // random file name as Android does not allow overriding if file already exists + const path = `${ + firebase.utils.FilePath.DOCUMENT_DIRECTORY + }/invertase/pauseDownload${Math.round(Math.random() * 1000)}.gif`; + const downloadTask = writeToFile(storageRef, path); + + let hadRunningStatus = false; + let hadPausedStatus = false; + let hadResumedStatus = false; + + downloadTask.on( + 'state_changed', + snapshot => { + // TODO(salakar) validate snapshot props + // 1) pause when we receive first running event + if (snapshot.state === firebase.storage.TaskState.RUNNING && !hadRunningStatus) { + hadRunningStatus = true; + downloadTask.pause(); + } + + // 2) resume when we receive first paused event + if (snapshot.state === firebase.storage.TaskState.PAUSED) { + hadPausedStatus = true; + downloadTask.resume(); + } + + // 3) track that we resumed on 2nd running status whilst paused + if ( + snapshot.state === firebase.storage.TaskState.RUNNING && + hadRunningStatus && + hadPausedStatus && + !hadResumedStatus + ) { + hadResumedStatus = true; + } + + // 4) finally confirm we received all statuses + if (snapshot.state === firebase.storage.TaskState.SUCCESS) { + should.equal(hadRunningStatus, true); + should.equal(hadPausedStatus, true); + should.equal(hadResumedStatus, true); + resolve(); + } + }, + error => { + reject(error); + }, + ); + + await promise; + }); + }); + + describe('cancel()', function () { + // TODO stage a file big enough to test upload cancel + xit('successfully cancels an upload', async function () { + const { getStorage, ref, writeToFile, putFile } = storageModular; + const storageRef = ref( + getStorage(), + device.getPlatform() === 'ios' ? '/1mbTestFile.gif' : '/cat.gif', + ); + await writeToFile( + storageRef, + `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/pauseUpload.gif`, + ); + + const ref2 = ref(getStorage(), '/successful-cancel.jpg'); + + //Upload and cancel + const { resolve, reject, promise } = Promise.defer(); + const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/pauseUpload.gif`; + const uploadTask = putFile(ref2, path); + + let hadRunningStatus = false; + let hadCancelledStatus = false; + + uploadTask.on( + 'state_changed', + snapshot => { + // TODO(salakar) validate snapshot props + // 1) cancel it when we receive first running event + if (snapshot.state === firebase.storage.TaskState.RUNNING && !hadRunningStatus) { + hadRunningStatus = true; + uploadTask.cancel(); + } + + // 2) confirm cancellation + if (snapshot.state === firebase.storage.TaskState.CANCELLED) { + should.equal(hadRunningStatus, true); + hadCancelledStatus = true; + } + + if (snapshot.state === firebase.storage.TaskState.ERROR) { + throw new Error('Should not error if cancelled?'); + } + + if (snapshot.state === firebase.storage.TaskState.SUCCESS) { + reject(new Error('UploadTask did not cancel!')); + } + }, + error => { + should.equal(hadRunningStatus, true); + should.equal(hadCancelledStatus, true); + error.code.should.equal('storage/cancelled'); + error.message.should.containEql('User cancelled the operation.'); + resolve(); + }, + ); + + await promise; + }); + }); + + // TODO stage a file big enough to cancel a download + xit('successfully cancels a download', async function () { + const { getStorage, ref, writeToFile } = storageModular; + const storageRef = ref(getStorage(), '/1mbTestFile.gif'); + await Utils.sleep(10000); + const { resolve, reject, promise } = Promise.defer(); + const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/testUploadFile.jpg`; + const downloadTask = writeToFile(storageRef, path); + + let hadRunningStatus = false; + let hadCancelledStatus = false; + + downloadTask.on( + 'state_changed', + snapshot => { + // TODO(salakar) validate snapshot props + // 1) cancel it when we receive first running event + if (snapshot.state === firebase.storage.TaskState.RUNNING && !hadRunningStatus) { + hadRunningStatus = true; + downloadTask.cancel(); + } + + // 2) confirm cancellation + if (snapshot.state === firebase.storage.TaskState.CANCELLED) { + should.equal(hadRunningStatus, true); + hadCancelledStatus = true; + } + + if (snapshot.state === firebase.storage.TaskState.ERROR) { + throw new Error('Should not error if cancelled?'); + } + + if (snapshot.state === firebase.storage.TaskState.SUCCESS) { + reject(new Error('DownloadTask did not cancel!')); + } + }, + error => { + should.equal(hadRunningStatus, true); + should.equal(hadCancelledStatus, true); + error.code.should.equal('storage/cancelled'); + error.message.should.containEql('User cancelled the operation.'); + resolve(); + }, + ); + + await promise; + }); + }); +}); diff --git a/packages/storage/e2e/storage.e2e.js b/packages/storage/e2e/storage.e2e.js deleted file mode 100644 index 6f8afe630a..0000000000 --- a/packages/storage/e2e/storage.e2e.js +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -const { PATH } = require('./helpers'); - -describe('storage()', function () { - describe('namespace', function () { - it('accessible from firebase.app()', function () { - const app = firebase.app(); - should.exist(app.storage); - app.storage().app.should.equal(app); - }); - - it('supports multiple apps', async function () { - firebase.storage().app.name.should.equal('[DEFAULT]'); - - firebase - .storage(firebase.app('secondaryFromNative')) - .app.name.should.equal('secondaryFromNative'); - - firebase.app('secondaryFromNative').storage().app.name.should.equal('secondaryFromNative'); - }); - - it('supports specifying a bucket', async function () { - const bucket = 'gs://react-native-firebase-testing'; - const defaultInstance = firebase.storage(); - defaultInstance.app.name.should.equal('[DEFAULT]'); - should.equal( - defaultInstance._customUrlOrRegion, - 'gs://react-native-firebase-testing.appspot.com', - ); - firebase.storage().app.name.should.equal('[DEFAULT]'); - const instanceForBucket = firebase.app().storage(bucket); - instanceForBucket._customUrlOrRegion.should.equal(bucket); - }); - - it('throws an error if an invalid bucket url is provided', async function () { - const bucket = 'invalid://react-native-firebase-testing'; - try { - firebase.app().storage(bucket); - return Promise.reject(new Error('Did not throw.')); - } catch (error) { - error.message.should.containEql("bucket url must be a string and begin with 'gs://'"); - return Promise.resolve(); - } - }); - - // FIXME on android this is unathorized against emulator but works on iOS? - xit('uploads to a custom bucket when specified', async function () { - if (device.getPlatform() === 'ios') { - const jsonDerulo = JSON.stringify({ foo: 'bar' }); - const bucket = 'gs://react-native-firebase-testing'; - - const uploadTaskSnapshot = await firebase - .app() - .storage(bucket) - .ref(`${PATH}/putStringCustomBucket.json`) - .putString(jsonDerulo, firebase.storage.StringFormat.RAW, { - contentType: 'application/json', - }); - - uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); - uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); - uploadTaskSnapshot.metadata.should.be.an.Object(); - } else { - this.skip(); - } - }); - }); - - describe('ref', function () { - it('uses default path if none provided', async function () { - const ref = firebase.storage().ref(); - ref.fullPath.should.equal('/'); - }); - - it('accepts a custom path', async function () { - const ref = firebase.storage().ref('foo/bar/baz.png'); - ref.fullPath.should.equal('foo/bar/baz.png'); - }); - - it('strips leading / from custom path', async function () { - const ref = firebase.storage().ref('/foo/bar/baz.png'); - ref.fullPath.should.equal('foo/bar/baz.png'); - }); - - it('throws an error if custom path not a string', async function () { - try { - firebase.storage().ref({ derp: true }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'path' must be a string value"); - return Promise.resolve(); - } - }); - }); - - describe('refFromURL', function () { - it('accepts a gs url', async function () { - const url = 'gs://foo/bar/baz.png'; - const ref = firebase.storage().refFromURL(url); - ref.toString().should.equal(url); - }); - - it('accepts a https url', async function () { - const url = - 'https://firebasestorage.googleapis.com/v0/b/react-native-firebase-testing.appspot.com/o/1mbTestFile.gif?alt=media'; - const ref = firebase.storage().refFromURL(url); - ref.bucket.should.equal('react-native-firebase-testing.appspot.com'); - ref.name.should.equal('1mbTestFile.gif'); - ref.toString().should.equal('gs://react-native-firebase-testing.appspot.com/1mbTestFile.gif'); - }); - - it('accepts a https encoded url', async function () { - const url = - 'https%3A%2F%2Ffirebasestorage.googleapis.com%2Fv0%2Fb%2Freact-native-firebase-testing.appspot.com%2Fo%2F1mbTestFile.gif%3Falt%3Dmedia'; - const ref = firebase.storage().refFromURL(url); - ref.bucket.should.equal('react-native-firebase-testing.appspot.com'); - ref.name.should.equal('1mbTestFile.gif'); - ref.toString().should.equal('gs://react-native-firebase-testing.appspot.com/1mbTestFile.gif'); - }); - - it('throws an error if https url could not be parsed', async function () { - try { - firebase.storage().refFromURL('https://invertase.io'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("unable to parse 'url'"); - return Promise.resolve(); - } - }); - - it('accepts a gs url without a fullPath', async function () { - const url = 'gs://some-bucket'; - const ref = firebase.storage().refFromURL(url); - ref.toString().should.equal(url); - }); - - it('throws an error if url is not a string', async function () { - try { - firebase.storage().refFromURL({ derp: true }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'url' must be a string value"); - return Promise.resolve(); - } - }); - - it('throws an error if url does not start with gs:// or https://', async function () { - try { - firebase.storage().refFromURL('bs://foo/bar/cat.gif'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("begin with 'gs://'"); - return Promise.resolve(); - } - }); - }); - - describe('setMaxOperationRetryTime', function () { - it('should set', async function () { - firebase.storage().maxOperationRetryTime.should.equal(120000); - await firebase.storage().setMaxOperationRetryTime(100000); - firebase.storage().maxOperationRetryTime.should.equal(100000); - }); - - it('throws if time is not a number value', async function () { - try { - await firebase.storage().setMaxOperationRetryTime('im a teapot'); - return Promise.reject(new Error('Did not throw')); - } catch (error) { - error.message.should.containEql("'time' must be a number value"); - return Promise.resolve(); - } - }); - }); - - describe('setMaxUploadRetryTime', function () { - it('should set', async function () { - firebase.storage().maxUploadRetryTime.should.equal(600000); - await firebase.storage().setMaxUploadRetryTime(120000); - firebase.storage().maxUploadRetryTime.should.equal(120000); - }); - - it('throws if time is not a number value', async function () { - try { - await firebase.storage().setMaxUploadRetryTime('im a teapot'); - return Promise.reject(new Error('Did not throw')); - } catch (error) { - error.message.should.containEql("'time' must be a number value"); - return Promise.resolve(); - } - }); - }); - - describe('setMaxDownloadRetryTime', function () { - it('should set', async function () { - firebase.storage().maxDownloadRetryTime.should.equal(600000); - await firebase.storage().setMaxDownloadRetryTime(120000); - firebase.storage().maxDownloadRetryTime.should.equal(120000); - }); - - it('throws if time is not a number value', async function () { - try { - await firebase.storage().setMaxDownloadRetryTime('im a teapot'); - return Promise.reject(new Error('Did not throw')); - } catch (error) { - error.message.should.containEql("'time' must be a number value"); - return Promise.resolve(); - } - }); - }); -}); diff --git a/packages/storage/e2e/storage.modular.e2e.js b/packages/storage/e2e/storage.modular.e2e.js new file mode 100644 index 0000000000..e7a18e2554 --- /dev/null +++ b/packages/storage/e2e/storage.modular.e2e.js @@ -0,0 +1,489 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +const { PATH } = require('./helpers'); + +describe('storage() modular', function () { + describe('firebase v8 compatibility', function () { + describe('storage()', function () { + describe('namespace', function () { + it('accessible from firebase.app()', function () { + const app = firebase.app(); + should.exist(app.storage); + app.storage().app.should.equal(app); + }); + + it('supports multiple apps', async function () { + firebase.storage().app.name.should.equal('[DEFAULT]'); + + firebase + .storage(firebase.app('secondaryFromNative')) + .app.name.should.equal('secondaryFromNative'); + + firebase + .app('secondaryFromNative') + .storage() + .app.name.should.equal('secondaryFromNative'); + }); + + it('supports specifying a bucket', async function () { + const bucket = 'gs://react-native-firebase-testing'; + const defaultInstance = firebase.storage(); + defaultInstance.app.name.should.equal('[DEFAULT]'); + should.equal( + defaultInstance._customUrlOrRegion, + 'gs://react-native-firebase-testing.appspot.com', + ); + firebase.storage().app.name.should.equal('[DEFAULT]'); + const instanceForBucket = firebase.app().storage(bucket); + instanceForBucket._customUrlOrRegion.should.equal(bucket); + }); + + it('throws an error if an invalid bucket url is provided', async function () { + const bucket = 'invalid://react-native-firebase-testing'; + try { + firebase.app().storage(bucket); + return Promise.reject(new Error('Did not throw.')); + } catch (error) { + error.message.should.containEql("bucket url must be a string and begin with 'gs://'"); + return Promise.resolve(); + } + }); + + // FIXME on android this is unathorized against emulator but works on iOS? + xit('uploads to a custom bucket when specified', async function () { + if (device.getPlatform() === 'ios') { + const jsonDerulo = JSON.stringify({ foo: 'bar' }); + const bucket = 'gs://react-native-firebase-testing'; + + const uploadTaskSnapshot = await firebase + .app() + .storage(bucket) + .ref(`${PATH}/putStringCustomBucket.json`) + .putString(jsonDerulo, firebase.storage.StringFormat.RAW, { + contentType: 'application/json', + }); + + uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); + uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); + uploadTaskSnapshot.metadata.should.be.an.Object(); + } else { + this.skip(); + } + }); + }); + + describe('ref', function () { + it('uses default path if none provided', async function () { + const ref = firebase.storage().ref(); + ref.fullPath.should.equal('/'); + }); + + it('accepts a custom path', async function () { + const ref = firebase.storage().ref('foo/bar/baz.png'); + ref.fullPath.should.equal('foo/bar/baz.png'); + }); + + it('strips leading / from custom path', async function () { + const ref = firebase.storage().ref('/foo/bar/baz.png'); + ref.fullPath.should.equal('foo/bar/baz.png'); + }); + + it('throws an error if custom path not a string', async function () { + try { + firebase.storage().ref({ derp: true }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'path' must be a string value"); + return Promise.resolve(); + } + }); + }); + + describe('refFromURL', function () { + it('accepts a gs url', async function () { + const url = 'gs://foo/bar/baz.png'; + const ref = firebase.storage().refFromURL(url); + ref.toString().should.equal(url); + }); + + it('accepts a https url', async function () { + const url = + 'https://firebasestorage.googleapis.com/v0/b/react-native-firebase-testing.appspot.com/o/1mbTestFile.gif?alt=media'; + const ref = firebase.storage().refFromURL(url); + ref.bucket.should.equal('react-native-firebase-testing.appspot.com'); + ref.name.should.equal('1mbTestFile.gif'); + ref + .toString() + .should.equal('gs://react-native-firebase-testing.appspot.com/1mbTestFile.gif'); + }); + + it('accepts a https encoded url', async function () { + const url = + 'https%3A%2F%2Ffirebasestorage.googleapis.com%2Fv0%2Fb%2Freact-native-firebase-testing.appspot.com%2Fo%2F1mbTestFile.gif%3Falt%3Dmedia'; + const ref = firebase.storage().refFromURL(url); + ref.bucket.should.equal('react-native-firebase-testing.appspot.com'); + ref.name.should.equal('1mbTestFile.gif'); + ref + .toString() + .should.equal('gs://react-native-firebase-testing.appspot.com/1mbTestFile.gif'); + }); + + it('throws an error if https url could not be parsed', async function () { + try { + firebase.storage().refFromURL('https://invertase.io'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("unable to parse 'url'"); + return Promise.resolve(); + } + }); + + it('accepts a gs url without a fullPath', async function () { + const url = 'gs://some-bucket'; + const ref = firebase.storage().refFromURL(url); + ref.toString().should.equal(url); + }); + + it('throws an error if url is not a string', async function () { + try { + firebase.storage().refFromURL({ derp: true }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'url' must be a string value"); + return Promise.resolve(); + } + }); + + it('throws an error if url does not start with gs:// or https://', async function () { + try { + firebase.storage().refFromURL('bs://foo/bar/cat.gif'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("begin with 'gs://'"); + return Promise.resolve(); + } + }); + }); + + describe('setMaxOperationRetryTime', function () { + it('should set', async function () { + await firebase.storage().setMaxOperationRetryTime(120000); + firebase.storage().maxOperationRetryTime.should.equal(120000); + await firebase.storage().setMaxOperationRetryTime(100000); + firebase.storage().maxOperationRetryTime.should.equal(100000); + }); + + it('throws if time is not a number value', async function () { + try { + await firebase.storage().setMaxOperationRetryTime('im a teapot'); + return Promise.reject(new Error('Did not throw')); + } catch (error) { + error.message.should.containEql("'time' must be a number value"); + return Promise.resolve(); + } + }); + }); + + describe('setMaxUploadRetryTime', function () { + it('should set', async function () { + await firebase.storage().setMaxUploadRetryTime(600000); + firebase.storage().maxUploadRetryTime.should.equal(600000); + await firebase.storage().setMaxUploadRetryTime(120000); + firebase.storage().maxUploadRetryTime.should.equal(120000); + }); + + it('throws if time is not a number value', async function () { + try { + await firebase.storage().setMaxUploadRetryTime('im a teapot'); + return Promise.reject(new Error('Did not throw')); + } catch (error) { + error.message.should.containEql("'time' must be a number value"); + return Promise.resolve(); + } + }); + }); + + describe('setMaxDownloadRetryTime', function () { + it('should set', async function () { + await firebase.storage().setMaxDownloadRetryTime(600000); + firebase.storage().maxDownloadRetryTime.should.equal(600000); + await firebase.storage().setMaxDownloadRetryTime(120000); + firebase.storage().maxDownloadRetryTime.should.equal(120000); + }); + + it('throws if time is not a number value', async function () { + try { + await firebase.storage().setMaxDownloadRetryTime('im a teapot'); + return Promise.reject(new Error('Did not throw')); + } catch (error) { + error.message.should.containEql("'time' must be a number value"); + return Promise.resolve(); + } + }); + }); + }); + }); + describe('modular', function () { + describe('getStorage', function () { + it('pass app as argument', function () { + const { getStorage } = storageModular; + + const storage = getStorage(firebase.app()); + + storage.constructor.name.should.be.equal('FirebaseStorageModule'); + }); + + it('no app as argument', function () { + const { getStorage } = storageModular; + + const storage = getStorage(); + + storage.constructor.name.should.be.equal('FirebaseStorageModule'); + }); + }); + + it('supports specifying a bucket', async function () { + const { getStorage } = storageModular; + const bucket = 'gs://react-native-firebase-testing'; + const defaultInstance = getStorage(); + defaultInstance.app.name.should.equal('[DEFAULT]'); + should.equal( + defaultInstance._customUrlOrRegion, + 'gs://react-native-firebase-testing.appspot.com', + ); + getStorage().app.name.should.equal('[DEFAULT]'); + const instanceForBucket = getStorage(null, bucket); + instanceForBucket._customUrlOrRegion.should.equal(bucket); + }); + + it('throws an error if an invalid bucket url is provided', async function () { + const { getStorage } = storageModular; + const bucket = 'invalid://react-native-firebase-testing'; + try { + getStorage(null, bucket); + return Promise.reject(new Error('Did not throw.')); + } catch (error) { + error.message.should.containEql("bucket url must be a string and begin with 'gs://'"); + return Promise.resolve(); + } + }); + + // FIXME on android this is unathorized against emulator but works on iOS? + xit('uploads to a custom bucket when specified', async function () { + if (device.getPlatform() === 'ios') { + const { getStorage, ref, uploadString } = storageModular; + const jsonDerulo = JSON.stringify({ foo: 'bar' }); + const bucket = 'gs://react-native-firebase-testing'; + const storage = getStorage(null, bucket); + const storageReference = ref(storage, `${PATH}/putStringCustomBucket.json`); + const uploadTaskSnapshot = await uploadString( + storageReference, + jsonDerulo, + firebase.storage.StringFormat.RAW, + { + contentType: 'application/json', + }, + ); + + uploadTaskSnapshot.state.should.eql(storage.TaskState.SUCCESS); + uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); + uploadTaskSnapshot.metadata.should.be.an.Object(); + } else { + this.skip(); + } + }); + }); + + describe('ref', function () { + it('uses default path if none provided', async function () { + const { getStorage, ref } = storageModular; + const storageRef = ref(getStorage()); + storageRef.fullPath.should.equal('/'); + }); + + it('accepts a custom path', async function () { + const { getStorage, ref } = storageModular; + const storageRef = ref(getStorage(), 'foo/bar/baz.png'); + + storageRef.fullPath.should.equal('foo/bar/baz.png'); + }); + + it('strips leading / from custom path', async function () { + const { getStorage, ref } = storageModular; + const storageRef = ref(getStorage(), '/foo/bar/baz.png'); + storageRef.fullPath.should.equal('foo/bar/baz.png'); + }); + + it('throws an error if custom path not a string', async function () { + try { + const { getStorage, ref } = storageModular; + ref(getStorage(), { derp: true }); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'path' must be a string value"); + return Promise.resolve(); + } + }); + }); + + describe('refFromURL', function () { + it('accepts a gs url', async function () { + const { getStorage, refFromURL, toString } = storageModular; + + const url = 'gs://foo/bar/baz.png'; + const ref = refFromURL(getStorage(), url); + + toString(ref).should.equal(url); + }); + + it('accepts a https url', async function () { + const { getStorage, refFromURL } = storageModular; + + const url = + 'https://firebasestorage.googleapis.com/v0/b/react-native-firebase-testing.appspot.com/o/1mbTestFile.gif?alt=media'; + const ref = refFromURL(getStorage(), url); + ref.bucket.should.equal('react-native-firebase-testing.appspot.com'); + ref.name.should.equal('1mbTestFile.gif'); + ref.toString().should.equal('gs://react-native-firebase-testing.appspot.com/1mbTestFile.gif'); + }); + + it('accepts a https encoded url', async function () { + const { getStorage, refFromURL } = storageModular; + + const url = + 'https%3A%2F%2Ffirebasestorage.googleapis.com%2Fv0%2Fb%2Freact-native-firebase-testing.appspot.com%2Fo%2F1mbTestFile.gif%3Falt%3Dmedia'; + const ref = refFromURL(getStorage(), url); + ref.bucket.should.equal('react-native-firebase-testing.appspot.com'); + ref.name.should.equal('1mbTestFile.gif'); + ref.toString().should.equal('gs://react-native-firebase-testing.appspot.com/1mbTestFile.gif'); + }); + + it('throws an error if https url could not be parsed', async function () { + try { + const { getStorage, refFromURL } = storageModular; + + refFromURL(getStorage(), 'https://invertase.io'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("unable to parse 'url'"); + return Promise.resolve(); + } + }); + + it('accepts a gs url without a fullPath', async function () { + const { getStorage, refFromURL } = storageModular; + + const url = 'gs://some-bucket'; + const ref = refFromURL(getStorage(), url); + ref.toString().should.equal(url); + }); + + it('throws an error if url is not a string', async function () { + try { + const { getStorage, refFromURL } = storageModular; + + refFromURL(getStorage(), { derp: true }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'url' must be a string value"); + return Promise.resolve(); + } + }); + + it('throws an error if url does not start with gs:// or https://', async function () { + try { + const { getStorage, refFromURL } = storageModular; + + refFromURL(getStorage(), 'bs://foo/bar/cat.gif'); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("begin with 'gs://'"); + return Promise.resolve(); + } + }); + }); + + describe('setMaxOperationRetryTime', function () { + it('should set', async function () { + const { getStorage, setMaxOperationRetryTime } = storageModular; + await setMaxOperationRetryTime(getStorage(), 120000); + getStorage().maxOperationRetryTime.should.equal(120000); + await setMaxOperationRetryTime(getStorage(), 100000); + getStorage().maxOperationRetryTime.should.equal(100000); + }); + + it('throws if time is not a number value', async function () { + try { + const { getStorage, setMaxOperationRetryTime } = storageModular; + + await setMaxOperationRetryTime(getStorage(), 'im a teapot'); + return Promise.reject(new Error('Did not throw')); + } catch (error) { + error.message.should.containEql("'time' must be a number value"); + return Promise.resolve(); + } + }); + }); + + describe('setMaxUploadRetryTime', function () { + it('should set', async function () { + const { getStorage, setMaxUploadRetryTime } = storageModular; + const storage = getStorage(); + await setMaxUploadRetryTime(storage, 600000); + storage.maxUploadRetryTime.should.equal(600000); + await setMaxUploadRetryTime(storage, 120000); + storage.maxUploadRetryTime.should.equal(120000); + }); + + it('throws if time is not a number value', async function () { + try { + const { getStorage, setMaxUploadRetryTime } = storageModular; + + await setMaxUploadRetryTime(getStorage(), 'im a teapot'); + return Promise.reject(new Error('Did not throw')); + } catch (error) { + error.message.should.containEql("'time' must be a number value"); + return Promise.resolve(); + } + }); + }); + + describe('setMaxDownloadRetryTime', function () { + it('should set', async function () { + const { getStorage, setMaxDownloadRetryTime } = storageModular; + const storage = getStorage(); + await setMaxDownloadRetryTime(storage, 600000); + storage.maxDownloadRetryTime.should.equal(600000); + await setMaxDownloadRetryTime(storage, 120000); + storage.maxDownloadRetryTime.should.equal(120000); + }); + + it('throws if time is not a number value', async function () { + try { + const { getStorage, setMaxDownloadRetryTime } = storageModular; + + await setMaxDownloadRetryTime(getStorage(), 'im a teapot'); + return Promise.reject(new Error('Did not throw')); + } catch (error) { + error.message.should.containEql("'time' must be a number value"); + return Promise.resolve(); + } + }); + }); +}); diff --git a/packages/storage/lib/index.d.ts b/packages/storage/lib/index.d.ts index da55535477..35fc9d2371 100644 --- a/packages/storage/lib/index.d.ts +++ b/packages/storage/lib/index.d.ts @@ -924,6 +924,21 @@ export namespace FirebaseStorageTypes { error?: NativeFirebaseError; } + /** + * Result returned from a non-resumable upload. + */ + export interface TaskResult { + /** + * The metadata of the tasks via a {@link storage.FullMetadata} interface. + */ + metadata: FullMetadata; + + /** + * The {@link storage.Reference} of the task. + */ + ref: Reference; + } + /** * The options `list()` accepts. */ @@ -961,6 +976,16 @@ export namespace FirebaseStorageTypes { prefixes: Reference[]; } + /** + * Storage Emulator options. Web only. + */ + export interface EmulatorMockTokenOptions { + /** + * the mock auth token to use for unit testing Security Rules. + */ + mockUserToken?: string; + } + /** * The Cloud Storage service is available for the default app, a given app or a specific storage bucket. * diff --git a/packages/storage/lib/index.js b/packages/storage/lib/index.js index 7a378ec58b..9031ca1226 100644 --- a/packages/storage/lib/index.js +++ b/packages/storage/lib/index.js @@ -26,6 +26,31 @@ import StorageStatics from './StorageStatics'; import { getGsUrlParts, getHttpUrlParts, handleStorageEvent } from './utils'; import version from './version'; +export { + getStorage, + connectStorageEmulator, + ref, + deleteObject, + getBlob, + getBytes, + getDownloadURL, + getMetadata, + getStream, + list, + listAll, + updateMetadata, + putFile, + writeToFile, + toString, + child, + setMaxDownloadRetryTime, + setMaxOperationRetryTime, + setMaxUploadRetryTime, + refFromURL, + uploadString, + uploadBytesResumable, +} from '../modular/index'; + const namespace = 'storage'; const nativeEvents = ['storage_event']; const nativeModuleName = 'RNFBStorageModule'; diff --git a/packages/storage/modular/index.js b/packages/storage/modular/index.js new file mode 100644 index 0000000000..6e46ed95d6 --- /dev/null +++ b/packages/storage/modular/index.js @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { firebase } from '..'; + +/** + * Returns a Storage instance for the given app. + * @param app - FirebaseApp. Optional. + * @param bucketUrl - Storage bucket URL. Optional. + * @returns {Storage} + */ +export function getStorage(app, bucketUrl) { + if (app) { + if (bucketUrl != null) { + return firebase.app(app.name).storage(bucketUrl); + } + + return firebase.app(app.name).storage(); + } + + if (bucketUrl != null) { + return firebase.app().storage(bucketUrl); + } + + return firebase.app().storage(); +} + +/** + * Modify this Storage instance to communicate with the Firebase Storage emulator. + * @param storage - Storage instance. + * @param host - emulator host (e.g. - 'localhost') + * @param port - emulator port (e.g. - 9199) + * @param options - `EmulatorMockTokenOptions` instance. Optional. Web only. + * @returns {void} + */ +export function connectStorageEmulator(storage, host, port, options) { + return storage.useEmulator(host, port, options); +} + +/** + * Modify this Storage instance to communicate with the Firebase Storage emulator. + * @param storage - Storage instance. + * @param path An optional string pointing to a location on the storage bucket. If no path + * is provided, the returned reference will be the bucket root path. Optional. + * @returns {Storage.Reference} + */ +export function ref(storage, path) { + return storage.ref(path); +} + +/** + * Deletes the object at this reference's location. + * @param storageRef - Storage `Reference` instance. + * @returns {Promise} + */ +export function deleteObject(storageRef) { + return storageRef.delete(); +} + +/** + * Downloads the data at the object's location. Returns an error if the object is not found. + * @param storageRef - Storage `Reference` instance. + * @returns {Promise} + */ +// eslint-disable-next-line +export function getBlob(storageRef) { + throw new Error('`getBlob()` is not implemented'); +} + +/** + * Downloads the data at the object's location. Returns an error if the object is not found. + * @param storageRef - Storage `Reference` instance. + * @param maxDownloadSizeBytes - The maximum allowed size in bytes to retrieve. Web only. + * @returns {Promise} + */ +// eslint-disable-next-line +export function getBytes(storageRef, maxDownloadSizeBytes) { + throw new Error('`getBytes()` is not implemented'); +} + +/** + * Deletes the object at this reference's location. + * @param storageRef - Storage `Reference` instance. + * @returns {Promise} + */ +export function getDownloadURL(storageRef) { + return storageRef.getDownloadURL(); +} + +/** + * Fetches metadata for the object at this location, if one exists. + * @param storageRef - Storage `Reference` instance. + * @returns {Promise} + */ +export function getMetadata(storageRef) { + return storageRef.getMetadata(); +} + +/** + * Downloads the data at the object's location. This API is only available in Nodejs. + * @param storageRef - Storage `Reference` instance. + * @param maxDownloadSizeBytes - The maximum allowed size in bytes to retrieve. Web only. + * @returns {NodeJS.ReadableStream;} + */ +// eslint-disable-next-line +export function getStream(storageRef, maxDownloadSizeBytes) { + throw new Error('`getStream()` is not implemented'); +} + +/** + * List items (files) and prefixes (folders) under this storage reference + * @param storageRef - Storage `Reference` instance. + * @param options - Storage `ListOptions` instance. The options list() accepts. + * @returns {Promise} + */ +export function list(storageRef, options) { + return storageRef.list(options); +} + +/** + * List all items (files) and prefixes (folders) under this storage reference. + * @param storageRef - Storage `Reference` instance. + * @returns {Promise} + */ +export function listAll(storageRef) { + return storageRef.listAll(); +} + +/** + * Updates the metadata for this object. + * @param storageRef - Storage `Reference` instance. + * @param metadata - A Storage `SettableMetadata` instance to update. + * @returns {Promise} + */ +export function updateMetadata(storageRef, metadata) { + return storageRef.updateMetadata(metadata); +} + +/** + * Uploads data to this object's location. The upload is not resumable. + * @param storageRef - Storage `Reference` instance. + * @param data - The data (Blob | Uint8Array | ArrayBuffer) to upload to the storage bucket at the reference location. + * @param metadata - A Storage `SettableMetadata` instance to update. Optional. + * @returns {Promise} + */ +// eslint-disable-next-line +export async function uploadBytes(storageRef, data, metadata) { + throw new Error('`uploadBytes()` is not implemented'); +} + +/** + * Uploads data to this object's location. The upload is not resumable. + * @param storageRef - Storage `Reference` instance. + * @param data - The data (Blob | Uint8Array | ArrayBuffer) to upload to the storage bucket at the reference location. + * @param metadata - A Storage `SettableMetadata` instance to update. Optional. + * @returns {Task} + */ +export function uploadBytesResumable(storageRef, data, metadata) { + return storageRef.put(data, metadata); +} + +/** + * Uploads data to this object's location. The upload is not resumable. + * @param storageRef - Storage `Reference` instance. + * @param value - The string to upload. + * @param format - The format of the string to upload ('raw' | 'base64' | 'base64url' | 'data_url'). Optional. + * @param metadata - A Storage `SettableMetadata` instance to update. Optional. + * @returns {Task} + */ +export function uploadString(storageRef, data, format, metadata) { + return storageRef.putString(data, format, metadata); +} + +// Methods not on the Firebase JS SDK below + +/** + * Returns a new Storage `Reference` instance from a storage bucket URL. + * @param storage - Storage instance. + * @param url - A storage bucket URL pointing to a single file or location. Must be either a `gs://` url or an `http` url. Not available on web. + * @returns {Reference} + */ +export function refFromURL(storage, url) { + return storage.refFromURL(url); +} + +/** + * Sets the maximum time in milliseconds to retry a download if a failure occurs.. android & iOS only. + * @param storage - Storage instance. + * @param time - The new maximum operation retry time in milliseconds. + * @returns {Promise} + */ +export function setMaxOperationRetryTime(storage, time) { + return storage.setMaxOperationRetryTime(time); +} + +/** + * Sets the maximum time in milliseconds to retry an upload if a failure occurs. android & iOS only. + * @param storage - Storage instance. + * @param time - The new maximum operation retry time in milliseconds. + * @returns {Promise} + */ +export function setMaxUploadRetryTime(storage, time) { + return storage.setMaxUploadRetryTime(time); +} + +/** + * Puts a file from local disk onto the storage bucket. + * @param storageRef - Storage Reference instance. + * @param localFilePath The local file path to upload to the bucket at the reference location. + * @param metadata Any additional `SettableMetadata` for this task. + * @returns {Task} + */ +export function putFile(storageRef, filePath, metadata) { + return storageRef.putFile(filePath, metadata); +} + +/** + * Downloads a file to the specified local file path on the device. + * @param storageRef - Storage Reference instance. + * @param localFilePath The local file path to upload to on the device. + * @returns {Task} + */ +export function writeToFile(storageRef, filePath) { + return storageRef.writeToFile(filePath); +} + +/** + * Returns a gs:// URL for this object in the form `gs://///`. + * @param storageRef - Storage Reference instance. + * @returns {String} + */ +export function toString(storageRef) { + return storageRef.toString(); +} + +/** + * Returns a reference to a relative path from this reference. + * @param storageRef - Storage Reference instance. + * @param path - The relative path from this reference. Leading, trailing, and consecutive slashes are removed. + * @returns {String} + */ +export function child(storageRef, path) { + return storageRef.child(path); +} + +/** + * Sets the maximum time in milliseconds to retry a download if a failure occurs. + * @param storage - Storage instance. + * @param time - The new maximum download retry time in milliseconds. + * @returns {Promise} + */ +export function setMaxDownloadRetryTime(storage, time) { + return storage.setMaxDownloadRetryTime(time); +} diff --git a/tests/app.js b/tests/app.js index b21f489ec4..95c2bd7876 100644 --- a/tests/app.js +++ b/tests/app.js @@ -38,6 +38,7 @@ import * as perfModular from '@react-native-firebase/perf'; import '@react-native-firebase/remote-config'; import * as remoteConfigModular from '@react-native-firebase/remote-config'; import '@react-native-firebase/storage'; +import * as storageModular from '@react-native-firebase/storage'; import jet from 'jet/platform/react-native'; import React from 'react'; import { AppRegistry, Button, NativeModules, Text, View } from 'react-native'; @@ -54,6 +55,7 @@ jet.exposeContextProperty('remoteConfigModular', remoteConfigModular); jet.exposeContextProperty('perfModular', perfModular); jet.exposeContextProperty('appCheckModular', appCheckModular); jet.exposeContextProperty('messagingModular', messagingModular); +jet.exposeContextProperty('storageModular', storageModular); firebase.database().useEmulator('localhost', 9000); firebase.auth().useEmulator('http://localhost:9099'); diff --git a/tests/e2e/globals.js b/tests/e2e/globals.js index b02066082e..965f156e7d 100644 --- a/tests/e2e/globals.js +++ b/tests/e2e/globals.js @@ -107,4 +107,10 @@ Object.defineProperty(global, 'appCheckModular', { }, }); +Object.defineProperty(global, 'storageModular', { + get() { + return jet.storageModular; + }, +}); + global.isCI = !!process.env.CI;