mirror of
https://github.com/hanzoai/firebase-js-sdk.git
synced 2026-09-24 16:59:10 +00:00
376 lines
12 KiB
TypeScript
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'
|
|
);
|
|
});
|
|
});
|
|
});
|