From 8427233759ce4c9908e22e3000784c02e0b2da98 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 8 Mar 2021 12:42:05 -0800 Subject: [PATCH 1/4] feat(rtdb): Support emulator mode for rules management operations --- src/database/database-internal.ts | 9 ++++- test/unit/database/database.spec.ts | 60 +++++++++++++++++++++++------ 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/src/database/database-internal.ts b/src/database/database-internal.ts index a469a41773..be679f0947 100644 --- a/src/database/database-internal.ts +++ b/src/database/database-internal.ts @@ -63,7 +63,7 @@ export class DatabaseService { /** * Returns the app associated with this DatabaseService instance. * - * @return {FirebaseApp} The app associated with this DatabaseService instance. + * @return The app associated with this DatabaseService instance. */ get app(): FirebaseApp { return this.appInternal; @@ -123,6 +123,11 @@ class DatabaseRulesClient { private readonly httpClient: AuthorizedHttpClient; constructor(app: FirebaseApp, dbUrl: string) { + const emulatorHost = process.env.FIREBASE_DATABASE_EMULATOR_HOST; + if (emulatorHost) { + dbUrl = `http://${emulatorHost}`; + } + const parsedUrl = new URL(dbUrl); parsedUrl.pathname = path.join(parsedUrl.pathname, RULES_URL_PATH); this.dbUrl = parsedUrl.toString(); @@ -133,7 +138,7 @@ class DatabaseRulesClient { * Gets the currently applied security rules as a string. The return value consists of * the rules source including comments. * - * @return {Promise} A promise fulfilled with the rules as a raw string. + * @return A promise fulfilled with the rules as a raw string. */ public getRules(): Promise { const req: HttpRequestConfig = { diff --git a/test/unit/database/database.spec.ts b/test/unit/database/database.spec.ts index 4a4f969b1e..9d18b42c57 100644 --- a/test/unit/database/database.spec.ts +++ b/test/unit/database/database.spec.ts @@ -48,7 +48,7 @@ describe('Database', () => { describe('Constructor', () => { const invalidApps = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; invalidApps.forEach((invalidApp) => { - it(`should throw given invalid app: ${ JSON.stringify(invalidApp) }`, () => { + it(`should throw given invalid app: ${JSON.stringify(invalidApp)}`, () => { expect(() => { const databaseAny: any = DatabaseService; return new databaseAny(invalidApp); @@ -154,11 +154,8 @@ describe('Database', () => { }`; const rulesPath = '.settings/rules.json'; - function callParamsForGet( - strict = false, - url = `https://databasename.firebaseio.com/${rulesPath}`, - ): HttpRequestConfig { - + function callParamsForGet(options?: { strict?: boolean; url?: string }): HttpRequestConfig { + const url = options?.url || `https://databasename.firebaseio.com/${rulesPath}`; const params: HttpRequestConfig = { method: 'GET', url, @@ -167,7 +164,7 @@ describe('Database', () => { }, }; - if (strict) { + if (options?.strict) { params.data = { format: 'strict' }; } @@ -215,7 +212,7 @@ describe('Database', () => { return db.getRules().then((result) => { expect(result).to.equal(rulesString); return expect(stub).to.have.been.calledOnce.and.calledWith( - callParamsForGet(false, `https://custom.firebaseio.com/${rulesPath}`)); + callParamsForGet({ url: `https://custom.firebaseio.com/${rulesPath}` })); }); }); @@ -225,7 +222,7 @@ describe('Database', () => { return db.getRules().then((result) => { expect(result).to.equal(rulesString); return expect(stub).to.have.been.calledOnce.and.calledWith( - callParamsForGet(false, `http://localhost:9000/${rulesPath}?ns=foo`)); + callParamsForGet({ url: `http://localhost:9000/${rulesPath}?ns=foo` })); }); }); @@ -259,7 +256,7 @@ describe('Database', () => { return db.getRulesJSON().then((result) => { expect(result).to.deep.equal(rules); return expect(stub).to.have.been.calledOnce.and.calledWith( - callParamsForGet(true)); + callParamsForGet({ strict: true })); }); }); @@ -269,7 +266,7 @@ describe('Database', () => { return db.getRulesJSON().then((result) => { expect(result).to.deep.equal(rules); return expect(stub).to.have.been.calledOnce.and.calledWith( - callParamsForGet(true, `https://custom.firebaseio.com/${rulesPath}`)); + callParamsForGet({ strict: true, url: `https://custom.firebaseio.com/${rulesPath}` })); }); }); @@ -279,7 +276,7 @@ describe('Database', () => { return db.getRulesJSON().then((result) => { expect(result).to.deep.equal(rules); return expect(stub).to.have.been.calledOnce.and.calledWith( - callParamsForGet(true, `http://localhost:9000/${rulesPath}?ns=foo`)); + callParamsForGet({ strict: true, url: `http://localhost:9000/${rulesPath}?ns=foo` })); }); }); @@ -409,5 +406,44 @@ describe('Database', () => { return db.setRules(rules).should.eventually.be.rejectedWith('network error'); }); }); + + describe('emulator mode', () => { + before(() => { + process.env.FIREBASE_DATABASE_EMULATOR_HOST = 'localhost:9090'; + }); + + after(() => { + delete process.env.FIREBASE_DATABASE_EMULATOR_HOST; + }); + + it('getRules should connect to the emulator', () => { + const db: Database = database.getDatabase(); + const stub = stubSuccessfulResponse(rules); + return db.getRules().then((result) => { + expect(result).to.equal(rulesString); + return expect(stub).to.have.been.calledOnce.and.calledWith( + callParamsForGet({ url: `http://localhost:9090/${rulesPath}` })); + }); + }); + + it('getRulesJSON should connect to the emulator', () => { + const db: Database = database.getDatabase(); + const stub = stubSuccessfulResponse(rules); + return db.getRulesJSON().then((result) => { + expect(result).to.equal(rules); + return expect(stub).to.have.been.calledOnce.and.calledWith( + callParamsForGet({ strict: true, url: `http://localhost:9090/${rulesPath}` })); + }); + }); + + it('setRules should connect to the emulator', () => { + const db: Database = database.getDatabase(); + const stub = stubSuccessfulResponse({}); + return db.setRules(rulesString).then(() => { + return expect(stub).to.have.been.calledOnce.and.calledWith( + callParamsForPut(rulesString, `http://localhost:9090/${rulesPath}`)); + }); + }); + }); }); }); From aeb6043314ff5cd42e054f394e118eb3eef5af07 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 8 Mar 2021 13:20:54 -0800 Subject: [PATCH 2/4] fix: Adding namespace to emulated URL string --- src/database/database-internal.ts | 7 +- test/unit/database/database.spec.ts | 105 +++++++++++++++++++++------- 2 files changed, 83 insertions(+), 29 deletions(-) diff --git a/src/database/database-internal.ts b/src/database/database-internal.ts index be679f0947..e8c7a174a8 100644 --- a/src/database/database-internal.ts +++ b/src/database/database-internal.ts @@ -123,12 +123,15 @@ class DatabaseRulesClient { private readonly httpClient: AuthorizedHttpClient; constructor(app: FirebaseApp, dbUrl: string) { + let parsedUrl = new URL(dbUrl); const emulatorHost = process.env.FIREBASE_DATABASE_EMULATOR_HOST; if (emulatorHost) { - dbUrl = `http://${emulatorHost}`; + const hostname = parsedUrl.hostname; + const dotIndex = hostname.indexOf('.'); + const namespace = hostname.substring(0, dotIndex).toLowerCase(); + parsedUrl = new URL(`http://${emulatorHost}?ns=${namespace}`); } - const parsedUrl = new URL(dbUrl); parsedUrl.pathname = path.join(parsedUrl.pathname, RULES_URL_PATH); this.dbUrl = parsedUrl.toString(); this.httpClient = new AuthorizedHttpClient(app); diff --git a/test/unit/database/database.spec.ts b/test/unit/database/database.spec.ts index 9d18b42c57..519087d412 100644 --- a/test/unit/database/database.spec.ts +++ b/test/unit/database/database.spec.ts @@ -408,40 +408,91 @@ describe('Database', () => { }); describe('emulator mode', () => { - before(() => { - process.env.FIREBASE_DATABASE_EMULATOR_HOST = 'localhost:9090'; - }); + const namespacedRulesPath = `${rulesPath}?ns=databasename`; - after(() => { - delete process.env.FIREBASE_DATABASE_EMULATOR_HOST; - }); + describe('from environment variable', () => { + before(() => { + process.env.FIREBASE_DATABASE_EMULATOR_HOST = 'localhost:9090'; + }); - it('getRules should connect to the emulator', () => { - const db: Database = database.getDatabase(); - const stub = stubSuccessfulResponse(rules); - return db.getRules().then((result) => { - expect(result).to.equal(rulesString); - return expect(stub).to.have.been.calledOnce.and.calledWith( - callParamsForGet({ url: `http://localhost:9090/${rulesPath}` })); + after(() => { + delete process.env.FIREBASE_DATABASE_EMULATOR_HOST; }); - }); - it('getRulesJSON should connect to the emulator', () => { - const db: Database = database.getDatabase(); - const stub = stubSuccessfulResponse(rules); - return db.getRulesJSON().then((result) => { - expect(result).to.equal(rules); - return expect(stub).to.have.been.calledOnce.and.calledWith( - callParamsForGet({ strict: true, url: `http://localhost:9090/${rulesPath}` })); + it('getRules should connect to the emulator', () => { + const db: Database = database.getDatabase(); + const stub = stubSuccessfulResponse(rules); + return db.getRules().then((result) => { + expect(result).to.equal(rulesString); + return expect(stub).to.have.been.calledOnce.and.calledWith( + callParamsForGet({ url: `http://localhost:9090/${namespacedRulesPath}` })); + }); + }); + + it('getRulesJSON should connect to the emulator', () => { + const db: Database = database.getDatabase(); + const stub = stubSuccessfulResponse(rules); + return db.getRulesJSON().then((result) => { + expect(result).to.equal(rules); + return expect(stub).to.have.been.calledOnce.and.calledWith( + callParamsForGet({ strict: true, url: `http://localhost:9090/${namespacedRulesPath}` })); + }); + }); + + it('setRules should connect to the emulator', () => { + const db: Database = database.getDatabase(); + const stub = stubSuccessfulResponse({}); + return db.setRules(rulesString).then(() => { + return expect(stub).to.have.been.calledOnce.and.calledWith( + callParamsForPut(rulesString, `http://localhost:9090/${namespacedRulesPath}`)); + }); }); }); - it('setRules should connect to the emulator', () => { - const db: Database = database.getDatabase(); - const stub = stubSuccessfulResponse({}); - return db.setRules(rulesString).then(() => { - return expect(stub).to.have.been.calledOnce.and.calledWith( - callParamsForPut(rulesString, `http://localhost:9090/${rulesPath}`)); + describe('from app options', () => { + let emulatorApp: FirebaseApp; + let emulatorDatabase: DatabaseService; + + beforeEach(() => { + emulatorApp = mocks.appWithOptions({ + databaseURL: 'http://localhost:9091?ns=databasename', + }); + emulatorDatabase = new DatabaseService(emulatorApp); + }); + + afterEach(() => { + return emulatorDatabase.delete().then(() => { + return emulatorApp.delete(); + }); + }); + + it('getRules should connect to the emulator', () => { + const db: Database = emulatorDatabase.getDatabase(); + const stub = stubSuccessfulResponse(rules); + return db.getRules().then((result) => { + expect(result).to.equal(rulesString); + return expect(stub).to.have.been.calledOnce.and.calledWith( + callParamsForGet({ url: `http://localhost:9091/${namespacedRulesPath}` })); + }); + }); + + it('getRulesJSON should connect to the emulator', () => { + const db: Database = emulatorDatabase.getDatabase(); + const stub = stubSuccessfulResponse(rules); + return db.getRulesJSON().then((result) => { + expect(result).to.equal(rules); + return expect(stub).to.have.been.calledOnce.and.calledWith( + callParamsForGet({ strict: true, url: `http://localhost:9091/${namespacedRulesPath}` })); + }); + }); + + it('setRules should connect to the emulator', () => { + const db: Database = emulatorDatabase.getDatabase(); + const stub = stubSuccessfulResponse({}); + return db.setRules(rulesString).then(() => { + return expect(stub).to.have.been.calledOnce.and.calledWith( + callParamsForPut(rulesString, `http://localhost:9091/${namespacedRulesPath}`)); + }); }); }); }); From f20ac9ff07053c0e50beb98a4ce7598ceb19dfa3 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 8 Mar 2021 15:06:08 -0800 Subject: [PATCH 3/4] fix: Consolidated unit testing --- src/database/database-internal.ts | 15 ++- test/unit/database/database.spec.ts | 150 +++++++++++++++------------- 2 files changed, 90 insertions(+), 75 deletions(-) diff --git a/src/database/database-internal.ts b/src/database/database-internal.ts index e8c7a174a8..a079c94bca 100644 --- a/src/database/database-internal.ts +++ b/src/database/database-internal.ts @@ -126,9 +126,7 @@ class DatabaseRulesClient { let parsedUrl = new URL(dbUrl); const emulatorHost = process.env.FIREBASE_DATABASE_EMULATOR_HOST; if (emulatorHost) { - const hostname = parsedUrl.hostname; - const dotIndex = hostname.indexOf('.'); - const namespace = hostname.substring(0, dotIndex).toLowerCase(); + const namespace = extractNamespace(parsedUrl); parsedUrl = new URL(`http://${emulatorHost}?ns=${namespace}`); } @@ -241,3 +239,14 @@ class DatabaseRulesClient { return `${intro}: ${err.response.text}`; } } + +function extractNamespace(parsedUrl: URL): string { + const ns = parsedUrl.searchParams.get('ns'); + if (ns) { + return ns; + } + + const hostname = parsedUrl.hostname; + const dotIndex = hostname.indexOf('.'); + return hostname.substring(0, dotIndex).toLowerCase(); +} diff --git a/test/unit/database/database.spec.ts b/test/unit/database/database.spec.ts index 519087d412..3979719ab6 100644 --- a/test/unit/database/database.spec.ts +++ b/test/unit/database/database.spec.ts @@ -408,90 +408,96 @@ describe('Database', () => { }); describe('emulator mode', () => { - const namespacedRulesPath = `${rulesPath}?ns=databasename`; + interface EmulatorTestConfig { + name: string; + setUp: () => FirebaseApp; + tearDown?: () => void; + url: string; + } - describe('from environment variable', () => { - before(() => { - process.env.FIREBASE_DATABASE_EMULATOR_HOST = 'localhost:9090'; - }); + const configs: EmulatorTestConfig[] = [ + { + name: 'with environment variable', + setUp: () => { + process.env.FIREBASE_DATABASE_EMULATOR_HOST = 'localhost:9090'; + return mocks.app(); + }, + tearDown: () => { + delete process.env.FIREBASE_DATABASE_EMULATOR_HOST; + }, + url: `http://localhost:9090/${rulesPath}?ns=databasename`, + }, + { + name: 'with app options', + setUp: () => { + return mocks.appWithOptions({ + databaseURL: 'http://localhost:9091?ns=databasename', + }); + }, + url: `http://localhost:9091/${rulesPath}?ns=databasename`, + }, + { + name: 'with environment variable overriding app options', + setUp: () => { + process.env.FIREBASE_DATABASE_EMULATOR_HOST = 'localhost:9090'; + return mocks.appWithOptions({ + databaseURL: 'http://localhost:9091?ns=databasename', + }); + }, + tearDown: () => { + delete process.env.FIREBASE_DATABASE_EMULATOR_HOST; + }, + url: `http://localhost:9090/${rulesPath}?ns=databasename`, + }, + ]; - after(() => { - delete process.env.FIREBASE_DATABASE_EMULATOR_HOST; - }); + configs.forEach((config) => { + describe(config.name, () => { + let emulatorApp: FirebaseApp; + let emulatorDatabase: DatabaseService; - it('getRules should connect to the emulator', () => { - const db: Database = database.getDatabase(); - const stub = stubSuccessfulResponse(rules); - return db.getRules().then((result) => { - expect(result).to.equal(rulesString); - return expect(stub).to.have.been.calledOnce.and.calledWith( - callParamsForGet({ url: `http://localhost:9090/${namespacedRulesPath}` })); + before(() => { + emulatorApp = config.setUp(); + emulatorDatabase = new DatabaseService(emulatorApp); }); - }); - it('getRulesJSON should connect to the emulator', () => { - const db: Database = database.getDatabase(); - const stub = stubSuccessfulResponse(rules); - return db.getRulesJSON().then((result) => { - expect(result).to.equal(rules); - return expect(stub).to.have.been.calledOnce.and.calledWith( - callParamsForGet({ strict: true, url: `http://localhost:9090/${namespacedRulesPath}` })); - }); - }); + after(() => { + if (config.tearDown) { + config.tearDown(); + } - it('setRules should connect to the emulator', () => { - const db: Database = database.getDatabase(); - const stub = stubSuccessfulResponse({}); - return db.setRules(rulesString).then(() => { - return expect(stub).to.have.been.calledOnce.and.calledWith( - callParamsForPut(rulesString, `http://localhost:9090/${namespacedRulesPath}`)); + return emulatorDatabase.delete().then(() => { + return emulatorApp.delete(); + }); }); - }); - }); - - describe('from app options', () => { - let emulatorApp: FirebaseApp; - let emulatorDatabase: DatabaseService; - beforeEach(() => { - emulatorApp = mocks.appWithOptions({ - databaseURL: 'http://localhost:9091?ns=databasename', + it('getRules should connect to the emulator', () => { + const db: Database = emulatorDatabase.getDatabase(); + const stub = stubSuccessfulResponse(rules); + return db.getRules().then((result) => { + expect(result).to.equal(rulesString); + return expect(stub).to.have.been.calledOnce.and.calledWith( + callParamsForGet({ url: config.url })); + }); }); - emulatorDatabase = new DatabaseService(emulatorApp); - }); - afterEach(() => { - return emulatorDatabase.delete().then(() => { - return emulatorApp.delete(); + it('getRulesJSON should connect to the emulator', () => { + const db: Database = emulatorDatabase.getDatabase(); + const stub = stubSuccessfulResponse(rules); + return db.getRulesJSON().then((result) => { + expect(result).to.equal(rules); + return expect(stub).to.have.been.calledOnce.and.calledWith( + callParamsForGet({ strict: true, url: config.url })); + }); }); - }); - - it('getRules should connect to the emulator', () => { - const db: Database = emulatorDatabase.getDatabase(); - const stub = stubSuccessfulResponse(rules); - return db.getRules().then((result) => { - expect(result).to.equal(rulesString); - return expect(stub).to.have.been.calledOnce.and.calledWith( - callParamsForGet({ url: `http://localhost:9091/${namespacedRulesPath}` })); - }); - }); - - it('getRulesJSON should connect to the emulator', () => { - const db: Database = emulatorDatabase.getDatabase(); - const stub = stubSuccessfulResponse(rules); - return db.getRulesJSON().then((result) => { - expect(result).to.equal(rules); - return expect(stub).to.have.been.calledOnce.and.calledWith( - callParamsForGet({ strict: true, url: `http://localhost:9091/${namespacedRulesPath}` })); - }); - }); - it('setRules should connect to the emulator', () => { - const db: Database = emulatorDatabase.getDatabase(); - const stub = stubSuccessfulResponse({}); - return db.setRules(rulesString).then(() => { - return expect(stub).to.have.been.calledOnce.and.calledWith( - callParamsForPut(rulesString, `http://localhost:9091/${namespacedRulesPath}`)); + it('setRules should connect to the emulator', () => { + const db: Database = emulatorDatabase.getDatabase(); + const stub = stubSuccessfulResponse({}); + return db.setRules(rulesString).then(() => { + return expect(stub).to.have.been.calledOnce.and.calledWith( + callParamsForPut(rulesString, config.url)); + }); }); }); }); From 329527d4535c469c9d301a7b583a53622f586989 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 10 Mar 2021 12:07:27 -0800 Subject: [PATCH 4/4] fix: Removed extra whitespace --- src/database/database-internal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/database/database-internal.ts b/src/database/database-internal.ts index a079c94bca..b77f536b97 100644 --- a/src/database/database-internal.ts +++ b/src/database/database-internal.ts @@ -63,7 +63,7 @@ export class DatabaseService { /** * Returns the app associated with this DatabaseService instance. * - * @return The app associated with this DatabaseService instance. + * @return The app associated with this DatabaseService instance. */ get app(): FirebaseApp { return this.appInternal;