diff --git a/packages/installations/src/api/get-id.test.ts b/packages/installations/src/api/get-id.test.ts index 77f14a03cb8..2eeee61cff8 100644 --- a/packages/installations/src/api/get-id.test.ts +++ b/packages/installations/src/api/get-id.test.ts @@ -26,17 +26,14 @@ import { import { getFakeInstallations } from '../testing/fake-generators'; import '../testing/setup'; import { getId } from './get-id'; -import { - FirebaseInstallationsImpl, - AppConfig -} from '../interfaces/installation-impl'; +import { FirebaseInstallationsImpl } from '../interfaces/installation-impl'; const FID = 'disciples-of-the-watch'; describe('getId', () => { let installations: FirebaseInstallationsImpl; let getInstallationEntrySpy: SinonStub< - [AppConfig], + [FirebaseInstallationsImpl], Promise >; diff --git a/packages/installations/src/api/get-id.ts b/packages/installations/src/api/get-id.ts index 027526faee4..589e8b49550 100644 --- a/packages/installations/src/api/get-id.ts +++ b/packages/installations/src/api/get-id.ts @@ -30,7 +30,7 @@ import { Installations } from '../interfaces/public-types'; export async function getId(installations: Installations): Promise { const installationsImpl = installations as FirebaseInstallationsImpl; const { installationEntry, registrationPromise } = await getInstallationEntry( - installationsImpl.appConfig + installationsImpl ); if (registrationPromise) { diff --git a/packages/installations/src/api/get-token.test.ts b/packages/installations/src/api/get-token.test.ts index 430d341b3c2..7b798d2b4ab 100644 --- a/packages/installations/src/api/get-token.test.ts +++ b/packages/installations/src/api/get-token.test.ts @@ -175,7 +175,7 @@ const setupInstallationEntryMap: Map< describe('getToken', () => { let installations: FirebaseInstallationsImpl; let createInstallationRequestSpy: SinonStub< - [AppConfig, InProgressInstallationEntry], + [FirebaseInstallationsImpl, InProgressInstallationEntry], Promise >; let generateAuthTokenRequestSpy: SinonStub< diff --git a/packages/installations/src/api/get-token.ts b/packages/installations/src/api/get-token.ts index ca540019c2c..81a47939cbf 100644 --- a/packages/installations/src/api/get-token.ts +++ b/packages/installations/src/api/get-token.ts @@ -18,8 +18,7 @@ import { getInstallationEntry } from '../helpers/get-installation-entry'; import { refreshAuthToken } from '../helpers/refresh-auth-token'; import { - FirebaseInstallationsImpl, - AppConfig + FirebaseInstallationsImpl } from '../interfaces/installation-impl'; import { Installations } from '../interfaces/public-types'; @@ -36,7 +35,7 @@ export async function getToken( forceRefresh = false ): Promise { const installationsImpl = installations as FirebaseInstallationsImpl; - await completeInstallationRegistration(installationsImpl.appConfig); + await completeInstallationRegistration(installationsImpl); // At this point we either have a Registered Installation in the DB, or we've // already thrown an error. @@ -45,9 +44,9 @@ export async function getToken( } async function completeInstallationRegistration( - appConfig: AppConfig + installations: FirebaseInstallationsImpl ): Promise { - const { registrationPromise } = await getInstallationEntry(appConfig); + const { registrationPromise } = await getInstallationEntry(installations); if (registrationPromise) { // A createInstallation request is in progress. Wait until it finishes. diff --git a/packages/installations/src/functions/create-installation-request.test.ts b/packages/installations/src/functions/create-installation-request.test.ts index 67f00585595..0d4eaf37bcb 100644 --- a/packages/installations/src/functions/create-installation-request.test.ts +++ b/packages/installations/src/functions/create-installation-request.test.ts @@ -19,13 +19,15 @@ import { FirebaseError } from '@firebase/util'; import { expect } from 'chai'; import { SinonStub, stub } from 'sinon'; import { CreateInstallationResponse } from '../interfaces/api-response'; -import { AppConfig } from '../interfaces/installation-impl'; +import { + FirebaseInstallationsImpl +} from '../interfaces/installation-impl'; import { InProgressInstallationEntry, RequestStatus } from '../interfaces/installation-entry'; import { compareHeaders } from '../testing/compare-headers'; -import { getFakeAppConfig } from '../testing/fake-generators'; +import { getFakeInstallations } from '../testing/fake-generators'; import '../testing/setup'; import { INSTALLATIONS_API_URL, @@ -38,13 +40,13 @@ import { createInstallationRequest } from './create-installation-request'; const FID = 'defenders-of-the-faith'; describe('createInstallationRequest', () => { - let appConfig: AppConfig; + let fakeInstallations: FirebaseInstallationsImpl; let fetchSpy: SinonStub<[RequestInfo, RequestInit?], Promise>; let inProgressInstallationEntry: InProgressInstallationEntry; let response: CreateInstallationResponse; beforeEach(() => { - appConfig = getFakeAppConfig(); + fakeInstallations = getFakeInstallations(); inProgressInstallationEntry = { fid: FID, @@ -71,7 +73,7 @@ describe('createInstallationRequest', () => { it('registers a pending InstallationEntry', async () => { const registeredInstallationEntry = await createInstallationRequest( - appConfig, + fakeInstallations, inProgressInstallationEntry ); expect(registeredInstallationEntry.registrationStatus).to.equal( @@ -83,12 +85,13 @@ describe('createInstallationRequest', () => { const expectedHeaders = new Headers({ 'Content-Type': 'application/json', Accept: 'application/json', - 'x-goog-api-key': 'apiKey' + 'x-goog-api-key': 'apiKey', + 'x-firebase-client': 'a/1.2.3 b/2.3.4' }); const expectedBody = { fid: FID, authVersion: INTERNAL_AUTH_VERSION, - appId: appConfig.appId, + appId: fakeInstallations.appConfig.appId, sdkVersion: PACKAGE_VERSION }; const expectedRequest: RequestInit = { @@ -98,7 +101,10 @@ describe('createInstallationRequest', () => { }; const expectedEndpoint = `${INSTALLATIONS_API_URL}/projects/projectId/installations`; - await createInstallationRequest(appConfig, inProgressInstallationEntry); + await createInstallationRequest( + fakeInstallations, + inProgressInstallationEntry + ); expect(fetchSpy).to.be.calledOnceWith(expectedEndpoint, expectedRequest); const actualHeaders = fetchSpy.lastCall.lastArg.headers; compareHeaders(expectedHeaders, actualHeaders); @@ -117,7 +123,7 @@ describe('createInstallationRequest', () => { fetchSpy.resolves(new Response(JSON.stringify(response))); const registeredInstallationEntry = await createInstallationRequest( - appConfig, + fakeInstallations, inProgressInstallationEntry ); expect(registeredInstallationEntry.fid).to.equal(FID); @@ -138,7 +144,10 @@ describe('createInstallationRequest', () => { ); await expect( - createInstallationRequest(appConfig, inProgressInstallationEntry) + createInstallationRequest( + fakeInstallations, + inProgressInstallationEntry + ) ).to.be.rejectedWith(FirebaseError); }); @@ -157,7 +166,10 @@ describe('createInstallationRequest', () => { fetchSpy.onCall(1).resolves(new Response(JSON.stringify(response))); await expect( - createInstallationRequest(appConfig, inProgressInstallationEntry) + createInstallationRequest( + fakeInstallations, + inProgressInstallationEntry + ) ).to.be.fulfilled; expect(fetchSpy).to.be.calledTwice; }); diff --git a/packages/installations/src/functions/create-installation-request.ts b/packages/installations/src/functions/create-installation-request.ts index fe8242613f6..9a8916b895f 100644 --- a/packages/installations/src/functions/create-installation-request.ts +++ b/packages/installations/src/functions/create-installation-request.ts @@ -29,15 +29,25 @@ import { getInstallationsEndpoint, retryIfServerError } from './common'; -import { AppConfig } from '../interfaces/installation-impl'; +import { FirebaseInstallationsImpl } from '../interfaces/installation-impl'; export async function createInstallationRequest( - appConfig: AppConfig, + { appConfig, heartbeatServiceProvider }: FirebaseInstallationsImpl, { fid }: InProgressInstallationEntry ): Promise { const endpoint = getInstallationsEndpoint(appConfig); const headers = getHeaders(appConfig); + + // If heartbeat service exists, add the heartbeat string to the header. + const heartbeatService = heartbeatServiceProvider.getImmediate({ + optional: true + }); + if (heartbeatService) { + const heartbeatsHeader = await heartbeatService.getHeartbeatsHeader(); + headers.append('x-firebase-client', heartbeatsHeader); + } + const body = { fid, authVersion: INTERNAL_AUTH_VERSION, diff --git a/packages/installations/src/functions/generate-auth-token-request.test.ts b/packages/installations/src/functions/generate-auth-token-request.test.ts index 01be300d6f8..238dc1738ba 100644 --- a/packages/installations/src/functions/generate-auth-token-request.test.ts +++ b/packages/installations/src/functions/generate-auth-token-request.test.ts @@ -91,7 +91,8 @@ describe('generateAuthTokenRequest', () => { }); const expectedBody = { installation: { - sdkVersion: PACKAGE_VERSION + sdkVersion: PACKAGE_VERSION, + appId: installations.appConfig.appId } }; const expectedRequest: RequestInit = { diff --git a/packages/installations/src/functions/generate-auth-token-request.ts b/packages/installations/src/functions/generate-auth-token-request.ts index eb46447c6ed..15a5b91748f 100644 --- a/packages/installations/src/functions/generate-auth-token-request.ts +++ b/packages/installations/src/functions/generate-auth-token-request.ts @@ -52,7 +52,8 @@ export async function generateAuthTokenRequest( const body = { installation: { - sdkVersion: PACKAGE_VERSION + sdkVersion: PACKAGE_VERSION, + appId: appConfig.appId } }; diff --git a/packages/installations/src/helpers/get-installation-entry.test.ts b/packages/installations/src/helpers/get-installation-entry.test.ts index e8531d442d2..1103ad742b7 100644 --- a/packages/installations/src/helpers/get-installation-entry.test.ts +++ b/packages/installations/src/helpers/get-installation-entry.test.ts @@ -18,14 +18,14 @@ import { AssertionError, expect } from 'chai'; import { SinonFakeTimers, SinonStub, stub, useFakeTimers } from 'sinon'; import * as createInstallationRequestModule from '../functions/create-installation-request'; -import { AppConfig } from '../interfaces/installation-impl'; +import { AppConfig, FirebaseInstallationsImpl } from '../interfaces/installation-impl'; import { InProgressInstallationEntry, RegisteredInstallationEntry, RequestStatus, UnregisteredInstallationEntry } from '../interfaces/installation-entry'; -import { getFakeAppConfig } from '../testing/fake-generators'; +import { getFakeInstallations } from '../testing/fake-generators'; import '../testing/setup'; import { ERROR_FACTORY, ErrorCode } from '../util/errors'; import { sleep } from '../util/sleep'; @@ -37,15 +37,17 @@ const FID = 'cry-of-the-black-birds'; describe('getInstallationEntry', () => { let clock: SinonFakeTimers; + let fakeInstallations: FirebaseInstallationsImpl; let appConfig: AppConfig; let createInstallationRequestSpy: SinonStub< - [AppConfig, InProgressInstallationEntry], + [FirebaseInstallationsImpl, InProgressInstallationEntry], Promise >; beforeEach(() => { clock = useFakeTimers({ now: 1_000_000 }); - appConfig = getFakeAppConfig(); + fakeInstallations = getFakeInstallations(); + appConfig = fakeInstallations.appConfig; createInstallationRequestSpy = stub( createInstallationRequestModule, 'createInstallationRequest' @@ -78,7 +80,7 @@ describe('getInstallationEntry', () => { const oldDbEntry = await get(appConfig); expect(oldDbEntry).to.be.undefined; - const { installationEntry } = await getInstallationEntry(appConfig); + const { installationEntry } = await getInstallationEntry(fakeInstallations); const newDbEntry = await get(appConfig); expect(newDbEntry).to.deep.equal(installationEntry); @@ -90,7 +92,7 @@ describe('getInstallationEntry', () => { const oldDbEntry = await get(appConfig); expect(oldDbEntry).to.be.undefined; - const { installationEntry } = await getInstallationEntry(appConfig); + const { installationEntry } = await getInstallationEntry(fakeInstallations); const newDbEntry = await get(appConfig); expect(newDbEntry).to.deep.equal(installationEntry); @@ -98,7 +100,7 @@ describe('getInstallationEntry', () => { it('saves the InstallationEntry in the database when registration completes', async () => { const { installationEntry, registrationPromise } = - await getInstallationEntry(appConfig); + await getInstallationEntry(fakeInstallations); expect(installationEntry.registrationStatus).to.equal( RequestStatus.IN_PROGRESS ); @@ -126,7 +128,7 @@ describe('getInstallationEntry', () => { }); const { installationEntry, registrationPromise } = - await getInstallationEntry(appConfig); + await getInstallationEntry(fakeInstallations); expect(installationEntry.registrationStatus).to.equal( RequestStatus.IN_PROGRESS ); @@ -154,7 +156,7 @@ describe('getInstallationEntry', () => { }); const { installationEntry, registrationPromise } = - await getInstallationEntry(appConfig); + await getInstallationEntry(fakeInstallations); expect(installationEntry.registrationStatus).to.equal( RequestStatus.IN_PROGRESS ); @@ -170,8 +172,8 @@ describe('getInstallationEntry', () => { }); it('returns the same FID on subsequent calls', async () => { - const { installationEntry: entry1 } = await getInstallationEntry(appConfig); - const { installationEntry: entry2 } = await getInstallationEntry(appConfig); + const { installationEntry: entry1 } = await getInstallationEntry(fakeInstallations); + const { installationEntry: entry2 } = await getInstallationEntry(fakeInstallations); expect(entry1.fid).to.equal(entry2.fid); }); @@ -187,7 +189,7 @@ describe('getInstallationEntry', () => { it('returns a new pending InstallationEntry and triggers createInstallation', async () => { const { installationEntry, registrationPromise } = - await getInstallationEntry(appConfig); + await getInstallationEntry(fakeInstallations); if (installationEntry.registrationStatus !== RequestStatus.IN_PROGRESS) { throw new AssertionError('InstallationEntry is not IN_PROGRESS.'); @@ -208,7 +210,7 @@ describe('getInstallationEntry', () => { it('returns a new unregistered InstallationEntry if app is offline', async () => { stub(navigator, 'onLine').value(false); - const { installationEntry } = await getInstallationEntry(appConfig); + const { installationEntry } = await getInstallationEntry(fakeInstallations); expect(installationEntry).to.deep.equal({ fid: FID, @@ -219,18 +221,18 @@ describe('getInstallationEntry', () => { }); it('does not trigger createInstallation REST call on subsequent calls', async () => { - await getInstallationEntry(appConfig); - await getInstallationEntry(appConfig); + await getInstallationEntry(fakeInstallations); + await getInstallationEntry(fakeInstallations); expect(createInstallationRequestSpy).to.be.calledOnce; }); it('returns a registrationPromise on subsequent calls before initial promise resolves', async () => { const { registrationPromise: promise1 } = await getInstallationEntry( - appConfig + fakeInstallations ); const { registrationPromise: promise2 } = await getInstallationEntry( - appConfig + fakeInstallations ); expect(createInstallationRequestSpy).to.be.calledOnce; @@ -240,7 +242,7 @@ describe('getInstallationEntry', () => { it('does not return a registrationPromise on subsequent calls after initial promise resolves', async () => { const { registrationPromise: promise1 } = await getInstallationEntry( - appConfig + fakeInstallations ); expect(promise1).to.be.an.instanceOf(Promise); @@ -248,7 +250,7 @@ describe('getInstallationEntry', () => { await expect(promise1).to.be.fulfilled; const { registrationPromise: promise2 } = await getInstallationEntry( - appConfig + fakeInstallations ); expect(promise2).to.be.undefined; @@ -266,7 +268,7 @@ describe('getInstallationEntry', () => { // FID generation fails. generateInstallationEntrySpy.returns(generateFidModule.INVALID_FID); - const getInstallationEntryPromise = getInstallationEntry(appConfig); + const getInstallationEntryPromise = getInstallationEntry(fakeInstallations); const { installationEntry, registrationPromise } = await getInstallationEntryPromise; @@ -287,7 +289,7 @@ describe('getInstallationEntry', () => { it('returns a pending InstallationEntry and triggers createInstallation', async () => { const { installationEntry, registrationPromise } = - await getInstallationEntry(appConfig); + await getInstallationEntry(fakeInstallations); if (installationEntry.registrationStatus !== RequestStatus.IN_PROGRESS) { throw new AssertionError('InstallationEntry is not IN_PROGRESS.'); @@ -306,7 +308,7 @@ describe('getInstallationEntry', () => { it('returns the same InstallationEntry if the app is offline', async () => { stub(navigator, 'onLine').value(false); - const { installationEntry } = await getInstallationEntry(appConfig); + const { installationEntry } = await getInstallationEntry(fakeInstallations); expect(installationEntry).to.deep.equal({ fid: FID, @@ -329,7 +331,7 @@ describe('getInstallationEntry', () => { it("returns the same InstallationEntry if the request hasn't timed out", async () => { clock.now = 1_001_000; // One second after the request was initiated. - const { installationEntry } = await getInstallationEntry(appConfig); + const { installationEntry } = await getInstallationEntry(fakeInstallations); expect(installationEntry).to.deep.equal({ fid: FID, @@ -347,7 +349,7 @@ describe('getInstallationEntry', () => { true /* Needed to allow the createInstallation request to complete. */ }); - const installationEntryPromise = getInstallationEntry(appConfig); + const installationEntryPromise = getInstallationEntry(fakeInstallations); // The pending request fails after a while. clock.tick(3000); @@ -389,7 +391,7 @@ describe('getInstallationEntry', () => { true /* Needed to allow the createInstallation request to complete. */ }); - const installationEntryPromise = getInstallationEntry(appConfig); + const installationEntryPromise = getInstallationEntry(fakeInstallations); // The pending request fails after a while. clock.tick(3000); @@ -418,7 +420,7 @@ describe('getInstallationEntry', () => { it('returns a new pending InstallationEntry and triggers createInstallation if the request had already timed out', async () => { clock.now = 1_015_000; // Fifteen seconds after the request was initiated. - const { installationEntry } = await getInstallationEntry(appConfig); + const { installationEntry } = await getInstallationEntry(fakeInstallations); expect(installationEntry).to.deep.equal({ fid: FID, @@ -432,7 +434,7 @@ describe('getInstallationEntry', () => { stub(navigator, 'onLine').value(false); clock.now = 1_015_000; // Fifteen seconds after the request was initiated. - const { installationEntry } = await getInstallationEntry(appConfig); + const { installationEntry } = await getInstallationEntry(fakeInstallations); expect(installationEntry).to.deep.equal({ fid: FID, @@ -454,7 +456,7 @@ describe('getInstallationEntry', () => { }); it('returns the InstallationEntry from the database', async () => { - const { installationEntry } = await getInstallationEntry(appConfig); + const { installationEntry } = await getInstallationEntry(fakeInstallations); expect(installationEntry).to.deep.equal({ fid: FID, diff --git a/packages/installations/src/helpers/get-installation-entry.ts b/packages/installations/src/helpers/get-installation-entry.ts index fb91642d29d..1de700910ff 100644 --- a/packages/installations/src/helpers/get-installation-entry.ts +++ b/packages/installations/src/helpers/get-installation-entry.ts @@ -16,7 +16,10 @@ */ import { createInstallationRequest } from '../functions/create-installation-request'; -import { AppConfig } from '../interfaces/installation-impl'; +import { + AppConfig, + FirebaseInstallationsImpl +} from '../interfaces/installation-impl'; import { InProgressInstallationEntry, InstallationEntry, @@ -40,14 +43,14 @@ export interface InstallationEntryWithRegistrationPromise { * Also triggers a registration request if it is necessary and possible. */ export async function getInstallationEntry( - appConfig: AppConfig + installations: FirebaseInstallationsImpl ): Promise { let registrationPromise: Promise | undefined; - const installationEntry = await update(appConfig, oldEntry => { + const installationEntry = await update(installations.appConfig, oldEntry => { const installationEntry = updateOrCreateInstallationEntry(oldEntry); const entryWithPromise = triggerRegistrationIfNecessary( - appConfig, + installations, installationEntry ); registrationPromise = entryWithPromise.registrationPromise; @@ -88,7 +91,7 @@ function updateOrCreateInstallationEntry( * to be registered. */ function triggerRegistrationIfNecessary( - appConfig: AppConfig, + installations: FirebaseInstallationsImpl, installationEntry: InstallationEntry ): InstallationEntryWithRegistrationPromise { if (installationEntry.registrationStatus === RequestStatus.NOT_STARTED) { @@ -110,7 +113,7 @@ function triggerRegistrationIfNecessary( registrationTime: Date.now() }; const registrationPromise = registerInstallation( - appConfig, + installations, inProgressEntry ); return { installationEntry: inProgressEntry, registrationPromise }; @@ -119,7 +122,7 @@ function triggerRegistrationIfNecessary( ) { return { installationEntry, - registrationPromise: waitUntilFidRegistration(appConfig) + registrationPromise: waitUntilFidRegistration(installations) }; } else { return { installationEntry }; @@ -128,23 +131,23 @@ function triggerRegistrationIfNecessary( /** This will be executed only once for each new Firebase Installation. */ async function registerInstallation( - appConfig: AppConfig, + installations: FirebaseInstallationsImpl, installationEntry: InProgressInstallationEntry ): Promise { try { const registeredInstallationEntry = await createInstallationRequest( - appConfig, + installations, installationEntry ); - return set(appConfig, registeredInstallationEntry); + return set(installations.appConfig, registeredInstallationEntry); } catch (e) { if (isServerError(e) && e.customData.serverCode === 409) { // Server returned a "FID can not be used" error. // Generate a new ID next time. - await remove(appConfig); + await remove(installations.appConfig); } else { // Registration failed. Set FID as not registered. - await set(appConfig, { + await set(installations.appConfig, { fid: installationEntry.fid, registrationStatus: RequestStatus.NOT_STARTED }); @@ -155,24 +158,26 @@ async function registerInstallation( /** Call if FID registration is pending in another request. */ async function waitUntilFidRegistration( - appConfig: AppConfig + installations: FirebaseInstallationsImpl ): Promise { // Unfortunately, there is no way of reliably observing when a value in // IndexedDB changes (yet, see https://github.com/WICG/indexed-db-observers), // so we need to poll. - let entry: InstallationEntry = await updateInstallationRequest(appConfig); + let entry: InstallationEntry = await updateInstallationRequest( + installations.appConfig + ); while (entry.registrationStatus === RequestStatus.IN_PROGRESS) { // createInstallation request still in progress. await sleep(100); - entry = await updateInstallationRequest(appConfig); + entry = await updateInstallationRequest(installations.appConfig); } if (entry.registrationStatus === RequestStatus.NOT_STARTED) { // The request timed out or failed in a different call. Try again. const { installationEntry, registrationPromise } = - await getInstallationEntry(appConfig); + await getInstallationEntry(installations); if (registrationPromise) { return registrationPromise;