diff --git a/packages/data-connect/src/api/DataConnect.ts b/packages/data-connect/src/api/DataConnect.ts index 9393b9ef4e4..27ab83660fd 100644 --- a/packages/data-connect/src/api/DataConnect.ts +++ b/packages/data-connect/src/api/DataConnect.ts @@ -160,6 +160,7 @@ export class DataConnect { this._transport = new this._transportClass( this.dataConnectOptions, this.app.options.apiKey, + this.app.options.appId, this._authTokenProvider, this._appCheckTokenProvider, undefined, diff --git a/packages/data-connect/src/network/fetch.ts b/packages/data-connect/src/network/fetch.ts index fbd930977ab..928b9f873cf 100644 --- a/packages/data-connect/src/network/fetch.ts +++ b/packages/data-connect/src/network/fetch.ts @@ -34,6 +34,7 @@ export function dcFetch( url: string, body: U, { signal }: AbortController, + appId: string | null, accessToken: string | null, appCheckToken: string | null, _isUsingGen: boolean @@ -48,6 +49,9 @@ export function dcFetch( if (accessToken) { headers['X-Firebase-Auth-Token'] = accessToken; } + if (appId) { + headers['x-firebase-gmpid'] = appId; + } if (appCheckToken) { headers['X-Firebase-AppCheck'] = appCheckToken; } diff --git a/packages/data-connect/src/network/transport/index.ts b/packages/data-connect/src/network/transport/index.ts index 8ccbc88f435..5518faa0f95 100644 --- a/packages/data-connect/src/network/transport/index.ts +++ b/packages/data-connect/src/network/transport/index.ts @@ -45,6 +45,7 @@ export interface CancellableOperation extends PromiseLike<{ data: T }> { export type TransportClass = new ( options: DataConnectOptions, apiKey?: string, + appId?: string, authProvider?: AuthTokenProvider, appCheckProvider?: AppCheckTokenProvider, transportOptions?: TransportOptions, diff --git a/packages/data-connect/src/network/transport/rest.ts b/packages/data-connect/src/network/transport/rest.ts index aaaf22abd64..85847868c5d 100644 --- a/packages/data-connect/src/network/transport/rest.ts +++ b/packages/data-connect/src/network/transport/rest.ts @@ -39,6 +39,7 @@ export class RESTTransport implements DataConnectTransport { constructor( options: DataConnectOptions, private apiKey?: string | undefined, + private appId?: string, private authProvider?: AuthTokenProvider | undefined, private appCheckProvider?: AppCheckTokenProvider | undefined, transportOptions?: TransportOptions | undefined, @@ -175,6 +176,7 @@ export class RESTTransport implements DataConnectTransport { variables: body } as unknown as U, // TODO(mtewani): This is a patch, fix this. abortController, + this.appId, this._accessToken, this._appCheckToken, this._isUsingGen @@ -203,6 +205,7 @@ export class RESTTransport implements DataConnectTransport { variables: body } as unknown as U, abortController, + this.appId, this._accessToken, this._appCheckToken, this._isUsingGen diff --git a/packages/data-connect/test/emulatorSeeder.ts b/packages/data-connect/test/emulatorSeeder.ts index df7071a5868..36cdf691169 100644 --- a/packages/data-connect/test/emulatorSeeder.ts +++ b/packages/data-connect/test/emulatorSeeder.ts @@ -82,7 +82,6 @@ export async function setupQueries( connection_string: 'postgresql://postgres:secretpassword@localhost:5432/postgres?sslmode=disable' }; - fs.writeFileSync('./emulator.json', JSON.stringify(toWrite)); return fetch(`http://localhost:${EMULATOR_PORT}/setupSchema`, { method: 'POST', body: JSON.stringify(toWrite) diff --git a/packages/data-connect/test/unit/fetch.test.ts b/packages/data-connect/test/unit/fetch.test.ts index f0d7f38ee8c..a50ac188724 100644 --- a/packages/data-connect/test/unit/fetch.test.ts +++ b/packages/data-connect/test/unit/fetch.test.ts @@ -40,7 +40,15 @@ describe('fetch', () => { message }); await expect( - dcFetch('http://localhost', {}, {} as AbortController, null, null, false) + dcFetch( + 'http://localhost', + {}, + {} as AbortController, + null, + null, + null, + false + ) ).to.eventually.be.rejectedWith(message); }); it('should throw a stringified message when the server responds with an error without a message property in the body', async () => { @@ -51,7 +59,15 @@ describe('fetch', () => { }; mockFetch(json); await expect( - dcFetch('http://localhost', {}, {} as AbortController, null, null, false) + dcFetch( + 'http://localhost', + {}, + {} as AbortController, + null, + null, + null, + false + ) ).to.eventually.be.rejectedWith(JSON.stringify(json)); }); }); diff --git a/packages/data-connect/test/unit/gmpid.test.ts b/packages/data-connect/test/unit/gmpid.test.ts new file mode 100644 index 00000000000..c6679ca242d --- /dev/null +++ b/packages/data-connect/test/unit/gmpid.test.ts @@ -0,0 +1,84 @@ +/** + * @license + * Copyright 2024 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 { deleteApp, initializeApp, FirebaseApp } from '@firebase/app'; +import { expect, use } from 'chai'; +import * as sinon from 'sinon'; +import sinonChai from 'sinon-chai'; + +import { DataConnect, executeQuery, getDataConnect, queryRef } from '../../src'; +import { initializeFetch } from '../../src/network/fetch'; + +use(sinonChai); +const json = { + message: 'unauthorized' +}; +const fakeFetchImpl = sinon.stub().returns( + Promise.resolve({ + json: () => { + return Promise.resolve(json); + }, + status: 401 + } as Response) +); + +describe('GMPID Tests', () => { + let dc: DataConnect; + let app: FirebaseApp; + const APPID = 'MYAPPID'; + beforeEach(() => { + initializeFetch(fakeFetchImpl); + app = initializeApp({ projectId: 'p', appId: APPID }, 'fdsasdf'); // TODO(mtewani): Replace with util function + dc = getDataConnect(app, { connector: 'c', location: 'l', service: 's' }); + }); + afterEach(async () => { + await dc._delete(); + await deleteApp(app); + }); + it('should send a request with the corresponding gmpid if using the app id is specified', async () => { + // @ts-ignore + await executeQuery(queryRef(dc, '')).catch(() => {}); + expect(fakeFetchImpl).to.be.calledWithMatch( + 'https://firebasedataconnect.googleapis.com/v1alpha/projects/p/locations/l/services/s/connectors/c:executeQuery', + { + headers: { + ['x-firebase-gmpid']: APPID + } + } + ); + }); + it('should send a request with no gmpid if using the app id is not specified', async () => { + const app2 = initializeApp({ projectId: 'p' }, 'def'); // TODO(mtewani): Replace with util function + const dc2 = getDataConnect(app2, { + connector: 'c', + location: 'l', + service: 's' + }); + // @ts-ignore + await executeQuery(queryRef(dc2, '')).catch(() => {}); + expect(fakeFetchImpl).to.be.calledWithMatch( + 'https://firebasedataconnect.googleapis.com/v1alpha/projects/p/locations/l/services/s/connectors/c:executeQuery', + { + headers: { + ['x-firebase-gmpid']: APPID + } + } + ); + await dc2._delete(); + await deleteApp(app2); + }); +}); diff --git a/packages/data-connect/test/unit/queries.test.ts b/packages/data-connect/test/unit/queries.test.ts index 444601bd5ea..68bd96268a6 100644 --- a/packages/data-connect/test/unit/queries.test.ts +++ b/packages/data-connect/test/unit/queries.test.ts @@ -68,7 +68,7 @@ describe('Queries', () => { it('[QUERY] should retry auth whenever the fetcher returns with unauthorized', async () => { initializeFetch(fakeFetchImpl); const authProvider = new FakeAuthProvider(); - const rt = new RESTTransport(options, undefined, authProvider); + const rt = new RESTTransport(options, undefined, undefined, authProvider); await expect(rt.invokeQuery('test', null)).to.eventually.be.rejectedWith( json.message ); @@ -77,7 +77,7 @@ describe('Queries', () => { it('[MUTATION] should retry auth whenever the fetcher returns with unauthorized', async () => { initializeFetch(fakeFetchImpl); const authProvider = new FakeAuthProvider(); - const rt = new RESTTransport(options, undefined, authProvider); + const rt = new RESTTransport(options, undefined, undefined, authProvider); await expect(rt.invokeMutation('test', null)).to.eventually.be.rejectedWith( json.message ); @@ -86,7 +86,7 @@ describe('Queries', () => { it("should not retry auth whenever the fetcher returns with unauthorized and the token doesn't change", async () => { initializeFetch(fakeFetchImpl); const authProvider = new FakeAuthProvider(); - const rt = new RESTTransport(options, undefined, authProvider); + const rt = new RESTTransport(options, undefined, undefined, authProvider); rt._setLastToken('initial token'); await expect( rt.invokeQuery('test', null) as Promise