Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions src/database/database-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -123,7 +123,13 @@ class DatabaseRulesClient {
private readonly httpClient: AuthorizedHttpClient;

constructor(app: FirebaseApp, dbUrl: string) {
const parsedUrl = new URL(dbUrl);
let parsedUrl = new URL(dbUrl);
const emulatorHost = process.env.FIREBASE_DATABASE_EMULATOR_HOST;
if (emulatorHost) {
const namespace = extractNamespace(parsedUrl);
parsedUrl = new URL(`http://${emulatorHost}?ns=${namespace}`);
}

parsedUrl.pathname = path.join(parsedUrl.pathname, RULES_URL_PATH);
this.dbUrl = parsedUrl.toString();
this.httpClient = new AuthorizedHttpClient(app);
Expand All @@ -133,7 +139,7 @@ class DatabaseRulesClient {
* Gets the currently applied security rules as a string. The return value consists of
* the rules source including comments.
*
* @return {Promise<string>} A promise fulfilled with the rules as a raw string.
* @return A promise fulfilled with the rules as a raw string.
*/
public getRules(): Promise<string> {
const req: HttpRequestConfig = {
Expand Down Expand Up @@ -233,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();
}
117 changes: 105 additions & 12 deletions test/unit/database/database.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand All @@ -167,7 +164,7 @@ describe('Database', () => {
},
};

if (strict) {
if (options?.strict) {
params.data = { format: 'strict' };
}

Expand Down Expand Up @@ -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}` }));
});
});

Expand All @@ -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` }));
});
});

Expand Down Expand Up @@ -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 }));
});
});

Expand All @@ -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}` }));
});
});

Expand All @@ -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` }));
});
});

Expand Down Expand Up @@ -409,5 +406,101 @@ describe('Database', () => {
return db.setRules(rules).should.eventually.be.rejectedWith('network error');
});
});

describe('emulator mode', () => {
interface EmulatorTestConfig {
name: string;
setUp: () => FirebaseApp;
tearDown?: () => void;
url: string;
}

const configs: EmulatorTestConfig[] = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: Consider merging these tests with the tests above. You can likely re-use EmulatorTestConfig (once renamed) and also validate the production URLs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a pretty good idea. However, upon trying it, required quite a bit of changes to the existing tests. Let's revisit this again in a future PR.

{
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`,
},
];

configs.forEach((config) => {
describe(config.name, () => {
let emulatorApp: FirebaseApp;
let emulatorDatabase: DatabaseService;

before(() => {
emulatorApp = config.setUp();
emulatorDatabase = new DatabaseService(emulatorApp);
});

after(() => {
if (config.tearDown) {
config.tearDown();
}

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: config.url }));
});
});

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('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));
});
});
});
});
});
});
});