Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions src/functions/functions-api-client-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const DEFAULT_LOCATION = 'us-central1';
*/
export class FunctionsApiClient {
private readonly httpClient: HttpClient;
private readonly emulatorHost?: string;
private projectId?: string;
private accountId?: string;

Expand All @@ -56,7 +57,8 @@ export class FunctionsApiClient {
message: 'First argument passed to getFunctions() must be a valid Firebase app instance.'
});
}
this.httpClient = new FunctionsHttpClient(app as FirebaseApp);
this.emulatorHost = process.env.CLOUD_TASKS_EMULATOR_HOST;
this.httpClient = new FunctionsHttpClient(app as FirebaseApp, this.emulatorHost);
Comment thread
jonathanedey marked this conversation as resolved.
Outdated
}
/**
* Deletes a task from a queue.
Expand Down Expand Up @@ -103,7 +105,7 @@ export class FunctionsApiClient {
}

try {
const serviceUrl = tasksEmulatorUrl(resources)?.concat('/', id)
const serviceUrl = tasksEmulatorUrl(resources, this.emulatorHost)?.concat('/', id)
?? await this.getUrl(resources, CLOUD_TASKS_API_URL_FORMAT.concat('/', id));
const request: HttpRequestConfig = {
method: 'DELETE',
Expand Down Expand Up @@ -165,7 +167,7 @@ export class FunctionsApiClient {
const task = this.validateTaskOptions(data, resources, opts);
try {
const serviceUrl =
tasksEmulatorUrl(resources) ??
tasksEmulatorUrl(resources, this.emulatorHost) ??
await this.getUrl(resources, CLOUD_TASKS_API_URL_FORMAT);

const taskPayload = await this.updateTaskPayload(task, resources, extensionId);
Expand Down Expand Up @@ -347,7 +349,7 @@ export class FunctionsApiClient {
}

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

Expand All @@ -368,7 +370,7 @@ export class FunctionsApiClient {
const account = await this.getServiceAccount();
task.httpRequest.oidcToken = { serviceAccountEmail: account };
} catch (e) {
if (process.env.CLOUD_TASKS_EMULATOR_HOST) {
if (this.emulatorHost) {
task.httpRequest.oidcToken = { serviceAccountEmail: EMULATED_SERVICE_ACCOUNT_DEFAULT };
} else {
throw e;
Expand Down Expand Up @@ -408,8 +410,12 @@ export class FunctionsApiClient {
* when communicating with the Emulator.
*/
class FunctionsHttpClient extends AuthorizedHttpClient {
constructor(app: FirebaseApp, private readonly emulatorHost?: string) {
super(app);
}

protected getToken(): Promise<string> {
if (process.env.CLOUD_TASKS_EMULATOR_HOST) {
if (this.emulatorHost) {
return Promise.resolve('owner');
}
return super.getToken();
Expand Down Expand Up @@ -448,9 +454,9 @@ export interface Task {
};
}

function tasksEmulatorUrl(resources: utils.ParsedResource): string | undefined {
if (process.env.CLOUD_TASKS_EMULATOR_HOST) {
return `http://${process.env.CLOUD_TASKS_EMULATOR_HOST}/projects/${resources.projectId}/locations/${resources.locationId}/queues/${resources.resourceId}/tasks`;
function tasksEmulatorUrl(resources: utils.ParsedResource, emulatorHost?: string): string | undefined {
if (emulatorHost) {
return `http://${emulatorHost}/projects/${resources.projectId}/locations/${resources.locationId}/queues/${resources.resourceId}/tasks`;
}
return undefined;
}
59 changes: 58 additions & 1 deletion test/unit/functions/functions-api-client-internal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,60 @@ describe('FunctionsApiClient', () => {
expect(() => new FunctionsApiClient(null as unknown as FirebaseApp))
.to.throw('First argument passed to getFunctions() must be a valid Firebase app instance.');
});

it('should cache CLOUD_TASKS_EMULATOR_HOST at construction time', async () => {
delete process.env.CLOUD_TASKS_EMULATOR_HOST;
const prodClient = new FunctionsApiClient(app);

process.env.CLOUD_TASKS_EMULATOR_HOST = CLOUD_TASKS_EMULATOR_HOST;
const emulatorClient = new FunctionsApiClient(app);

delete process.env.CLOUD_TASKS_EMULATOR_HOST;

const prodStub = sinon
.stub(HttpClient.prototype, 'send')
.resolves(utils.responseFrom({}, 200));
stubs.push(prodStub);

await prodClient.delete('mock-task', FUNCTION_NAME);
expect(prodStub).to.have.been.calledWith({
method: 'DELETE',
url: CLOUD_TASKS_URL.concat('/', 'mock-task'),
headers: EXPECTED_HEADERS,
});

prodStub.restore();

const emulatorStub = sinon
.stub(HttpClient.prototype, 'send')
.resolves(utils.responseFrom({}, 200));
stubs.push(emulatorStub);

await emulatorClient.delete('mock-task', FUNCTION_NAME);
expect(emulatorStub).to.have.been.calledWith({
method: 'DELETE',
url: CLOUD_TASKS_URL_EMULATOR.concat('/', 'mock-task'),
headers: EXPECTED_HEADERS_EMULATOR,
});
Comment thread
jonathanedey marked this conversation as resolved.
Outdated
});

it('should ignore empty string CLOUD_TASKS_EMULATOR_HOST', async () => {
process.env.CLOUD_TASKS_EMULATOR_HOST = '';
const emptyHostClient = new FunctionsApiClient(app);
delete process.env.CLOUD_TASKS_EMULATOR_HOST;

const stub = sinon
.stub(HttpClient.prototype, 'send')
.resolves(utils.responseFrom({}, 200));
stubs.push(stub);

await emptyHostClient.delete('mock-task', FUNCTION_NAME);
expect(stub).to.have.been.calledWith({
method: 'DELETE',
url: CLOUD_TASKS_URL.concat('/', 'mock-task'),
headers: EXPECTED_HEADERS,
});
});
});

describe('enqueue', () => {
Expand Down Expand Up @@ -529,6 +583,7 @@ describe('FunctionsApiClient', () => {
.resolves(utils.responseFrom({}, 200));
stubs.push(stub);
process.env.CLOUD_TASKS_EMULATOR_HOST = CLOUD_TASKS_EMULATOR_HOST;
apiClient = new FunctionsApiClient(app);
return apiClient.enqueue({}, FUNCTION_NAME, '', { uri: TEST_TASK_PAYLOAD.httpRequest.url })
.then(() => {
expect(stub).to.have.been.calledOnce.and.calledWith({
Expand All @@ -550,6 +605,7 @@ describe('FunctionsApiClient', () => {
.resolves(utils.responseFrom({}, 200));
stubs.push(stub);
process.env.CLOUD_TASKS_EMULATOR_HOST = CLOUD_TASKS_EMULATOR_HOST;
apiClient = new FunctionsApiClient(app);
return apiClient.enqueue({}, FUNCTION_NAME)
.then(() => {
expect(stub).to.have.been.calledOnce.and.calledWith({
Expand All @@ -569,7 +625,6 @@ describe('FunctionsApiClient', () => {
projectId: 'test-project',
serviceAccountId: ''
});
apiClient = new FunctionsApiClient(app);

const expectedPayload = deepCopy(TEST_TASK_PAYLOAD);
expectedPayload.httpRequest.oidcToken = { serviceAccountEmail: EMULATED_SERVICE_ACCOUNT_DEFAULT };
Expand All @@ -578,6 +633,7 @@ describe('FunctionsApiClient', () => {
.resolves(utils.responseFrom({}, 200));
stubs.push(stub);
process.env.CLOUD_TASKS_EMULATOR_HOST = CLOUD_TASKS_EMULATOR_HOST;
apiClient = new FunctionsApiClient(app);
return apiClient.enqueue({}, FUNCTION_NAME, '', { uri: TEST_TASK_PAYLOAD.httpRequest.url })
.then(() => {
expect(stub).to.have.been.calledOnce.and.calledWith({
Expand Down Expand Up @@ -631,6 +687,7 @@ describe('FunctionsApiClient', () => {

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