Skip to content

Commit 32fc9fe

Browse files
committed
fix(auth): skip project config name assertion when using the Auth emulator
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 48d5212 commit 32fc9fe

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
@@ -1984,6 +1984,11 @@ export abstract class AbstractAuthRequestHandler {
19841984
/** Instantiates the getConfig endpoint settings. */
19851985
const GET_PROJECT_CONFIG = new ApiSettings('/config', 'GET')
19861986
.setResponseValidator((response: any) => {
1987+
// The Auth emulator does not populate the resource `name` field on the
1988+
// project config response, so skip the assertion when running against it.
1989+
if (useEmulator()) {
1990+
return;
1991+
}
19871992
// Response should always contain at least the config name.
19881993
if (!validator.isNonEmptyString(response.name)) {
19891994
throw new FirebaseAuthError(
@@ -1996,6 +2001,11 @@ const GET_PROJECT_CONFIG = new ApiSettings('/config', 'GET')
19962001
/** Instantiates the updateConfig endpoint settings. */
19972002
const UPDATE_PROJECT_CONFIG = new ApiSettings('/config?updateMask={updateMask}', 'PATCH')
19982003
.setResponseValidator((response: any) => {
2004+
// The Auth emulator does not populate the resource `name` field on the
2005+
// project config response, so skip the assertion when running against it.
2006+
if (useEmulator()) {
2007+
return;
2008+
}
19992009
// Response should always contain at least the config name.
20002010
if (!validator.isNonEmptyString(response.name)) {
20012011
throw new FirebaseAuthError(

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();
@@ -4417,6 +4417,131 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => {
44174417
});
44184418

44194419
if (handler.supportsTenantManagement) {
4420+
describe('getProjectConfig', () => {
4421+
const path = '/v2/projects/project_id/config';
4422+
const method = 'GET';
4423+
const expectedResult = utils.responseFrom({
4424+
name: 'projects/project_id/config',
4425+
});
4426+
4427+
afterEach(() => {
4428+
delete process.env.FIREBASE_AUTH_EMULATOR_HOST;
4429+
});
4430+
4431+
it('should be fulfilled with the project config response', () => {
4432+
const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult);
4433+
stubs.push(stub);
4434+
4435+
const requestHandler = handler.init(mockApp) as AuthRequestHandler;
4436+
return requestHandler.getProjectConfig()
4437+
.then((result) => {
4438+
expect(result).to.deep.equal(expectedResult.data);
4439+
expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, {}));
4440+
});
4441+
});
4442+
4443+
it('should be rejected when the response is missing the name field', () => {
4444+
const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({}));
4445+
stubs.push(stub);
4446+
4447+
const requestHandler = handler.init(mockApp) as AuthRequestHandler;
4448+
return requestHandler.getProjectConfig()
4449+
.then(() => {
4450+
throw new Error('Unexpected success');
4451+
}, (error) => {
4452+
expect(error).to.have.property('code', 'auth/internal-error');
4453+
expect(error.message).to.equal('INTERNAL ASSERT FAILED: Unable to get project config');
4454+
});
4455+
});
4456+
4457+
it('should be fulfilled when the response is missing the name field and the emulator is running', () => {
4458+
const emulatorHost = 'localhost:9099';
4459+
process.env.FIREBASE_AUTH_EMULATOR_HOST = emulatorHost;
4460+
const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({}));
4461+
stubs.push(stub);
4462+
4463+
const requestHandler = handler.init(mockApp) as AuthRequestHandler;
4464+
return requestHandler.getProjectConfig()
4465+
.then((result) => {
4466+
expect(result).to.deep.equal({});
4467+
expect(stub).to.have.been.calledOnce.and.calledWith({
4468+
method,
4469+
url: `http://${emulatorHost}/identitytoolkit.googleapis.com${path}`,
4470+
headers: expectedHeadersEmulator,
4471+
data: {},
4472+
timeout,
4473+
});
4474+
});
4475+
});
4476+
});
4477+
4478+
describe('updateProjectConfig', () => {
4479+
const path = '/v2/projects/project_id/config';
4480+
const method = 'PATCH';
4481+
const validRequest: UpdateProjectConfigRequest = {
4482+
smsRegionConfig: {
4483+
allowlistOnly: {
4484+
allowedRegions: ['AC', 'AD'],
4485+
},
4486+
},
4487+
};
4488+
const expectedPath = path + '?updateMask=smsRegionConfig.allowlistOnly.allowedRegions';
4489+
const expectedResult = utils.responseFrom({
4490+
name: 'projects/project_id/config',
4491+
});
4492+
4493+
afterEach(() => {
4494+
delete process.env.FIREBASE_AUTH_EMULATOR_HOST;
4495+
});
4496+
4497+
it('should be fulfilled with the updated project config response', () => {
4498+
const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult);
4499+
stubs.push(stub);
4500+
4501+
const requestHandler = handler.init(mockApp) as AuthRequestHandler;
4502+
return requestHandler.updateProjectConfig(validRequest)
4503+
.then((result) => {
4504+
expect(result).to.deep.equal(expectedResult.data);
4505+
expect(stub).to.have.been.calledOnce.and.calledWith(
4506+
callParams(expectedPath, method, validRequest));
4507+
});
4508+
});
4509+
4510+
it('should be rejected when the response is missing the name field', () => {
4511+
const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({}));
4512+
stubs.push(stub);
4513+
4514+
const requestHandler = handler.init(mockApp) as AuthRequestHandler;
4515+
return requestHandler.updateProjectConfig(validRequest)
4516+
.then(() => {
4517+
throw new Error('Unexpected success');
4518+
}, (error) => {
4519+
expect(error).to.have.property('code', 'auth/internal-error');
4520+
expect(error.message).to.equal('INTERNAL ASSERT FAILED: Unable to update project config');
4521+
});
4522+
});
4523+
4524+
it('should be fulfilled when the response is missing the name field and the emulator is running', () => {
4525+
const emulatorHost = 'localhost:9099';
4526+
process.env.FIREBASE_AUTH_EMULATOR_HOST = emulatorHost;
4527+
const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({}));
4528+
stubs.push(stub);
4529+
4530+
const requestHandler = handler.init(mockApp) as AuthRequestHandler;
4531+
return requestHandler.updateProjectConfig(validRequest)
4532+
.then((result) => {
4533+
expect(result).to.deep.equal({});
4534+
expect(stub).to.have.been.calledOnce.and.calledWith({
4535+
method,
4536+
url: `http://${emulatorHost}/identitytoolkit.googleapis.com${expectedPath}`,
4537+
headers: expectedHeadersEmulator,
4538+
data: validRequest,
4539+
timeout,
4540+
});
4541+
});
4542+
});
4543+
});
4544+
44204545
describe('getTenant', () => {
44214546
const path = '/v2/projects/project_id/tenants/tenant-id';
44224547
const method = 'GET';

0 commit comments

Comments
 (0)