From 7405e7d593b40c9945c32ffbe66ac6fb11e2991e Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 16 Mar 2022 15:25:30 -0700 Subject: [PATCH] [Auth] heartbeat implementation (#6033) * Auth heartbeat implementation * Undo worker commenting out * Changeset --- .changeset/neat-olives-punch.md | 6 ++++ packages/auth-compat/src/auth.test.ts | 11 +++++-- .../auth-compat/src/popup_redirect.test.ts | 3 +- packages/auth-compat/test/helpers/helpers.ts | 8 +++++ packages/auth/src/api/index.ts | 3 +- packages/auth/src/core/auth/auth_impl.test.ts | 29 +++++++++++++++++-- packages/auth/src/core/auth/auth_impl.ts | 11 +++++++ packages/auth/src/core/auth/register.ts | 9 +++--- .../auth/src/platform_browser/auth.test.ts | 8 ++--- packages/auth/test/helpers/mock_auth.ts | 13 ++++++++- 10 files changed, 84 insertions(+), 17 deletions(-) create mode 100644 .changeset/neat-olives-punch.md diff --git a/.changeset/neat-olives-punch.md b/.changeset/neat-olives-punch.md new file mode 100644 index 00000000000..9ae7caec82a --- /dev/null +++ b/.changeset/neat-olives-punch.md @@ -0,0 +1,6 @@ +--- +"@firebase/auth-compat": patch +"@firebase/auth": patch +--- + +Heartbeat diff --git a/packages/auth-compat/src/auth.test.ts b/packages/auth-compat/src/auth.test.ts index 0299170827f..af49682afed 100644 --- a/packages/auth-compat/src/auth.test.ts +++ b/packages/auth-compat/src/auth.test.ts @@ -24,6 +24,7 @@ import sinonChai from 'sinon-chai'; import { Auth } from './auth'; import { CompatPopupRedirectResolver } from './popup_redirect'; import * as platform from './platform'; +import { FAKE_HEARTBEAT_CONTROLLER_PROVIDER } from '../test/helpers/helpers'; use(sinonChai); @@ -41,9 +42,13 @@ describe('auth compat', () => { beforeEach(() => { app = { options: { apiKey: 'api-key' } } as FirebaseApp; - underlyingAuth = new exp.AuthImpl(app, { - apiKey: 'api-key' - } as exp.ConfigInternal); + underlyingAuth = new exp.AuthImpl( + app, + FAKE_HEARTBEAT_CONTROLLER_PROVIDER, + { + apiKey: 'api-key' + } as exp.ConfigInternal + ); sinon.stub(underlyingAuth, '_initializeWithPersistence'); providerStub = sinon.createStubInstance(Provider); diff --git a/packages/auth-compat/src/popup_redirect.test.ts b/packages/auth-compat/src/popup_redirect.test.ts index be01b8f622e..80bc382e4fa 100644 --- a/packages/auth-compat/src/popup_redirect.test.ts +++ b/packages/auth-compat/src/popup_redirect.test.ts @@ -22,6 +22,7 @@ import * as exp from '@firebase/auth/internal'; import * as platform from './platform'; import { CompatPopupRedirectResolver } from './popup_redirect'; import { FirebaseApp } from '@firebase/app-compat'; +import { FAKE_HEARTBEAT_CONTROLLER_PROVIDER } from '../test/helpers/helpers'; use(sinonChai); @@ -41,7 +42,7 @@ describe('popup_redirect/CompatPopupRedirectResolver', () => { beforeEach(() => { compatResolver = new CompatPopupRedirectResolver(); const app = { options: { apiKey: 'api-key' } } as FirebaseApp; - auth = new exp.AuthImpl(app, { + auth = new exp.AuthImpl(app, FAKE_HEARTBEAT_CONTROLLER_PROVIDER, { apiKey: 'api-key' } as exp.ConfigInternal); }); diff --git a/packages/auth-compat/test/helpers/helpers.ts b/packages/auth-compat/test/helpers/helpers.ts index 005f8efa61a..aa5069461c6 100644 --- a/packages/auth-compat/test/helpers/helpers.ts +++ b/packages/auth-compat/test/helpers/helpers.ts @@ -17,6 +17,7 @@ import * as sinon from 'sinon'; import firebase from '@firebase/app-compat'; +import { Provider } from '@firebase/component'; import '../..'; import * as exp from '@firebase/auth/internal'; @@ -26,6 +27,13 @@ import { } from '../../../auth/test/helpers/integration/settings'; import { resetEmulator } from '../../../auth/test/helpers/integration/emulator_rest_helpers'; +// Heartbeat is fully tested in core auth impl +export const FAKE_HEARTBEAT_CONTROLLER_PROVIDER = { + getImmediate(): undefined { + return undefined; + } +} as unknown as Provider<'heartbeat'>; + export function initializeTestInstance(): void { firebase.initializeApp(getAppConfig()); const stub = stubConsoleToSilenceEmulatorWarnings(); diff --git a/packages/auth/src/api/index.ts b/packages/auth/src/api/index.ts index f727d8d2f66..a3375a2829c 100644 --- a/packages/auth/src/api/index.ts +++ b/packages/auth/src/api/index.ts @@ -37,7 +37,8 @@ export const enum HttpHeader { CONTENT_TYPE = 'Content-Type', X_FIREBASE_LOCALE = 'X-Firebase-Locale', X_CLIENT_VERSION = 'X-Client-Version', - X_FIREBASE_GMPID = 'X-Firebase-gmpid' + X_FIREBASE_GMPID = 'X-Firebase-gmpid', + X_FIREBASE_CLIENT = 'X-Firebase-Client', } export const enum Endpoint { diff --git a/packages/auth/src/core/auth/auth_impl.test.ts b/packages/auth/src/core/auth/auth_impl.test.ts index 4e226973a0c..bfc8ca75223 100644 --- a/packages/auth/src/core/auth/auth_impl.test.ts +++ b/packages/auth/src/core/auth/auth_impl.test.ts @@ -23,7 +23,7 @@ import sinonChai from 'sinon-chai'; import { FirebaseApp } from '@firebase/app'; import { FirebaseError } from '@firebase/util'; -import { testAuth, testUser } from '../../../test/helpers/mock_auth'; +import { FAKE_HEARTBEAT_CONTROLLER, FAKE_HEARTBEAT_CONTROLLER_PROVIDER, testAuth, testUser } from '../../../test/helpers/mock_auth'; import { AuthInternal } from '../../model/auth'; import { UserInternal } from '../../model/user'; import { PersistenceInternal } from '../persistence'; @@ -53,7 +53,7 @@ describe('core/auth/auth_impl', () => { beforeEach(async () => { persistenceStub = sinon.stub(_getInstance(inMemoryPersistence)); - const authImpl = new AuthImpl(FAKE_APP, { + const authImpl = new AuthImpl(FAKE_APP, FAKE_HEARTBEAT_CONTROLLER_PROVIDER, { apiKey: FAKE_APP.options.apiKey!, apiHost: DefaultConfig.API_HOST, apiScheme: DefaultConfig.API_SCHEME, @@ -431,7 +431,7 @@ describe('core/auth/auth_impl', () => { }); it('prevents initialization from completing', async () => { - const authImpl = new AuthImpl(FAKE_APP, { + const authImpl = new AuthImpl(FAKE_APP, FAKE_HEARTBEAT_CONTROLLER_PROVIDER, { apiKey: FAKE_APP.options.apiKey!, apiHost: DefaultConfig.API_HOST, apiScheme: DefaultConfig.API_SCHEME, @@ -474,6 +474,29 @@ describe('core/auth/auth_impl', () => { 'X-Client-Version': 'v', 'X-Firebase-gmpid': 'app-id', }); + delete auth.app.options.appId; + }); + + it('adds the heartbeat if available', async () => { + sinon.stub(FAKE_HEARTBEAT_CONTROLLER, 'getHeartbeatsHeader').returns(Promise.resolve('heartbeat')); + expect(await auth._getAdditionalHeaders()).to.eql({ + 'X-Client-Version': 'v', + 'X-Firebase-Client': 'heartbeat', + }); + }); + + it('does not add heartbeat if none returned', async () => { + sinon.stub(FAKE_HEARTBEAT_CONTROLLER, 'getHeartbeatsHeader').returns(Promise.resolve('')); + expect(await auth._getAdditionalHeaders()).to.eql({ + 'X-Client-Version': 'v', + }); + }); + + it('does not add heartbeat if controller unavailable', async () => { + sinon.stub(FAKE_HEARTBEAT_CONTROLLER_PROVIDER, 'getImmediate').returns(undefined as any); + expect(await auth._getAdditionalHeaders()).to.eql({ + 'X-Client-Version': 'v', + }); }); }); }); diff --git a/packages/auth/src/core/auth/auth_impl.ts b/packages/auth/src/core/auth/auth_impl.ts index b71e4e24bb4..d20c1596874 100644 --- a/packages/auth/src/core/auth/auth_impl.ts +++ b/packages/auth/src/core/auth/auth_impl.ts @@ -16,6 +16,7 @@ */ import { _FirebaseService, FirebaseApp } from '@firebase/app'; +import { Provider } from '@firebase/component'; import { Auth, AuthErrorMap, @@ -103,6 +104,7 @@ export class AuthImpl implements AuthInternal, _FirebaseService { constructor( public readonly app: FirebaseApp, + private readonly heartbeatServiceProvider: Provider<'heartbeat'>, public readonly config: ConfigInternal ) { this.name = app.name; @@ -583,9 +585,18 @@ export class AuthImpl implements AuthInternal, _FirebaseService { const headers: Record = { [HttpHeader.X_CLIENT_VERSION]: this.clientVersion, }; + if (this.app.options.appId) { headers[HttpHeader.X_FIREBASE_GMPID] = this.app.options.appId; } + + // If the heartbeat service exists, add the heartbeat string + const heartbeatsHeader = await this.heartbeatServiceProvider.getImmediate({ + optional: true, + })?.getHeartbeatsHeader(); + if (heartbeatsHeader) { + headers[HttpHeader.X_FIREBASE_CLIENT] = heartbeatsHeader; + } return headers; } } diff --git a/packages/auth/src/core/auth/register.ts b/packages/auth/src/core/auth/register.ts index abff9c25db0..a8439bc418a 100644 --- a/packages/auth/src/core/auth/register.ts +++ b/packages/auth/src/core/auth/register.ts @@ -19,7 +19,7 @@ import { _registerComponent, registerVersion } from '@firebase/app'; import { Component, ComponentType, - InstantiationMode + InstantiationMode, } from '@firebase/component'; import { name, version } from '../../../package.json'; @@ -61,8 +61,9 @@ export function registerAuth(clientPlatform: ClientPlatform): void { _ComponentName.AUTH, (container, { options: deps }: { options?: Dependencies }) => { const app = container.getProvider('app').getImmediate()!; + const heartbeatServiceProvider = container.getProvider<'heartbeat'>('heartbeat'); const { apiKey, authDomain } = app.options; - return (app => { + return ((app, heartbeatServiceProvider) => { _assert( apiKey && !apiKey.includes(':'), AuthErrorCode.INVALID_API_KEY, @@ -82,11 +83,11 @@ export function registerAuth(clientPlatform: ClientPlatform): void { sdkClientVersion: _getClientVersion(clientPlatform) }; - const authInstance = new AuthImpl(app, config); + const authInstance = new AuthImpl(app, heartbeatServiceProvider, config); _initializeAuthInstance(authInstance, deps); return authInstance; - })(app); + })(app, heartbeatServiceProvider); }, ComponentType.PUBLIC ) diff --git a/packages/auth/src/platform_browser/auth.test.ts b/packages/auth/src/platform_browser/auth.test.ts index fd4aac82df8..ad104dfceb7 100644 --- a/packages/auth/src/platform_browser/auth.test.ts +++ b/packages/auth/src/platform_browser/auth.test.ts @@ -28,7 +28,7 @@ import { } from '../model/public_types'; import { OperationType } from '../model/enums'; -import { testAuth, testUser } from '../../test/helpers/mock_auth'; +import { FAKE_HEARTBEAT_CONTROLLER_PROVIDER, testAuth, testUser } from '../../test/helpers/mock_auth'; import { AuthImpl, DefaultConfig } from '../core/auth/auth_impl'; import { _initializeAuthInstance } from '../core/auth/initialize'; import { AuthErrorCode } from '../core/errors'; @@ -66,7 +66,7 @@ describe('core/auth/auth_impl', () => { beforeEach(async () => { persistenceStub = sinon.stub(_getInstance(inMemoryPersistence)); - const authImpl = new AuthImpl(FAKE_APP, { + const authImpl = new AuthImpl(FAKE_APP, FAKE_HEARTBEAT_CONTROLLER_PROVIDER, { apiKey: FAKE_APP.options.apiKey!, apiHost: DefaultConfig.API_HOST, apiScheme: DefaultConfig.API_SCHEME, @@ -132,7 +132,7 @@ describe('core/auth/initializeAuth', () => { popupRedirectResolver?: PopupRedirectResolver, authDomain = FAKE_APP.options.authDomain ): Promise { - const auth = new AuthImpl(FAKE_APP, { + const auth = new AuthImpl(FAKE_APP, FAKE_HEARTBEAT_CONTROLLER_PROVIDER, { apiKey: FAKE_APP.options.apiKey!, apiHost: DefaultConfig.API_HOST, apiScheme: DefaultConfig.API_SCHEME, @@ -359,7 +359,7 @@ describe('core/auth/initializeAuth', () => { // Manually initialize auth to make sure no error is thrown, // since the _initializeAuthInstance function floats - const auth = new AuthImpl(FAKE_APP, { + const auth = new AuthImpl(FAKE_APP, FAKE_HEARTBEAT_CONTROLLER_PROVIDER, { apiKey: FAKE_APP.options.apiKey!, apiHost: DefaultConfig.API_HOST, apiScheme: DefaultConfig.API_SCHEME, diff --git a/packages/auth/test/helpers/mock_auth.ts b/packages/auth/test/helpers/mock_auth.ts index 459621cd77d..5b85e162933 100644 --- a/packages/auth/test/helpers/mock_auth.ts +++ b/packages/auth/test/helpers/mock_auth.ts @@ -16,6 +16,7 @@ */ import { FirebaseApp } from '@firebase/app'; +import { Provider } from '@firebase/component'; import { PopupRedirectResolver } from '../../src/model/public_types'; import { debugErrorMap } from '../../src'; @@ -44,6 +45,16 @@ const FAKE_APP: FirebaseApp = { automaticDataCollectionEnabled: false }; +export const FAKE_HEARTBEAT_CONTROLLER = { + getHeartbeatsHeader: async () => '', +}; + +export const FAKE_HEARTBEAT_CONTROLLER_PROVIDER: Provider<'heartbeat'> = { + getImmediate(): typeof FAKE_HEARTBEAT_CONTROLLER { + return FAKE_HEARTBEAT_CONTROLLER; + } +} as unknown as Provider<'heartbeat'>; + export class MockPersistenceLayer extends InMemoryPersistence { lastObjectSet: PersistedBlob | null = null; @@ -62,7 +73,7 @@ export async function testAuth( popupRedirectResolver?: PopupRedirectResolver, persistence = new MockPersistenceLayer() ): Promise { - const auth: TestAuth = new AuthImpl(FAKE_APP, { + const auth: TestAuth = new AuthImpl(FAKE_APP, FAKE_HEARTBEAT_CONTROLLER_PROVIDER, { apiKey: TEST_KEY, authDomain: TEST_AUTH_DOMAIN, apiHost: TEST_HOST,