Skip to content

Commit 994fd43

Browse files
authored
feat(rtdb): Support emulator mode for rules management operations (#1190)
* feat(rtdb): Support emulator mode for rules management operations * fix: Adding namespace to emulated URL string * fix: Consolidated unit testing * fix: Removed extra whitespace
1 parent 6bcffa2 commit 994fd43

File tree

2 files changed

+125
-15
lines changed

2 files changed

+125
-15
lines changed

src/database/database-internal.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export class DatabaseService {
6363
/**
6464
* Returns the app associated with this DatabaseService instance.
6565
*
66-
* @return {FirebaseApp} The app associated with this DatabaseService instance.
66+
* @return The app associated with this DatabaseService instance.
6767
*/
6868
get app(): FirebaseApp {
6969
return this.appInternal;
@@ -123,7 +123,13 @@ class DatabaseRulesClient {
123123
private readonly httpClient: AuthorizedHttpClient;
124124

125125
constructor(app: FirebaseApp, dbUrl: string) {
126-
const parsedUrl = new URL(dbUrl);
126+
let parsedUrl = new URL(dbUrl);
127+
const emulatorHost = process.env.FIREBASE_DATABASE_EMULATOR_HOST;
128+
if (emulatorHost) {
129+
const namespace = extractNamespace(parsedUrl);
130+
parsedUrl = new URL(`http://${emulatorHost}?ns=${namespace}`);
131+
}
132+
127133
parsedUrl.pathname = path.join(parsedUrl.pathname, RULES_URL_PATH);
128134
this.dbUrl = parsedUrl.toString();
129135
this.httpClient = new AuthorizedHttpClient(app);
@@ -133,7 +139,7 @@ class DatabaseRulesClient {
133139
* Gets the currently applied security rules as a string. The return value consists of
134140
* the rules source including comments.
135141
*
136-
* @return {Promise<string>} A promise fulfilled with the rules as a raw string.
142+
* @return A promise fulfilled with the rules as a raw string.
137143
*/
138144
public getRules(): Promise<string> {
139145
const req: HttpRequestConfig = {
@@ -233,3 +239,14 @@ class DatabaseRulesClient {
233239
return `${intro}: ${err.response.text}`;
234240
}
235241
}
242+
243+
function extractNamespace(parsedUrl: URL): string {
244+
const ns = parsedUrl.searchParams.get('ns');
245+
if (ns) {
246+
return ns;
247+
}
248+
249+
const hostname = parsedUrl.hostname;
250+
const dotIndex = hostname.indexOf('.');
251+
return hostname.substring(0, dotIndex).toLowerCase();
252+
}

test/unit/database/database.spec.ts

Lines changed: 105 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ describe('Database', () => {
4848
describe('Constructor', () => {
4949
const invalidApps = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop];
5050
invalidApps.forEach((invalidApp) => {
51-
it(`should throw given invalid app: ${ JSON.stringify(invalidApp) }`, () => {
51+
it(`should throw given invalid app: ${JSON.stringify(invalidApp)}`, () => {
5252
expect(() => {
5353
const databaseAny: any = DatabaseService;
5454
return new databaseAny(invalidApp);
@@ -154,11 +154,8 @@ describe('Database', () => {
154154
}`;
155155
const rulesPath = '.settings/rules.json';
156156

157-
function callParamsForGet(
158-
strict = false,
159-
url = `https://databasename.firebaseio.com/${rulesPath}`,
160-
): HttpRequestConfig {
161-
157+
function callParamsForGet(options?: { strict?: boolean; url?: string }): HttpRequestConfig {
158+
const url = options?.url || `https://databasename.firebaseio.com/${rulesPath}`;
162159
const params: HttpRequestConfig = {
163160
method: 'GET',
164161
url,
@@ -167,7 +164,7 @@ describe('Database', () => {
167164
},
168165
};
169166

170-
if (strict) {
167+
if (options?.strict) {
171168
params.data = { format: 'strict' };
172169
}
173170

@@ -215,7 +212,7 @@ describe('Database', () => {
215212
return db.getRules().then((result) => {
216213
expect(result).to.equal(rulesString);
217214
return expect(stub).to.have.been.calledOnce.and.calledWith(
218-
callParamsForGet(false, `https://custom.firebaseio.com/${rulesPath}`));
215+
callParamsForGet({ url: `https://custom.firebaseio.com/${rulesPath}` }));
219216
});
220217
});
221218

@@ -225,7 +222,7 @@ describe('Database', () => {
225222
return db.getRules().then((result) => {
226223
expect(result).to.equal(rulesString);
227224
return expect(stub).to.have.been.calledOnce.and.calledWith(
228-
callParamsForGet(false, `http://localhost:9000/${rulesPath}?ns=foo`));
225+
callParamsForGet({ url: `http://localhost:9000/${rulesPath}?ns=foo` }));
229226
});
230227
});
231228

@@ -259,7 +256,7 @@ describe('Database', () => {
259256
return db.getRulesJSON().then((result) => {
260257
expect(result).to.deep.equal(rules);
261258
return expect(stub).to.have.been.calledOnce.and.calledWith(
262-
callParamsForGet(true));
259+
callParamsForGet({ strict: true }));
263260
});
264261
});
265262

@@ -269,7 +266,7 @@ describe('Database', () => {
269266
return db.getRulesJSON().then((result) => {
270267
expect(result).to.deep.equal(rules);
271268
return expect(stub).to.have.been.calledOnce.and.calledWith(
272-
callParamsForGet(true, `https://custom.firebaseio.com/${rulesPath}`));
269+
callParamsForGet({ strict: true, url: `https://custom.firebaseio.com/${rulesPath}` }));
273270
});
274271
});
275272

@@ -279,7 +276,7 @@ describe('Database', () => {
279276
return db.getRulesJSON().then((result) => {
280277
expect(result).to.deep.equal(rules);
281278
return expect(stub).to.have.been.calledOnce.and.calledWith(
282-
callParamsForGet(true, `http://localhost:9000/${rulesPath}?ns=foo`));
279+
callParamsForGet({ strict: true, url: `http://localhost:9000/${rulesPath}?ns=foo` }));
283280
});
284281
});
285282

@@ -409,5 +406,101 @@ describe('Database', () => {
409406
return db.setRules(rules).should.eventually.be.rejectedWith('network error');
410407
});
411408
});
409+
410+
describe('emulator mode', () => {
411+
interface EmulatorTestConfig {
412+
name: string;
413+
setUp: () => FirebaseApp;
414+
tearDown?: () => void;
415+
url: string;
416+
}
417+
418+
const configs: EmulatorTestConfig[] = [
419+
{
420+
name: 'with environment variable',
421+
setUp: () => {
422+
process.env.FIREBASE_DATABASE_EMULATOR_HOST = 'localhost:9090';
423+
return mocks.app();
424+
},
425+
tearDown: () => {
426+
delete process.env.FIREBASE_DATABASE_EMULATOR_HOST;
427+
},
428+
url: `http://localhost:9090/${rulesPath}?ns=databasename`,
429+
},
430+
{
431+
name: 'with app options',
432+
setUp: () => {
433+
return mocks.appWithOptions({
434+
databaseURL: 'http://localhost:9091?ns=databasename',
435+
});
436+
},
437+
url: `http://localhost:9091/${rulesPath}?ns=databasename`,
438+
},
439+
{
440+
name: 'with environment variable overriding app options',
441+
setUp: () => {
442+
process.env.FIREBASE_DATABASE_EMULATOR_HOST = 'localhost:9090';
443+
return mocks.appWithOptions({
444+
databaseURL: 'http://localhost:9091?ns=databasename',
445+
});
446+
},
447+
tearDown: () => {
448+
delete process.env.FIREBASE_DATABASE_EMULATOR_HOST;
449+
},
450+
url: `http://localhost:9090/${rulesPath}?ns=databasename`,
451+
},
452+
];
453+
454+
configs.forEach((config) => {
455+
describe(config.name, () => {
456+
let emulatorApp: FirebaseApp;
457+
let emulatorDatabase: DatabaseService;
458+
459+
before(() => {
460+
emulatorApp = config.setUp();
461+
emulatorDatabase = new DatabaseService(emulatorApp);
462+
});
463+
464+
after(() => {
465+
if (config.tearDown) {
466+
config.tearDown();
467+
}
468+
469+
return emulatorDatabase.delete().then(() => {
470+
return emulatorApp.delete();
471+
});
472+
});
473+
474+
it('getRules should connect to the emulator', () => {
475+
const db: Database = emulatorDatabase.getDatabase();
476+
const stub = stubSuccessfulResponse(rules);
477+
return db.getRules().then((result) => {
478+
expect(result).to.equal(rulesString);
479+
return expect(stub).to.have.been.calledOnce.and.calledWith(
480+
callParamsForGet({ url: config.url }));
481+
});
482+
});
483+
484+
it('getRulesJSON should connect to the emulator', () => {
485+
const db: Database = emulatorDatabase.getDatabase();
486+
const stub = stubSuccessfulResponse(rules);
487+
return db.getRulesJSON().then((result) => {
488+
expect(result).to.equal(rules);
489+
return expect(stub).to.have.been.calledOnce.and.calledWith(
490+
callParamsForGet({ strict: true, url: config.url }));
491+
});
492+
});
493+
494+
it('setRules should connect to the emulator', () => {
495+
const db: Database = emulatorDatabase.getDatabase();
496+
const stub = stubSuccessfulResponse({});
497+
return db.setRules(rulesString).then(() => {
498+
return expect(stub).to.have.been.calledOnce.and.calledWith(
499+
callParamsForPut(rulesString, config.url));
500+
});
501+
});
502+
});
503+
});
504+
});
412505
});
413506
});

0 commit comments

Comments
 (0)