Skip to content

Commit 42ec68e

Browse files
authored
fix(auth): skip project config name assertion when using the Auth emulator (#3142)
The Auth emulator does not populate the resource `name` field on its /config responses, so getProjectConfig() and updateProjectConfig() throw "INTERNAL ASSERT FAILED: Unable to get/update project config" against the emulator. Skip the assertion in both validators when useEmulator() is true. Production behavior is unchanged — a backend response missing `name` still throws. The guard reuses the existing useEmulator() helper, matching the same dynamic-read pattern AuthResourceUrlBuilder and AuthHttpClient already use to branch on the emulator. Fixes #2461.
1 parent a1d8c6e commit 42ec68e

2 files changed

Lines changed: 136 additions & 1 deletion

File tree

src/auth/auth-api-request.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2035,6 +2035,11 @@ export abstract class AbstractAuthRequestHandler {
20352035
/** Instantiates the getConfig endpoint settings. */
20362036
const GET_PROJECT_CONFIG = new ApiSettings('/config', 'GET')
20372037
.setResponseValidator((response: RequestResponse) => {
2038+
// The Auth emulator does not populate the resource `name` field on the
2039+
// project config response, so skip the assertion when running against it.
2040+
if (useEmulator()) {
2041+
return;
2042+
}
20382043
const data = response.data;
20392044
// Response should always contain at least the config name.
20402045
if (!validator.isNonEmptyString(data?.name)) {
@@ -2049,6 +2054,11 @@ const GET_PROJECT_CONFIG = new ApiSettings('/config', 'GET')
20492054
/** Instantiates the updateConfig endpoint settings. */
20502055
const UPDATE_PROJECT_CONFIG = new ApiSettings('/config?updateMask={updateMask}', 'PATCH')
20512056
.setResponseValidator((response: RequestResponse) => {
2057+
// The Auth emulator does not populate the resource `name` field on the
2058+
// project config response, so skip the assertion when running against it.
2059+
if (useEmulator()) {
2060+
return;
2061+
}
20522062
const data = response.data;
20532063
// Response should always contain at least the config name.
20542064
if (!validator.isNonEmptyString(data?.name)) {

test/unit/auth/auth-api-request.spec.ts

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import { getMetricsHeader, getSdkVersion } from '../../../src/utils/index';
4646
import {
4747
UserImportRecord, OIDCAuthProviderConfig, SAMLAuthProviderConfig, OIDCUpdateAuthProviderRequest,
4848
SAMLUpdateAuthProviderRequest, UserIdentifier, UpdateRequest, UpdateMultiFactorInfoRequest,
49-
CreateTenantRequest, UpdateTenantRequest,
49+
CreateTenantRequest, UpdateTenantRequest, UpdateProjectConfigRequest,
5050
} from '../../../src/auth/index';
5151

5252
chai.should();
@@ -4461,6 +4461,131 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => {
44614461
});
44624462

44634463
if (handler.supportsTenantManagement) {
4464+
describe('getProjectConfig', () => {
4465+
const path = '/v2/projects/project_id/config';
4466+
const method = 'GET';
4467+
const expectedResult = utils.responseFrom({
4468+
name: 'projects/project_id/config',
4469+
});
4470+
4471+
afterEach(() => {
4472+
delete process.env.FIREBASE_AUTH_EMULATOR_HOST;
4473+
});
4474+
4475+
it('should be fulfilled with the project config response', () => {
4476+
const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult);
4477+
stubs.push(stub);
4478+
4479+
const requestHandler = handler.init(mockApp) as AuthRequestHandler;
4480+
return requestHandler.getProjectConfig()
4481+
.then((result) => {
4482+
expect(result).to.deep.equal(expectedResult.data);
4483+
expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, {}));
4484+
});
4485+
});
4486+
4487+
it('should be rejected when the response is missing the name field', () => {
4488+
const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({}));
4489+
stubs.push(stub);
4490+
4491+
const requestHandler = handler.init(mockApp) as AuthRequestHandler;
4492+
return requestHandler.getProjectConfig()
4493+
.then(() => {
4494+
throw new Error('Unexpected success');
4495+
}, (error) => {
4496+
expect(error).to.have.property('code', 'auth/internal-error');
4497+
expect(error.message).to.equal('INTERNAL ASSERT FAILED: Unable to get project config');
4498+
});
4499+
});
4500+
4501+
it('should be fulfilled when the response is missing the name field and the emulator is running', () => {
4502+
const emulatorHost = 'localhost:9099';
4503+
process.env.FIREBASE_AUTH_EMULATOR_HOST = emulatorHost;
4504+
const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({}));
4505+
stubs.push(stub);
4506+
4507+
const requestHandler = handler.init(mockApp) as AuthRequestHandler;
4508+
return requestHandler.getProjectConfig()
4509+
.then((result) => {
4510+
expect(result).to.deep.equal({});
4511+
expect(stub).to.have.been.calledOnce.and.calledWith({
4512+
method,
4513+
url: `http://${emulatorHost}/identitytoolkit.googleapis.com${path}`,
4514+
headers: expectedHeadersEmulator,
4515+
data: {},
4516+
timeout,
4517+
});
4518+
});
4519+
});
4520+
});
4521+
4522+
describe('updateProjectConfig', () => {
4523+
const path = '/v2/projects/project_id/config';
4524+
const method = 'PATCH';
4525+
const validRequest: UpdateProjectConfigRequest = {
4526+
smsRegionConfig: {
4527+
allowlistOnly: {
4528+
allowedRegions: ['AC', 'AD'],
4529+
},
4530+
},
4531+
};
4532+
const expectedPath = path + '?updateMask=smsRegionConfig.allowlistOnly.allowedRegions';
4533+
const expectedResult = utils.responseFrom({
4534+
name: 'projects/project_id/config',
4535+
});
4536+
4537+
afterEach(() => {
4538+
delete process.env.FIREBASE_AUTH_EMULATOR_HOST;
4539+
});
4540+
4541+
it('should be fulfilled with the updated project config response', () => {
4542+
const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult);
4543+
stubs.push(stub);
4544+
4545+
const requestHandler = handler.init(mockApp) as AuthRequestHandler;
4546+
return requestHandler.updateProjectConfig(validRequest)
4547+
.then((result) => {
4548+
expect(result).to.deep.equal(expectedResult.data);
4549+
expect(stub).to.have.been.calledOnce.and.calledWith(
4550+
callParams(expectedPath, method, validRequest));
4551+
});
4552+
});
4553+
4554+
it('should be rejected when the response is missing the name field', () => {
4555+
const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({}));
4556+
stubs.push(stub);
4557+
4558+
const requestHandler = handler.init(mockApp) as AuthRequestHandler;
4559+
return requestHandler.updateProjectConfig(validRequest)
4560+
.then(() => {
4561+
throw new Error('Unexpected success');
4562+
}, (error) => {
4563+
expect(error).to.have.property('code', 'auth/internal-error');
4564+
expect(error.message).to.equal('INTERNAL ASSERT FAILED: Unable to update project config');
4565+
});
4566+
});
4567+
4568+
it('should be fulfilled when the response is missing the name field and the emulator is running', () => {
4569+
const emulatorHost = 'localhost:9099';
4570+
process.env.FIREBASE_AUTH_EMULATOR_HOST = emulatorHost;
4571+
const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({}));
4572+
stubs.push(stub);
4573+
4574+
const requestHandler = handler.init(mockApp) as AuthRequestHandler;
4575+
return requestHandler.updateProjectConfig(validRequest)
4576+
.then((result) => {
4577+
expect(result).to.deep.equal({});
4578+
expect(stub).to.have.been.calledOnce.and.calledWith({
4579+
method,
4580+
url: `http://${emulatorHost}/identitytoolkit.googleapis.com${expectedPath}`,
4581+
headers: expectedHeadersEmulator,
4582+
data: validRequest,
4583+
timeout,
4584+
});
4585+
});
4586+
});
4587+
});
4588+
44644589
describe('getTenant', () => {
44654590
const path = '/v2/projects/project_id/tenants/tenant-id';
44664591
const method = 'GET';

0 commit comments

Comments
 (0)