voluptasmollitia
Version:
Monorepo for the Firebase JavaScript SDK
471 lines (438 loc) • 14 kB
text/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 { assert, expect, use } from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import * as sinon from 'sinon';
import { useFakeTimers } from 'sinon';
import * as sinonChai from 'sinon-chai';
import { FirebaseError, getUA } from '@firebase/util';
import { mockEndpoint } from '../../test/helpers/api/helper';
import { testAuth, TestAuth } from '../../test/helpers/mock_auth';
import * as mockFetch from '../../test/helpers/mock_fetch';
import { AuthErrorCode } from '../core/errors';
import { ConfigInternal } from '../model/auth';
import {
_getFinalTarget,
_performApiRequest,
DEFAULT_API_TIMEOUT_MS,
Endpoint,
HttpHeader,
HttpMethod,
_addTidIfNecessary
} from './';
import { ServerError } from './errors';
import { SDK_VERSION } from '@firebase/app-exp';
import { _getBrowserName } from '../core/util/browser';
use(sinonChai);
use(chaiAsPromised);
describe('api/_performApiRequest', () => {
const request = {
requestKey: 'request-value'
};
const serverResponse = {
responseKey: 'response-value'
};
let auth: TestAuth;
beforeEach(async () => {
auth = await testAuth();
});
context('with regular requests', () => {
beforeEach(mockFetch.setUp);
afterEach(mockFetch.tearDown);
it('should set the correct request, method and HTTP Headers', async () => {
const mock = mockEndpoint(Endpoint.SIGN_UP, serverResponse);
const response = await _performApiRequest<
typeof request,
typeof serverResponse
>(auth, HttpMethod.POST, Endpoint.SIGN_UP, request);
expect(response).to.eql(serverResponse);
expect(mock.calls.length).to.eq(1);
expect(mock.calls[0].method).to.eq(HttpMethod.POST);
expect(mock.calls[0].request).to.eql(request);
expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq(
'application/json'
);
expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq(
'testSDK/0.0.0'
);
});
it('should set the device language if available', async () => {
auth.languageCode = 'jp';
const mock = mockEndpoint(Endpoint.SIGN_UP, serverResponse);
const response = await _performApiRequest<
typeof request,
typeof serverResponse
>(auth, HttpMethod.POST, Endpoint.SIGN_UP, request);
expect(response).to.eql(serverResponse);
expect(mock.calls[0].headers.get(HttpHeader.X_FIREBASE_LOCALE)).to.eq(
'jp'
);
});
it('should set the framework in clientVersion if logged', async () => {
auth._logFramework('Mythical');
const mock = mockEndpoint(Endpoint.SIGN_UP, serverResponse);
const response = await _performApiRequest<
typeof request,
typeof serverResponse
>(auth, HttpMethod.POST, Endpoint.SIGN_UP, request);
expect(response).to.eql(serverResponse);
expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq(
`${_getBrowserName(getUA())}/JsCore/${SDK_VERSION}/Mythical`
);
// If a new framework is logged, the client version header should change as well.
auth._logFramework('Magical');
const response2 = await _performApiRequest<
typeof request,
typeof serverResponse
>(auth, HttpMethod.POST, Endpoint.SIGN_UP, request);
expect(response2).to.eql(serverResponse);
expect(mock.calls[1].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq(
// frameworks should be sorted alphabetically
`${_getBrowserName(getUA())}/JsCore/${SDK_VERSION}/Magical,Mythical`
);
});
it('should translate server errors to auth errors', async () => {
const mock = mockEndpoint(
Endpoint.SIGN_UP,
{
error: {
code: 400,
message: ServerError.EMAIL_EXISTS,
errors: [
{
message: ServerError.EMAIL_EXISTS
}
]
}
},
400
);
const promise = _performApiRequest<typeof request, typeof serverResponse>(
auth,
HttpMethod.POST,
Endpoint.SIGN_UP,
request
);
await expect(promise).to.be.rejectedWith(
FirebaseError,
'auth/email-already-in-use'
);
expect(mock.calls[0].request).to.eql(request);
});
it('should translate server success with errorMessage into auth error', async () => {
const response = {
errorMessage: ServerError.FEDERATED_USER_ID_ALREADY_LINKED,
idToken: 'foo-bar'
};
const mock = mockEndpoint(Endpoint.SIGN_IN_WITH_IDP, response, 200);
const promise = _performApiRequest<typeof request, typeof serverResponse>(
auth,
HttpMethod.POST,
Endpoint.SIGN_IN_WITH_IDP,
request
);
await expect(promise)
.to.be.rejectedWith(FirebaseError, 'auth/credential-already-in-use')
.eventually.with.deep.property('customData', {
appName: 'test-app',
_tokenResponse: response
});
expect(mock.calls[0].request).to.eql(request);
});
it('should translate complex server errors to auth errors', async () => {
const mock = mockEndpoint(
Endpoint.SIGN_UP,
{
error: {
code: 400,
message: `${ServerError.INVALID_PHONE_NUMBER} : TOO_SHORT`,
errors: [
{
message: ServerError.EMAIL_EXISTS
}
]
}
},
400
);
const promise = _performApiRequest<typeof request, typeof serverResponse>(
auth,
HttpMethod.POST,
Endpoint.SIGN_UP,
request
);
await expect(promise).to.be.rejectedWith(
FirebaseError,
'auth/invalid-phone-number'
);
expect(mock.calls[0].request).to.eql(request);
});
it('should handle unknown server errors', async () => {
const mock = mockEndpoint(
Endpoint.SIGN_UP,
{
error: {
code: 400,
message: 'Awesome error',
errors: [
{
message: 'Awesome error'
}
]
}
},
400
);
const promise = _performApiRequest<typeof request, typeof serverResponse>(
auth,
HttpMethod.POST,
Endpoint.SIGN_UP,
request
);
await expect(promise).to.be.rejectedWith(
FirebaseError,
'auth/awesome-error'
);
expect(mock.calls[0].request).to.eql(request);
});
it('should support custom error handling per endpoint', async () => {
const mock = mockEndpoint(
Endpoint.SIGN_UP,
{
error: {
code: 400,
message: ServerError.EXPIRED_OOB_CODE,
errors: [
{
message: ServerError.EXPIRED_OOB_CODE
}
]
}
},
400
);
const promise = _performApiRequest<typeof request, typeof serverResponse>(
auth,
HttpMethod.POST,
Endpoint.SIGN_UP,
request,
{
[ServerError.EXPIRED_OOB_CODE]: AuthErrorCode.ARGUMENT_ERROR
}
);
await expect(promise).to.be.rejectedWith(
FirebaseError,
'auth/argument-error'
);
expect(mock.calls[0].request).to.eql(request);
});
});
context('with network issues', () => {
afterEach(mockFetch.tearDown);
it('should handle timeouts', async () => {
const clock = useFakeTimers();
mockFetch.setUpWithOverride(() => {
return new Promise<never>(() => null);
});
const promise = _performApiRequest<typeof request, never>(
auth,
HttpMethod.POST,
Endpoint.SIGN_UP,
request
);
clock.tick(DEFAULT_API_TIMEOUT_MS.get() + 1);
await expect(promise).to.be.rejectedWith(FirebaseError, 'auth/timeout');
clock.restore();
});
it('should clear the network timeout on success', async () => {
const clock = useFakeTimers();
sinon.spy(clock, 'clearTimeout');
mockFetch.setUp();
mockEndpoint(Endpoint.SIGN_UP, {});
const promise = _performApiRequest(
auth,
HttpMethod.POST,
Endpoint.SIGN_UP,
request
);
await promise;
expect(clock.clearTimeout).to.have.been.called;
clock.restore();
});
it('should handle network failure', async () => {
mockFetch.setUpWithOverride(() => {
return new Promise<never>((_, reject) =>
reject(new Error('network error'))
);
});
const promise = _performApiRequest<typeof request, never>(
auth,
HttpMethod.POST,
Endpoint.SIGN_UP,
request
);
await expect(promise).to.be.rejectedWith(
FirebaseError,
'auth/network-request-failed'
);
});
});
context('edgcase error mapping', () => {
beforeEach(mockFetch.setUp);
afterEach(mockFetch.tearDown);
it('should generate a need_conirmation error with the response', async () => {
mockEndpoint(Endpoint.SIGN_UP, {
needConfirmation: true,
idToken: 'id-token'
});
try {
await _performApiRequest<typeof request, typeof serverResponse>(
auth,
HttpMethod.POST,
Endpoint.SIGN_UP,
request
);
assert.fail('Call should have failed');
} catch (e) {
expect(e.code).to.eq(`auth/${AuthErrorCode.NEED_CONFIRMATION}`);
expect((e as FirebaseError).customData!._tokenResponse).to.eql({
needConfirmation: true,
idToken: 'id-token'
});
}
});
it('should generate a credential already in use error', async () => {
const response = {
error: {
code: 400,
message: ServerError.FEDERATED_USER_ID_ALREADY_LINKED,
errors: [
{
message: ServerError.FEDERATED_USER_ID_ALREADY_LINKED
}
]
}
};
mockEndpoint(Endpoint.SIGN_UP, response, 400);
try {
await _performApiRequest<typeof request, typeof serverResponse>(
auth,
HttpMethod.POST,
Endpoint.SIGN_UP,
request
);
assert.fail('Call should have failed');
} catch (e) {
expect(e.code).to.eq(`auth/${AuthErrorCode.CREDENTIAL_ALREADY_IN_USE}`);
expect((e as FirebaseError).customData!._tokenResponse).to.eql(
response
);
}
});
it('should pull out email and phone number', async () => {
const response = {
error: {
code: 400,
message: ServerError.EMAIL_EXISTS,
errors: [
{
message: ServerError.EMAIL_EXISTS
}
]
},
email: 'email@test.com',
phoneNumber: '+1555-this-is-a-number'
};
mockEndpoint(Endpoint.SIGN_UP, response, 400);
try {
await _performApiRequest<typeof request, typeof serverResponse>(
auth,
HttpMethod.POST,
Endpoint.SIGN_UP,
request
);
assert.fail('Call should have failed');
} catch (e) {
expect(e.code).to.eq(`auth/${AuthErrorCode.EMAIL_EXISTS}`);
expect((e as FirebaseError).customData!.email).to.eq('email@test.com');
expect((e as FirebaseError).customData!.phoneNumber).to.eq(
'+1555-this-is-a-number'
);
}
});
});
context('_getFinalTarget', () => {
it('works properly with a non-emulated environment', () => {
expect(_getFinalTarget(auth, 'host', '/path', 'query=test')).to.eq(
'mock://host/path?query=test'
);
});
it('works properly with an emulated environment', () => {
(auth.config as ConfigInternal).emulator = {
url: 'http://localhost:5000/'
};
expect(_getFinalTarget(auth, 'host', '/path', 'query=test')).to.eq(
'http://localhost:5000/host/path?query=test'
);
});
});
context('_addTidIfNecessary', () => {
it('adds the tenant ID if it is not already defined', () => {
auth.tenantId = 'auth-tenant-id';
expect(
_addTidIfNecessary<Record<string, string>>(auth, { foo: 'bar' })
).to.eql({
tenantId: 'auth-tenant-id',
foo: 'bar'
});
});
it('does not overwrite the tenant ID if already supplied', () => {
auth.tenantId = 'auth-tenant-id';
expect(
_addTidIfNecessary<Record<string, string>>(auth, {
foo: 'bar',
tenantId: 'request-tenant-id'
})
).to.eql({
tenantId: 'request-tenant-id',
foo: 'bar'
});
});
it('leaves tenant id on the request even if not specified on auth', () => {
auth.tenantId = null;
expect(
_addTidIfNecessary<Record<string, string>>(auth, {
foo: 'bar',
tenantId: 'request-tenant-id'
})
).to.eql({
tenantId: 'request-tenant-id',
foo: 'bar'
});
});
it('does not attach the tenant ID at all if not specified', () => {
auth.tenantId = null;
expect(
_addTidIfNecessary<Record<string, string>>(auth, { foo: 'bar' })
)
.to.eql({
foo: 'bar'
})
.and.not.have.property('tenantId');
});
});
});