Skip to content

Commit 97f26b7

Browse files
authored
Add port configuration and discovery to rules-unit-testing (#4388)
1 parent 3d0cd6f commit 97f26b7

File tree

4 files changed

+157
-8
lines changed

4 files changed

+157
-8
lines changed

.changeset/serious-poems-wave.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@firebase/rules-unit-testing': minor
3+
---
4+
5+
Add port configuration and discovery methods to rules-unit-testing.

packages/rules-unit-testing/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@ export {
2525
apps,
2626
assertFails,
2727
assertSucceeds,
28+
discoverEmulators,
2829
clearFirestoreData,
2930
database,
3031
firestore,
3132
initializeAdminApp,
3233
initializeTestApp,
3334
loadDatabaseRules,
3435
loadFirestoreRules,
36+
useEmulators,
3537
withFunctionTriggersDisabled
3638
} from './src/api';

packages/rules-unit-testing/src/api/index.ts

Lines changed: 131 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const FIRESTORE_ADDRESS_DEFAULT: string = 'localhost:8080';
3838

3939
/** Environment variable to locate the Emulator Hub */
4040
const HUB_HOST_ENV: string = 'FIREBASE_EMULATOR_HUB';
41-
/** The default address for the Emulator hub */
41+
/** The default address for the Emulator Hub */
4242
const HUB_HOST_DEFAULT: string = 'localhost:4400';
4343

4444
/** The actual address for the database emulator */
@@ -47,6 +47,9 @@ let _databaseHost: string | undefined = undefined;
4747
/** The actual address for the Firestore emulator */
4848
let _firestoreHost: string | undefined = undefined;
4949

50+
/** The actual address for the Emulator Hub */
51+
let _hubHost: string | undefined = undefined;
52+
5053
export type Provider =
5154
| 'custom'
5255
| 'email'
@@ -118,6 +121,24 @@ export type FirebaseIdToken = {
118121
// new users should prefer 'sub' instead.
119122
export type TokenOptions = Partial<FirebaseIdToken> & { uid?: string };
120123

124+
/**
125+
* Host/port configuration for applicable Firebase Emulators.
126+
*/
127+
export type FirebaseEmulatorOptions = {
128+
firestore?: {
129+
host: string;
130+
port: number;
131+
};
132+
database?: {
133+
host: string;
134+
port: number;
135+
};
136+
hub?: {
137+
host: string;
138+
port: number;
139+
};
140+
};
141+
121142
function createUnsecuredJwt(token: TokenOptions, projectId?: string): string {
122143
// Unsecured JWTs use "none" as the algorithm.
123144
const header = {
@@ -206,6 +227,98 @@ export function initializeAdminApp(options: AdminAppOptions): firebase.app.App {
206227
return app;
207228
}
208229

230+
/**
231+
* Set the host and port configuration for applicable emulators. This will override any values
232+
* found in environment variables. Must be called before initializeAdminApp or initializeTestApp.
233+
*
234+
* @param options options object.
235+
*/
236+
export function useEmulators(options: FirebaseEmulatorOptions): void {
237+
if (!(options.database || options.firestore || options.hub)) {
238+
throw new Error(
239+
"Argument to useEmulators must contain at least one of 'database', 'firestore', or 'hub'."
240+
);
241+
}
242+
243+
if (options.database) {
244+
_databaseHost = getAddress(options.database.host, options.database.port);
245+
}
246+
247+
if (options.firestore) {
248+
_firestoreHost = getAddress(options.firestore.host, options.firestore.port);
249+
}
250+
251+
if (options.hub) {
252+
_hubHost = getAddress(options.hub.host, options.hub.port);
253+
}
254+
}
255+
256+
/**
257+
* Use the Firebase Emulator hub to discover other running emulators. Call useEmulators() with
258+
* the result to configure the library to use the discovered emulators.
259+
*
260+
* @param hubHost the host where the Emulator Hub is running (ex: 'localhost')
261+
* @param hubPort the port where the Emulator Hub is running (ex: 4400)
262+
*/
263+
export async function discoverEmulators(
264+
hubHost?: string,
265+
hubPort?: number
266+
): Promise<FirebaseEmulatorOptions> {
267+
if ((hubHost && !hubPort) || (!hubHost && hubPort)) {
268+
throw new Error(
269+
`Invalid configuration hubHost=${hubHost} and hubPort=${hubPort}. If either parameter is supplied, both must be defined.`
270+
);
271+
}
272+
273+
const hubAddress =
274+
hubHost && hubPort ? getAddress(hubHost, hubPort) : getHubHost();
275+
276+
const res = await requestPromise(request.get, {
277+
method: 'GET',
278+
uri: `http://${hubAddress}/emulators`
279+
});
280+
if (res.statusCode !== 200) {
281+
throw new Error(
282+
`HTTP Error ${res.statusCode} when attempting to reach Emulator Hub at ${hubAddress}, are you sure it is running?`
283+
);
284+
}
285+
286+
const options: FirebaseEmulatorOptions = {};
287+
288+
const data = JSON.parse(res.body);
289+
290+
if (data.database) {
291+
options.database = {
292+
host: data.database.host,
293+
port: data.database.port
294+
};
295+
}
296+
297+
if (data.firestore) {
298+
options.firestore = {
299+
host: data.firestore.host,
300+
port: data.firestore.port
301+
};
302+
}
303+
304+
if (data.hub) {
305+
options.hub = {
306+
host: data.hub.host,
307+
port: data.hub.port
308+
};
309+
}
310+
311+
return options;
312+
}
313+
314+
function getAddress(host: string, port: number) {
315+
if (host.includes('::')) {
316+
return `[${host}]:${port}`;
317+
} else {
318+
return `${host}:${port}`;
319+
}
320+
}
321+
209322
function getDatabaseHost() {
210323
if (!_databaseHost) {
211324
const fromEnv = process.env[DATABASE_ADDRESS_ENV];
@@ -238,6 +351,22 @@ function getFirestoreHost() {
238351
return _firestoreHost;
239352
}
240353

354+
function getHubHost() {
355+
if (!_hubHost) {
356+
const fromEnv = process.env[HUB_HOST_ENV];
357+
if (fromEnv) {
358+
_hubHost = fromEnv;
359+
} else {
360+
console.warn(
361+
`Warning: ${HUB_HOST_ENV} not set, using default value ${HUB_HOST_DEFAULT}`
362+
);
363+
_hubHost = HUB_HOST_DEFAULT;
364+
}
365+
}
366+
367+
return _hubHost;
368+
}
369+
241370
function getRandomAppName(): string {
242371
return 'app-' + new Date().getTime() + '-' + Math.random();
243372
}
@@ -406,13 +535,7 @@ export async function clearFirestoreData(
406535
export async function withFunctionTriggersDisabled<TResult>(
407536
fn: () => TResult | Promise<TResult>
408537
): Promise<TResult> {
409-
let hubHost = process.env[HUB_HOST_ENV];
410-
if (!hubHost) {
411-
console.warn(
412-
`${HUB_HOST_ENV} is not set, assuming the Emulator hub is running at ${HUB_HOST_DEFAULT}`
413-
);
414-
hubHost = HUB_HOST_DEFAULT;
415-
}
538+
const hubHost = getHubHost();
416539

417540
// Disable background triggers
418541
const disableRes = await requestPromise(request.put, {

packages/rules-unit-testing/test/database.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,25 @@ describe('Testing Module Tests', function () {
128128
.catch(() => {});
129129
});
130130

131+
it('discoverEmulators() finds all running emulators', async () => {
132+
const options = await firebase.discoverEmulators();
133+
134+
expect(options).to.deep.equal({
135+
database: {
136+
host: 'localhost',
137+
port: 9002
138+
},
139+
firestore: {
140+
host: 'localhost',
141+
port: 9003
142+
},
143+
hub: {
144+
host: 'localhost',
145+
port: 4400
146+
}
147+
});
148+
});
149+
131150
it('initializeTestApp() with auth=null does not set access token', async function () {
132151
const app = firebase.initializeTestApp({
133152
projectId: 'foo',

0 commit comments

Comments
 (0)