Files

376 lines
12 KiB
TypeScript

/**
* @license
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file 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 { expect } from 'chai';
import { FirebaseApp } from '@firebase/app-types';
import { StringFormat } from '../../src/implementation/string';
import { Headers } from '../../src/implementation/connection';
import { Metadata } from '../../src/metadata';
import {
Reference,
uploadString,
uploadBytesResumable,
deleteObject,
list,
getMetadata,
updateMetadata,
getDownloadURL,
uploadBytes
} from '../../src/reference';
import { FirebaseStorageImpl, ref } from '../../src/service';
import * as testShared from './testshared';
import { newTestConnection, TestingConnection } from './connection';
import { DEFAULT_HOST } from '../../src/implementation/constants';
import { FirebaseAuthInternalName } from '@firebase/auth-interop-types';
import { Provider } from '@firebase/component';
import { AppCheckInternalComponentName } from '@firebase/app-check-interop-types';
import { fakeServerHandler, storageServiceWithHandler } from './testshared';
import { decodeUint8Array } from '../../src/platform/base64';
import { injectTestConnection } from '../../src/platform/connection';
/* eslint-disable @typescript-eslint/no-floating-promises */
function makeFakeService(
app: FirebaseApp,
authProvider: Provider<FirebaseAuthInternalName>,
appCheckProvider: Provider<AppCheckInternalComponentName>
): FirebaseStorageImpl {
return new FirebaseStorageImpl(app, authProvider, appCheckProvider);
}
function makeStorage(url: string): Reference {
const service = new FirebaseStorageImpl(
{} as FirebaseApp,
testShared.emptyAuthProvider,
testShared.fakeAppCheckTokenProvider
);
return new Reference(service, url);
}
function withFakeSend(
testFn: (text: string, headers?: Headers) => void,
resolveFn: () => void
): Reference {
function newSend(
connection: TestingConnection,
url: string,
method: string,
body?: ArrayBufferView | Blob | string | null,
headers?: Headers
): void {
let text: Promise<string>;
if (body instanceof Uint8Array) {
text = Promise.resolve(decodeUint8Array(body));
} else {
text = (body as Blob).text();
}
text.then(text => {
testFn(text, headers);
connection.abort();
injectTestConnection(null);
resolveFn();
});
}
injectTestConnection(() => newTestConnection(newSend));
const service = makeFakeService(
testShared.fakeApp,
testShared.fakeAuthProvider,
testShared.fakeAppCheckTokenProvider
);
return ref(service, 'gs://test-bucket');
}
describe('Firebase Storage > Reference', () => {
const root = makeStorage('gs://test-bucket/');
const child = makeStorage('gs://test-bucket/hello');
describe('Path constructor', () => {
it('root', () => {
expect(root.toString()).to.equal('gs://test-bucket/');
});
it('keeps characters after ? on a gs:// string', () => {
const s = makeStorage('gs://test-bucket/this/ismyobject?hello');
expect(s.toString()).to.equal('gs://test-bucket/this/ismyobject?hello');
});
it("doesn't URL-decode on a gs:// string", () => {
const s = makeStorage('gs://test-bucket/%3F');
expect(s.toString()).to.equal('gs://test-bucket/%3F');
});
it('ignores URL params and fragments on an http URL', () => {
const s = makeStorage(
`http://${DEFAULT_HOST}/v0/b/test-bucket/o/my/object.txt` +
'?ignoreme#please'
);
expect(s.toString()).to.equal('gs://test-bucket/my/object.txt');
});
it('URL-decodes and ignores fragment on an http URL', () => {
const s = makeStorage(
`http://${DEFAULT_HOST}/v0/b/test-bucket/o/%3F?ignore`
);
expect(s.toString()).to.equal('gs://test-bucket/?');
});
it('ignores URL params and fragments on an https URL', () => {
const s = makeStorage(
`https://${DEFAULT_HOST}/v0/b/test-bucket/o/my/object.txt` +
'?ignoreme#please'
);
expect(s.toString()).to.equal('gs://test-bucket/my/object.txt');
});
it('URL-decodes and ignores fragment on an https URL', () => {
const s = makeStorage(
`https://${DEFAULT_HOST}/v0/b/test-bucket/o/%3F?ignore`
);
expect(s.toString()).to.equal('gs://test-bucket/?');
});
});
describe('toString', () => {
it("Doesn't add trailing slash", () => {
const s = makeStorage('gs://test-bucket/foo');
expect(s.toString()).to.equal('gs://test-bucket/foo');
});
it('Strips trailing slash', () => {
const s = makeStorage('gs://test-bucket/foo/');
expect(s.toString()).to.equal('gs://test-bucket/foo');
});
});
describe('parentReference', () => {
it('Returns null at root', () => {
expect(root.parent).to.be.null;
});
it('Returns root one level down', () => {
expect(child.parent!.toString()).to.equal('gs://test-bucket/');
});
it('Works correctly with empty levels', () => {
const s = makeStorage('gs://test-bucket/a///');
expect(s.parent!.toString()).to.equal('gs://test-bucket/a/');
});
});
describe('root', () => {
it('Returns self at root', () => {
expect(root.root.toString()).to.equal('gs://test-bucket/');
});
it('Returns root multiple levels down', () => {
const s = makeStorage('gs://test-bucket/a/b/c/d');
expect(s.root.toString()).to.equal('gs://test-bucket/');
});
});
describe('bucket', () => {
it('Returns bucket name', () => {
expect(root.bucket).to.equal('test-bucket');
});
});
describe('fullPath', () => {
it('Returns full path without leading slash', () => {
const s = makeStorage('gs://test-bucket/full/path');
expect(s.fullPath).to.equal('full/path');
});
});
describe('name', () => {
it('Works at top level', () => {
const s = makeStorage('gs://test-bucket/toplevel.txt');
expect(s.name).to.equal('toplevel.txt');
});
it('Works at not the top level', () => {
const s = makeStorage('gs://test-bucket/not/toplevel.txt');
expect(s.name).to.equal('toplevel.txt');
});
});
describe('get child with ref()', () => {
it('works with a simple string', () => {
expect(ref(root, 'a').toString()).to.equal('gs://test-bucket/a');
});
it('drops a trailing slash', () => {
expect(ref(root, 'ab/').toString()).to.equal('gs://test-bucket/ab');
});
it('compresses repeated slashes', () => {
expect(ref(root, '//a///b/////').toString()).to.equal(
'gs://test-bucket/a/b'
);
});
it('works chained multiple times with leading slashes', () => {
expect(
ref(ref(ref(ref(root, 'a'), '/b'), 'c'), 'd/e').toString()
).to.equal('gs://test-bucket/a/b/c/d/e');
});
});
it("Doesn't send Authorization on null auth token", done => {
function newSend(
connection: TestingConnection,
url: string,
method: string,
body?: ArrayBufferView | Blob | string | null,
headers?: Headers
): void {
expect(headers).to.not.be.undefined;
expect(headers!['Authorization']).to.be.undefined;
injectTestConnection(null);
done();
}
injectTestConnection(() => newTestConnection(newSend));
const service = makeFakeService(
testShared.fakeApp,
testShared.emptyAuthProvider,
testShared.fakeAppCheckTokenProvider
);
const reference = ref(service, 'gs://test-bucket');
getMetadata(ref(reference, 'foo'));
});
it('Works if the user logs in before creating the storage reference', done => {
// Regression test for b/27227221
function newSend(
connection: TestingConnection,
url: string,
method: string,
body?: ArrayBufferView | Blob | string | null,
headers?: Headers
): void {
expect(headers).to.not.be.undefined;
expect(headers!['Authorization']).to.equal(
'Firebase ' + testShared.authToken
);
injectTestConnection(null);
done();
}
injectTestConnection(() => newTestConnection(newSend));
const service = makeFakeService(
testShared.fakeApp,
testShared.fakeAuthProvider,
testShared.fakeAppCheckTokenProvider
);
const reference = ref(service, 'gs://test-bucket');
getMetadata(ref(reference, 'foo'));
});
describe('uploadString', () => {
it('Uses metadata.contentType for RAW format', done => {
// Regression test for b/30989476
const root = withFakeSend((text: string, headers?: Headers) => {
expect(text).to.include('"contentType":"lol/wut"');
}, done);
uploadString(ref(root, 'test'), 'hello', StringFormat.RAW, {
contentType: 'lol/wut'
} as Metadata);
});
it('Uses embedded content type in DATA_URL format', done => {
const root = withFakeSend((text: string) => {
expect(text).to.include('"contentType":"lol/wat"');
}, done);
uploadString(
ref(root, 'test'),
'data:lol/wat;base64,aaaa',
StringFormat.DATA_URL
);
});
it('Lets metadata.contentType override embedded content type in DATA_URL format', done => {
const root = withFakeSend((text: string) => {
expect(text).to.include('"contentType":"tomato/soup"');
}, done);
uploadString(
ref(root, 'test'),
'data:ignore/me;base64,aaaa',
StringFormat.DATA_URL,
{ contentType: 'tomato/soup' } as Metadata
);
});
});
describe('uploadBytes', () => {
it('Uses metadata.contentType', done => {
const root = withFakeSend((text: string) => {
expect(text).to.include('"contentType":"lol/wut"');
}, done);
uploadBytes(ref(root, 'hello'), new Uint8Array(), {
contentType: 'lol/wut'
} as Metadata);
});
it('uploads without error', async () => {
const storageService = storageServiceWithHandler(fakeServerHandler({}));
const root = ref(storageService, 'gs://test-bucket/');
const childRef = ref(root, 'child');
const blob = new Uint8Array([97]);
const result = await uploadBytes(childRef, blob);
expect(result.ref).to.equal(childRef);
});
});
describe('Argument verification', () => {
describe('list', () => {
it('throws on invalid maxResults', async () => {
expect(() => list(child, { maxResults: 0 })).to.throw(
'storage/invalid-argument'
);
expect(() => list(child, { maxResults: -4 })).to.throw(
'storage/invalid-argument'
);
expect(() => list(child, { maxResults: 1001 })).to.throw(
'storage/invalid-argument'
);
});
});
});
describe('root operations', () => {
it('uploadBytesResumable throws', () => {
expect(() => uploadBytesResumable(root, new Uint8Array())).to.throw(
'storage/invalid-root-operation'
);
});
it('uploadString throws', () => {
expect(() => uploadString(root, 'raw', StringFormat.RAW)).to.throw(
'storage/invalid-root-operation'
);
});
it('uploadBytes throws', () => {
expect(() => uploadBytes(root, new Uint8Array())).to.throw(
'storage/invalid-root-operation'
);
});
it('deleteObject throws', async () => {
expect(() => deleteObject(root)).to.throw(
'storage/invalid-root-operation'
);
});
it('getMetadata throws', async () => {
expect(() => getMetadata(root)).to.throw(
'storage/invalid-root-operation'
);
});
it('updateMetadata throws', async () => {
expect(() => updateMetadata(root, {} as Metadata)).to.throw(
'storage/invalid-root-operation'
);
});
it('getDownloadURL throws', async () => {
expect(() => getDownloadURL(root)).to.throw(
'storage/invalid-root-operation'
);
});
});
});