diff --git a/packages/functions/index.node.ts b/packages/functions/index.node.ts index a0117152ea7..3e30f5b452d 100644 --- a/packages/functions/index.node.ts +++ b/packages/functions/index.node.ts @@ -15,37 +15,8 @@ * limitations under the License. */ import firebase from '@firebase/app'; -import { FirebaseApp } from '@firebase/app-types'; -import { - FirebaseServiceFactory, - _FirebaseNamespace -} from '@firebase/app-types/private'; -import { Service } from './src/api/service'; +import { _FirebaseNamespace } from '@firebase/app-types/private'; +import { registerFunctions } from './src/config'; import 'isomorphic-fetch'; -/** - * Type constant for Firebase Functions. - */ -const FUNCTIONS_TYPE = 'functions'; - -function factory(app: FirebaseApp, _unused: unknown, region?: string): Service { - return new Service(app, region); -} - -export function registerFunctions(instance: _FirebaseNamespace): void { - const namespaceExports = { - // no-inline - Functions: Service - }; - instance.INTERNAL.registerService( - FUNCTIONS_TYPE, - factory as FirebaseServiceFactory, - namespaceExports, - // We don't need to wait on any AppHooks. - undefined, - // Allow multiple functions instances per app. - true - ); -} - registerFunctions(firebase as _FirebaseNamespace); diff --git a/packages/functions/index.ts b/packages/functions/index.ts index a8a43581df7..7c4c6357cc3 100644 --- a/packages/functions/index.ts +++ b/packages/functions/index.ts @@ -15,42 +15,9 @@ * limitations under the License. */ import firebase from '@firebase/app'; -import * as appTypes from '@firebase/app-types'; -import { - FirebaseServiceFactory, - _FirebaseNamespace -} from '@firebase/app-types/private'; +import { _FirebaseNamespace } from '@firebase/app-types/private'; import * as types from '@firebase/functions-types'; -import { Service } from './src/api/service'; - -/** - * Type constant for Firebase Functions. - */ -const FUNCTIONS_TYPE = 'functions'; - -function factory( - app: appTypes.FirebaseApp, - _unused: unknown, - region?: string -): Service { - return new Service(app, region); -} - -export function registerFunctions(instance: _FirebaseNamespace): void { - const namespaceExports = { - // no-inline - Functions: Service - }; - instance.INTERNAL.registerService( - FUNCTIONS_TYPE, - factory as FirebaseServiceFactory, - namespaceExports, - // We don't need to wait on any AppHooks. - undefined, - // Allow multiple functions instances per app. - true - ); -} +import { registerFunctions } from './src/config'; registerFunctions(firebase as _FirebaseNamespace); diff --git a/packages/functions/package.json b/packages/functions/package.json index eb2d611f2bc..eabfbb2d8e7 100644 --- a/packages/functions/package.json +++ b/packages/functions/package.json @@ -73,6 +73,7 @@ "dependencies": { "@firebase/functions-types": "0.3.10", "@firebase/messaging-types": "0.3.4", + "@firebase/component": "0.1.0", "isomorphic-fetch": "2.2.1", "tslib": "1.10.0" }, diff --git a/packages/functions/src/api/service.ts b/packages/functions/src/api/service.ts index 8a96c22e6cd..3627c195c62 100644 --- a/packages/functions/src/api/service.ts +++ b/packages/functions/src/api/service.ts @@ -26,6 +26,9 @@ import { import { _errorForResponse, HttpsErrorImpl } from './error'; import { ContextProvider } from '../context'; import { Serializer } from '../serializer'; +import { Provider } from '@firebase/component'; +import { FirebaseAuthInternal } from '@firebase/auth-interop-types'; +import { FirebaseMessaging } from '@firebase/messaging-types'; /** * The response to an http request. @@ -80,9 +83,11 @@ export class Service implements FirebaseFunctions, FirebaseService { */ constructor( private app_: FirebaseApp, + authProvider: Provider, + messagingProvider: Provider, private region_: string = 'us-central1' ) { - this.contextProvider = new ContextProvider(app_); + this.contextProvider = new ContextProvider(authProvider, messagingProvider); // Cancels all ongoing requests when resolved. this.cancelAllRequests = new Promise(resolve => { this.deleteService = () => { diff --git a/packages/functions/src/config.ts b/packages/functions/src/config.ts new file mode 100644 index 00000000000..f021e1fbcd7 --- /dev/null +++ b/packages/functions/src/config.ts @@ -0,0 +1,51 @@ +/** + * @license + * Copyright 2019 Google Inc. + * + * 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 { Service } from './api/service'; +import { + Component, + ComponentType, + ComponentContainer +} from '@firebase/component'; +import { _FirebaseNamespace } from '@firebase/app-types/private'; + +/** + * Type constant for Firebase Functions. + */ +const FUNCTIONS_TYPE = 'functions'; + +function factory(container: ComponentContainer, region?: string): Service { + // Dependencies + const app = container.getProvider('app').getImmediate(); + const authProvider = container.getProvider('auth-internal'); + const messagingProvider = container.getProvider('messaging'); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return new Service(app, authProvider, messagingProvider, region); +} + +export function registerFunctions(instance: _FirebaseNamespace): void { + const namespaceExports = { + // no-inline + Functions: Service + }; + instance.INTERNAL.registerComponent( + new Component(FUNCTIONS_TYPE, factory, ComponentType.PUBLIC) + .setServiceProps(namespaceExports) + .setMultipleInstances(true) + ); +} diff --git a/packages/functions/src/context.ts b/packages/functions/src/context.ts index b3bd5a7cc2e..59b56515638 100644 --- a/packages/functions/src/context.ts +++ b/packages/functions/src/context.ts @@ -14,9 +14,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { FirebaseApp } from '@firebase/app-types'; import { _FirebaseApp } from '@firebase/app-types/private'; import { FirebaseMessaging } from '@firebase/messaging-types'; +import { FirebaseAuthInternal } from '@firebase/auth-interop-types'; +import { Provider } from '@firebase/component'; /** * The metadata that should be supplied with function calls. @@ -30,11 +31,43 @@ export interface Context { * Helper class to get metadata that should be included with a function call. */ export class ContextProvider { - constructor(private readonly app: FirebaseApp) {} + private auth: FirebaseAuthInternal | null = null; + private messaging: FirebaseMessaging | null = null; + constructor( + authProvider: Provider, + messagingProvider: Provider + ) { + this.auth = authProvider.getImmediate(undefined, { optional: true }); + this.messaging = messagingProvider.getImmediate(undefined, { + optional: true + }); + + if (!this.auth) { + authProvider.get().then( + auth => (this.auth = auth), + () => { + /* get() never rejects */ + } + ); + } + + if (!this.messaging) { + messagingProvider.get().then( + messaging => (this.messaging = messaging), + () => { + /* get() never rejects */ + } + ); + } + } async getAuthToken(): Promise { + if (!this.auth) { + return undefined; + } + try { - const token = await (this.app as _FirebaseApp).INTERNAL.getToken(); + const token = await this.auth.getToken(); if (!token) { return undefined; } @@ -47,15 +80,11 @@ export class ContextProvider { async getInstanceIdToken(): Promise { try { - // HACK: Until we have a separate instanceId package, this is a quick way - // to load in the messaging instance for this app. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (!(this.app as any).messaging) { + if (!this.messaging) { return undefined; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const messaging = (this.app as any).messaging() as FirebaseMessaging; - const token = await messaging.getToken(); + + const token = await this.messaging.getToken(); if (!token) { return undefined; } diff --git a/packages/functions/test/browser/callable.test.ts b/packages/functions/test/browser/callable.test.ts index 00e970c1638..9c2b3eebb8f 100644 --- a/packages/functions/test/browser/callable.test.ts +++ b/packages/functions/test/browser/callable.test.ts @@ -18,48 +18,49 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; import { FirebaseApp } from '@firebase/app-types'; import { _FirebaseApp } from '@firebase/app-types/private'; -import firebase from '@firebase/app'; -import { Service } from '../../src/api/service'; - -/* eslint-disable import/no-duplicates */ -import '@firebase/messaging'; -import { isSupported } from '@firebase/messaging'; +import { makeFakeApp, createTestService } from '../utils'; +import { FirebaseMessaging } from '@firebase/messaging-types'; +import { + Provider, + ComponentContainer, + ComponentType, + Component +} from '@firebase/component'; // eslint-disable-next-line @typescript-eslint/no-require-imports export const TEST_PROJECT = require('../../../../config/project.json'); describe('Firebase Functions > Call', () => { - let app: FirebaseApp; - let functions: Service; - - before(() => { - const projectId = TEST_PROJECT.projectId; - const messagingSenderId = 'messaging-sender-id'; - const region = 'us-central1'; - try { - app = firebase.app('TEST-APP'); - } catch (e) { - app = firebase.initializeApp( - { projectId, messagingSenderId }, - 'TEST-APP' - ); - } - functions = new Service(app, region); + const app: FirebaseApp = makeFakeApp({ + projectId: TEST_PROJECT.projectId, + messagingSenderId: 'messaging-sender-id' }); + const region = 'us-central1'; // TODO(klimt): Move this to the cross-platform tests and delete this file, // once instance id works there. it('instance id', async () => { - if (!isSupported()) { - // Current platform does not support messaging, skip test. - return; - } + // mock firebase messaging + const messagingMock: FirebaseMessaging = ({ + getToken: () => Promise.resolve('iid') + } as unknown) as FirebaseMessaging; + const messagingProvider = new Provider( + 'messaging', + new ComponentContainer('test') + ); + messagingProvider.setComponent( + new Component('messaging', () => messagingMock, ComponentType.PRIVATE) + ); + + const functions = createTestService( + app, + region, + undefined, + messagingProvider + ); // Stub out the messaging method get an instance id token. - const messaging = firebase.messaging(app); - const stub = sinon - .stub(messaging, 'getToken') - .returns(Promise.resolve('iid')); + const stub = sinon.stub(messagingMock, 'getToken').callThrough(); const func = functions.httpsCallable('instanceIdTest'); const result = await func({}); diff --git a/packages/functions/test/callable.test.ts b/packages/functions/test/callable.test.ts index 9fc9817d1cb..2576a10aa4c 100644 --- a/packages/functions/test/callable.test.ts +++ b/packages/functions/test/callable.test.ts @@ -19,9 +19,14 @@ import * as sinon from 'sinon'; import { FirebaseApp } from '@firebase/app-types'; import { _FirebaseApp } from '@firebase/app-types/private'; import { FunctionsErrorCode } from '@firebase/functions-types'; -import firebase from '@firebase/app'; -import '@firebase/messaging'; -import { Service } from '../src/api/service'; +import { + Provider, + ComponentContainer, + Component, + ComponentType +} from '@firebase/component'; +import { FirebaseAuthInternal } from '@firebase/auth-interop-types'; +import { makeFakeApp, createTestService } from './utils'; // eslint-disable-next-line @typescript-eslint/no-require-imports export const TEST_PROJECT = require('../../../config/project.json'); @@ -50,7 +55,7 @@ async function expectError( describe('Firebase Functions > Call', () => { let app: FirebaseApp; - let functions: Service; + const region = 'us-central1'; before(() => { const useEmulator = !!process.env.FIREBASE_FUNCTIONS_EMULATOR_ORIGIN; @@ -58,24 +63,12 @@ describe('Firebase Functions > Call', () => { ? 'functions-integration-test' : TEST_PROJECT.projectId; const messagingSenderId = 'messaging-sender-id'; - const region = 'us-central1'; - try { - app = firebase.app('TEST-APP'); - } catch (e) { - app = firebase.initializeApp( - { projectId, messagingSenderId }, - 'TEST-APP' - ); - } - functions = new Service(app, region); - if (useEmulator) { - functions.useFunctionsEmulator( - process.env.FIREBASE_FUNCTIONS_EMULATOR_ORIGIN! - ); - } + + app = makeFakeApp({ projectId, messagingSenderId }); }); it('simple data', async () => { + const functions = createTestService(app, region); // TODO(klimt): Should we add an API to create a "long" in JS? const data = { bool: true, @@ -96,16 +89,29 @@ describe('Firebase Functions > Call', () => { }); it('scalars', async () => { + const functions = createTestService(app, region); const func = functions.httpsCallable('scalarTest'); const result = await func(17); expect(result.data).to.equal(76); }); it('token', async () => { - // Stub out the internals to get an auth token. - const stub = sinon.stub((app as _FirebaseApp).INTERNAL, 'getToken'); - stub.returns(Promise.resolve({ accessToken: 'token' })); + // mock auth-internal service + const authMock: FirebaseAuthInternal = ({ + getToken: () => Promise.resolve({ accessToken: 'token' }) + } as unknown) as FirebaseAuthInternal; + const authProvider = new Provider( + 'auth-internal', + new ComponentContainer('test') + ); + authProvider.setComponent( + new Component('auth-internal', () => authMock, ComponentType.PRIVATE) + ); + + const functions = createTestService(app, region, authProvider); + // Stub out the internals to get an auth token. + const stub = sinon.stub(authMock, 'getToken').callThrough(); const func = functions.httpsCallable('tokenTest'); const result = await func({}); expect(result.data).to.deep.equal({}); @@ -132,6 +138,7 @@ describe('Firebase Functions > Call', () => { */ it('null', async () => { + const functions = createTestService(app, region); const func = functions.httpsCallable('nullTest'); let result = await func(null); expect(result.data).to.be.null; @@ -142,21 +149,25 @@ describe('Firebase Functions > Call', () => { }); it('missing result', async () => { + const functions = createTestService(app, region); const func = functions.httpsCallable('missingResultTest'); await expectError(func(), 'internal', 'Response is missing data field.'); }); it('unhandled error', async () => { + const functions = createTestService(app, region); const func = functions.httpsCallable('unhandledErrorTest'); await expectError(func(), 'internal', 'internal'); }); it('unknown error', async () => { + const functions = createTestService(app, region); const func = functions.httpsCallable('unknownErrorTest'); await expectError(func(), 'internal', 'internal'); }); it('explicit error', async () => { + const functions = createTestService(app, region); const func = functions.httpsCallable('explicitErrorTest'); await expectError(func(), 'out-of-range', 'explicit nope', { start: 10, @@ -166,11 +177,13 @@ describe('Firebase Functions > Call', () => { }); it('http error', async () => { + const functions = createTestService(app, region); const func = functions.httpsCallable('httpErrorTest'); await expectError(func(), 'invalid-argument', 'invalid-argument'); }); it('timeout', async () => { + const functions = createTestService(app, region); const func = functions.httpsCallable('timeoutTest', { timeout: 10 }); await expectError(func(), 'deadline-exceeded', 'deadline-exceeded'); }); diff --git a/packages/functions/test/service.test.ts b/packages/functions/test/service.test.ts index c90749ced12..71ece550379 100644 --- a/packages/functions/test/service.test.ts +++ b/packages/functions/test/service.test.ts @@ -15,7 +15,7 @@ * limitations under the License. */ import { assert } from 'chai'; -import { Service } from '../src/api/service'; +import { createTestService } from './utils'; describe('Firebase Functions > Service', () => { describe('simple constructor', () => { @@ -24,7 +24,7 @@ describe('Firebase Functions > Service', () => { projectId: 'my-project' } }; - const service = new Service(app); + const service = createTestService(app); it('has valid urls', () => { assert.equal( @@ -48,7 +48,7 @@ describe('Firebase Functions > Service', () => { projectId: 'my-project' } }; - const service = new Service(app, 'my-region'); + const service = createTestService(app, 'my-region'); it('has valid urls', () => { assert.equal( diff --git a/packages/functions/test/utils.ts b/packages/functions/test/utils.ts new file mode 100644 index 00000000000..ebf65d8e350 --- /dev/null +++ b/packages/functions/test/utils.ts @@ -0,0 +1,63 @@ +/** + * @license + * Copyright 2019 Google Inc. + * + * 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 { FirebaseOptions, FirebaseApp } from '@firebase/app-types'; +import { Provider, ComponentContainer } from '@firebase/component'; +import { FirebaseAuthInternal } from '@firebase/auth-interop-types'; +import { FirebaseMessaging } from '@firebase/messaging-types'; +import { Service } from '../src/api/service'; + +export function makeFakeApp(options: FirebaseOptions = {}): FirebaseApp { + options = { + apiKey: 'apiKey', + projectId: 'projectId', + authDomain: 'authDomain', + messagingSenderId: '1234567890', + databaseURL: 'databaseUrl', + storageBucket: 'storageBucket', + appId: '1:777777777777:web:d93b5ca1475efe57', + ...options + }; + return { + name: 'appName', + options, + automaticDataCollectionEnabled: true, + delete: async () => {} + }; +} + +export function createTestService( + app: FirebaseApp, + region?: string, + authProvider = new Provider( + 'auth-internal', + new ComponentContainer('test') + ), + messagingProvider = new Provider( + 'messaging', + new ComponentContainer('test') + ) +): Service { + const functions = new Service(app, authProvider, messagingProvider, region); + const useEmulator = !!process.env.FIREBASE_FUNCTIONS_EMULATOR_ORIGIN; + if (useEmulator) { + functions.useFunctionsEmulator( + process.env.FIREBASE_FUNCTIONS_EMULATOR_ORIGIN! + ); + } + return functions; +}