Skip to content

Commit fe71c55

Browse files
authored
fix(functions): store CLOUD_TASKS_EMULATOR_HOST at construction time (#3167)
* fix(functions): store CLOUD_TASKS_EMULATOR_HOST at construction time * fix: address gemini review
1 parent 659c056 commit fe71c55

2 files changed

Lines changed: 70 additions & 10 deletions

File tree

src/functions/functions-api-client-internal.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const DEFAULT_LOCATION = 'us-central1';
4646
*/
4747
export class FunctionsApiClient {
4848
private readonly httpClient: HttpClient;
49+
private readonly emulatorHost?: string;
4950
private projectId?: string;
5051
private accountId?: string;
5152

@@ -56,7 +57,9 @@ export class FunctionsApiClient {
5657
message: 'First argument passed to getFunctions() must be a valid Firebase app instance.'
5758
});
5859
}
59-
this.httpClient = new FunctionsHttpClient(app as FirebaseApp);
60+
const emulatorHost = process.env.CLOUD_TASKS_EMULATOR_HOST?.trim();
61+
this.emulatorHost = emulatorHost || undefined;
62+
this.httpClient = new FunctionsHttpClient(app as FirebaseApp, this.emulatorHost);
6063
}
6164
/**
6265
* Deletes a task from a queue.
@@ -103,7 +106,7 @@ export class FunctionsApiClient {
103106
}
104107

105108
try {
106-
const serviceUrl = tasksEmulatorUrl(resources)?.concat('/', id)
109+
const serviceUrl = tasksEmulatorUrl(resources, this.emulatorHost)?.concat('/', id)
107110
?? await this.getUrl(resources, CLOUD_TASKS_API_URL_FORMAT.concat('/', id));
108111
const request: HttpRequestConfig = {
109112
method: 'DELETE',
@@ -165,7 +168,7 @@ export class FunctionsApiClient {
165168
const task = this.validateTaskOptions(data, resources, opts);
166169
try {
167170
const serviceUrl =
168-
tasksEmulatorUrl(resources) ??
171+
tasksEmulatorUrl(resources, this.emulatorHost) ??
169172
await this.getUrl(resources, CLOUD_TASKS_API_URL_FORMAT);
170173

171174
const taskPayload = await this.updateTaskPayload(task, resources, extensionId);
@@ -347,7 +350,7 @@ export class FunctionsApiClient {
347350
}
348351

349352
private async updateTaskPayload(task: Task, resources: utils.ParsedResource, extensionId?: string): Promise<Task> {
350-
const defaultUrl = process.env.CLOUD_TASKS_EMULATOR_HOST ?
353+
const defaultUrl = this.emulatorHost ?
351354
''
352355
: await this.getUrl(resources, FIREBASE_FUNCTION_URL_FORMAT);
353356

@@ -368,7 +371,7 @@ export class FunctionsApiClient {
368371
const account = await this.getServiceAccount();
369372
task.httpRequest.oidcToken = { serviceAccountEmail: account };
370373
} catch (e) {
371-
if (process.env.CLOUD_TASKS_EMULATOR_HOST) {
374+
if (this.emulatorHost) {
372375
task.httpRequest.oidcToken = { serviceAccountEmail: EMULATED_SERVICE_ACCOUNT_DEFAULT };
373376
} else {
374377
throw e;
@@ -408,8 +411,12 @@ export class FunctionsApiClient {
408411
* when communicating with the Emulator.
409412
*/
410413
class FunctionsHttpClient extends AuthorizedHttpClient {
414+
constructor(app: FirebaseApp, private readonly emulatorHost?: string) {
415+
super(app);
416+
}
417+
411418
protected getToken(): Promise<string> {
412-
if (process.env.CLOUD_TASKS_EMULATOR_HOST) {
419+
if (this.emulatorHost) {
413420
return Promise.resolve('owner');
414421
}
415422
return super.getToken();
@@ -448,9 +455,9 @@ export interface Task {
448455
};
449456
}
450457

451-
function tasksEmulatorUrl(resources: utils.ParsedResource): string | undefined {
452-
if (process.env.CLOUD_TASKS_EMULATOR_HOST) {
453-
return `http://${process.env.CLOUD_TASKS_EMULATOR_HOST}/projects/${resources.projectId}/locations/${resources.locationId}/queues/${resources.resourceId}/tasks`;
458+
function tasksEmulatorUrl(resources: utils.ParsedResource, emulatorHost?: string): string | undefined {
459+
if (emulatorHost) {
460+
return `http://${emulatorHost}/projects/${resources.projectId}/locations/${resources.locationId}/queues/${resources.resourceId}/tasks`;
454461
}
455462
return undefined;
456463
}

test/unit/functions/functions-api-client-internal.spec.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,56 @@ describe('FunctionsApiClient', () => {
130130
expect(() => new FunctionsApiClient(null as unknown as FirebaseApp))
131131
.to.throw('First argument passed to getFunctions() must be a valid Firebase app instance.');
132132
});
133+
134+
it('should cache CLOUD_TASKS_EMULATOR_HOST at construction time', async () => {
135+
delete process.env.CLOUD_TASKS_EMULATOR_HOST;
136+
const prodClient = new FunctionsApiClient(app);
137+
138+
process.env.CLOUD_TASKS_EMULATOR_HOST = CLOUD_TASKS_EMULATOR_HOST;
139+
const emulatorClient = new FunctionsApiClient(app);
140+
141+
delete process.env.CLOUD_TASKS_EMULATOR_HOST;
142+
143+
const sendStub = sinon
144+
.stub(HttpClient.prototype, 'send')
145+
.resolves(utils.responseFrom({}, 200));
146+
stubs.push(sendStub);
147+
148+
await prodClient.delete('mock-task', FUNCTION_NAME);
149+
await emulatorClient.delete('mock-task', FUNCTION_NAME);
150+
151+
expect(sendStub).to.have.been.calledTwice;
152+
expect(sendStub.firstCall).to.have.been.calledWith({
153+
method: 'DELETE',
154+
url: CLOUD_TASKS_URL.concat('/', 'mock-task'),
155+
headers: EXPECTED_HEADERS,
156+
});
157+
expect(sendStub.secondCall).to.have.been.calledWith({
158+
method: 'DELETE',
159+
url: CLOUD_TASKS_URL_EMULATOR.concat('/', 'mock-task'),
160+
headers: EXPECTED_HEADERS_EMULATOR,
161+
});
162+
});
163+
164+
for (const hostVal of ['', ' ']) {
165+
it(`should ignore CLOUD_TASKS_EMULATOR_HOST when set to "${hostVal}"`, async () => {
166+
process.env.CLOUD_TASKS_EMULATOR_HOST = hostVal;
167+
const emptyHostClient = new FunctionsApiClient(app);
168+
delete process.env.CLOUD_TASKS_EMULATOR_HOST;
169+
170+
const stub = sinon
171+
.stub(HttpClient.prototype, 'send')
172+
.resolves(utils.responseFrom({}, 200));
173+
stubs.push(stub);
174+
175+
await emptyHostClient.delete('mock-task', FUNCTION_NAME);
176+
expect(stub).to.have.been.calledWith({
177+
method: 'DELETE',
178+
url: CLOUD_TASKS_URL.concat('/', 'mock-task'),
179+
headers: EXPECTED_HEADERS,
180+
});
181+
});
182+
}
133183
});
134184

135185
describe('enqueue', () => {
@@ -529,6 +579,7 @@ describe('FunctionsApiClient', () => {
529579
.resolves(utils.responseFrom({}, 200));
530580
stubs.push(stub);
531581
process.env.CLOUD_TASKS_EMULATOR_HOST = CLOUD_TASKS_EMULATOR_HOST;
582+
apiClient = new FunctionsApiClient(app);
532583
return apiClient.enqueue({}, FUNCTION_NAME, '', { uri: TEST_TASK_PAYLOAD.httpRequest.url })
533584
.then(() => {
534585
expect(stub).to.have.been.calledOnce.and.calledWith({
@@ -550,6 +601,7 @@ describe('FunctionsApiClient', () => {
550601
.resolves(utils.responseFrom({}, 200));
551602
stubs.push(stub);
552603
process.env.CLOUD_TASKS_EMULATOR_HOST = CLOUD_TASKS_EMULATOR_HOST;
604+
apiClient = new FunctionsApiClient(app);
553605
return apiClient.enqueue({}, FUNCTION_NAME)
554606
.then(() => {
555607
expect(stub).to.have.been.calledOnce.and.calledWith({
@@ -569,7 +621,6 @@ describe('FunctionsApiClient', () => {
569621
projectId: 'test-project',
570622
serviceAccountId: ''
571623
});
572-
apiClient = new FunctionsApiClient(app);
573624

574625
const expectedPayload = deepCopy(TEST_TASK_PAYLOAD);
575626
expectedPayload.httpRequest.oidcToken = { serviceAccountEmail: EMULATED_SERVICE_ACCOUNT_DEFAULT };
@@ -578,6 +629,7 @@ describe('FunctionsApiClient', () => {
578629
.resolves(utils.responseFrom({}, 200));
579630
stubs.push(stub);
580631
process.env.CLOUD_TASKS_EMULATOR_HOST = CLOUD_TASKS_EMULATOR_HOST;
632+
apiClient = new FunctionsApiClient(app);
581633
return apiClient.enqueue({}, FUNCTION_NAME, '', { uri: TEST_TASK_PAYLOAD.httpRequest.url })
582634
.then(() => {
583635
expect(stub).to.have.been.calledOnce.and.calledWith({
@@ -631,6 +683,7 @@ describe('FunctionsApiClient', () => {
631683

632684
it('should redirect to the emulator when CLOUD_TASKS_EMULATOR_HOST is set', async () => {
633685
process.env.CLOUD_TASKS_EMULATOR_HOST = CLOUD_TASKS_EMULATOR_HOST;
686+
apiClient = new FunctionsApiClient(app);
634687
const stub = sinon
635688
.stub(HttpClient.prototype, 'send')
636689
.resolves(utils.responseFrom({}, 200));

0 commit comments

Comments
 (0)