Skip to content

Commit 66deb25

Browse files
samtsternyuchenshi
andauthored
Storage Emulator support in rules-unit-testing (#4863)
* Storage Emulator support in rules-unit-testing * Minor fixes * Update deps and API * Fix tests * Prettier * Fix database tests * Fix host parsing * Fix typo * Skip storage test in CI * Delete yarn.lock * Revert yarn.lock changes. Co-authored-by: Yuchen Shi <[email protected]>
1 parent 81c131a commit 66deb25

File tree

7 files changed

+232
-33
lines changed

7 files changed

+232
-33
lines changed

.changeset/moody-suits-punch.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 support for Storage emulator to rules-unit-testing

packages/rules-unit-testing/firebase.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
"functions": {
33
"source": "."
44
},
5+
"storage": {
6+
"rules": "test/storage.rules"
7+
},
58
"emulators": {
69
"firestore": {
710
"port": 9003
@@ -12,6 +15,9 @@
1215
"functions": {
1316
"port": 9004
1417
},
18+
"storage": {
19+
"port": 9199
20+
},
1521
"ui": {
1622
"enabled": false
1723
}

packages/rules-unit-testing/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export {
3333
initializeTestApp,
3434
loadDatabaseRules,
3535
loadFirestoreRules,
36+
loadStorageRules,
3637
useEmulators,
3738
withFunctionTriggersDisabled
3839
} from './src/api';

packages/rules-unit-testing/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"build:deps": "lerna run --scope @firebase/rules-unit-testing --include-dependencies build",
1616
"dev": "rollup -c -w",
1717
"test:nyc": "TS_NODE_CACHE=NO TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --reporter lcovonly -- mocha 'test/{,!(browser)/**/}*.test.ts' --config ../../config/mocharc.node.js",
18-
"test": "firebase --project=foo --debug emulators:exec 'yarn test:nyc'",
18+
"test": "FIREBASE_CLI_PREVIEWS=storageemulator STORAGE_EMULATOR_HOST=http://localhost:9199 firebase --project=foo --debug emulators:exec 'yarn test:nyc'",
1919
"test:ci": "node ../../scripts/run_tests_in_ci.js -s test"
2020
},
2121
"license": "Apache-2.0",
@@ -29,15 +29,15 @@
2929
"devDependencies": {
3030
"@google-cloud/firestore": "4.8.1",
3131
"@types/request": "2.48.5",
32-
"firebase-admin": "9.4.2",
33-
"firebase-tools": "9.1.0",
32+
"firebase-admin": "9.7.0",
33+
"firebase-tools": "9.10.1",
3434
"firebase-functions": "3.13.0",
3535
"rollup": "2.35.1",
3636
"rollup-plugin-typescript2": "0.29.0"
3737
},
3838
"peerDependencies": {
3939
"@google-cloud/firestore": "^4.2.0",
40-
"firebase-admin": "^9.0.0"
40+
"firebase-admin": "^9.7.0"
4141
},
4242
"repository": {
4343
"directory": "packages/rules-unit-testing",

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

Lines changed: 129 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,37 @@
1616
*/
1717

1818
import firebase from 'firebase';
19+
import 'firebase/database';
20+
import 'firebase/firestore';
21+
import 'firebase/storage';
22+
23+
import type { app } from 'firebase-admin';
1924
import { _FirebaseApp } from '@firebase/app-types/private';
2025
import { FirebaseAuthInternal } from '@firebase/auth-interop-types';
2126
import * as request from 'request';
2227
import { base64 } from '@firebase/util';
2328
import { setLogLevel, LogLevel } from '@firebase/logger';
2429
import { Component, ComponentType } from '@firebase/component';
2530

26-
const { firestore, database } = firebase;
27-
export { firestore, database };
31+
const { firestore, database, storage } = firebase;
32+
export { firestore, database, storage };
2833

2934
/** If this environment variable is set, use it for the database emulator's address. */
3035
const DATABASE_ADDRESS_ENV: string = 'FIREBASE_DATABASE_EMULATOR_HOST';
3136
/** The default address for the local database emulator. */
3237
const DATABASE_ADDRESS_DEFAULT: string = 'localhost:9000';
3338

34-
/** If any of environment variable is set, use it for the Firestore emulator. */
39+
/** If this environment variable is set, use it for the Firestore emulator. */
3540
const FIRESTORE_ADDRESS_ENV: string = 'FIRESTORE_EMULATOR_HOST';
3641
/** The default address for the local Firestore emulator. */
3742
const FIRESTORE_ADDRESS_DEFAULT: string = 'localhost:8080';
3843

44+
/** If this environment variable is set, use it for the Storage emulator. */
45+
const FIREBASE_STORAGE_ADDRESS_ENV: string = 'FIREBASE_STORAGE_EMULATOR_HOST';
46+
const CLOUD_STORAGE_ADDRESS_ENV: string = 'STORAGE_EMULATOR_HOST';
47+
/** The default address for the local Firestore emulator. */
48+
const STORAGE_ADDRESS_DEFAULT: string = 'localhost:9199';
49+
3950
/** Environment variable to locate the Emulator Hub */
4051
const HUB_HOST_ENV: string = 'FIREBASE_EMULATOR_HUB';
4152
/** The default address for the Emulator Hub */
@@ -47,6 +58,9 @@ let _databaseHost: string | undefined = undefined;
4758
/** The actual address for the Firestore emulator */
4859
let _firestoreHost: string | undefined = undefined;
4960

61+
/** The actual address for the Storage emulator */
62+
let _storageHost: string | undefined = undefined;
63+
5064
/** The actual address for the Emulator Hub */
5165
let _hubHost: string | undefined = undefined;
5266

@@ -133,6 +147,10 @@ export type FirebaseEmulatorOptions = {
133147
host: string;
134148
port: number;
135149
};
150+
storage?: {
151+
host: string;
152+
port: number;
153+
};
136154
hub?: {
137155
host: string;
138156
port: number;
@@ -193,6 +211,7 @@ export function apps(): firebase.app.App[] {
193211
export type AppOptions = {
194212
databaseName?: string;
195213
projectId?: string;
214+
storageBucket?: string;
196215
auth?: TokenOptions;
197216
};
198217
/** Construct an App authenticated with options.auth. */
@@ -201,19 +220,29 @@ export function initializeTestApp(options: AppOptions): firebase.app.App {
201220
? createUnsecuredJwt(options.auth, options.projectId)
202221
: undefined;
203222

204-
return initializeApp(jwt, options.databaseName, options.projectId);
223+
return initializeApp(
224+
jwt,
225+
options.databaseName,
226+
options.projectId,
227+
options.storageBucket
228+
);
205229
}
206230

207231
export type AdminAppOptions = {
208232
databaseName?: string;
209233
projectId?: string;
234+
storageBucket?: string;
210235
};
211236
/** Construct an App authenticated as an admin user. */
212-
export function initializeAdminApp(options: AdminAppOptions): firebase.app.App {
237+
export function initializeAdminApp(options: AdminAppOptions): app.App {
213238
const admin = require('firebase-admin');
214239

215-
const app = admin.initializeApp(
216-
getAppOptions(options.databaseName, options.projectId),
240+
const app: app.App = admin.initializeApp(
241+
getAppOptions(
242+
options.databaseName,
243+
options.projectId,
244+
options.storageBucket
245+
),
217246
getRandomAppName()
218247
);
219248

@@ -248,6 +277,10 @@ export function useEmulators(options: FirebaseEmulatorOptions): void {
248277
_firestoreHost = getAddress(options.firestore.host, options.firestore.port);
249278
}
250279

280+
if (options.storage) {
281+
_storageHost = getAddress(options.storage.host, options.storage.port);
282+
}
283+
251284
if (options.hub) {
252285
_hubHost = getAddress(options.hub.host, options.hub.port);
253286
}
@@ -301,6 +334,13 @@ export async function discoverEmulators(
301334
};
302335
}
303336

337+
if (data.storage) {
338+
options.storage = {
339+
host: data.storage.host,
340+
port: data.storage.port
341+
};
342+
}
343+
304344
if (data.hub) {
305345
options.hub = {
306346
host: data.hub.host,
@@ -351,6 +391,27 @@ function getFirestoreHost() {
351391
return _firestoreHost;
352392
}
353393

394+
function getStorageHost() {
395+
if (!_storageHost) {
396+
const fromEnv =
397+
process.env[FIREBASE_STORAGE_ADDRESS_ENV] ||
398+
process.env[CLOUD_STORAGE_ADDRESS_ENV];
399+
if (fromEnv) {
400+
// The STORAGE_EMULATOR_HOST env var is an older Cloud Standard which includes http:// while
401+
// the FIREBASE_STORAGE_EMULATOR_HOST is a newer variable supported beginning in the Admin
402+
// SDK v9.7.0 which does not have the protocol.
403+
_storageHost = fromEnv.replace('http://', '');
404+
} else {
405+
console.warn(
406+
`Warning: ${FIREBASE_STORAGE_ADDRESS_ENV} not set, using default value ${STORAGE_ADDRESS_DEFAULT}`
407+
);
408+
_storageHost = STORAGE_ADDRESS_DEFAULT;
409+
}
410+
}
411+
412+
return _storageHost;
413+
}
414+
354415
function getHubHost() {
355416
if (!_hubHost) {
356417
const fromEnv = process.env[HUB_HOST_ENV];
@@ -367,34 +428,52 @@ function getHubHost() {
367428
return _hubHost;
368429
}
369430

431+
function parseHost(host: string): { hostname: string; port: number } {
432+
const withProtocol = host.startsWith("http") ? host : `http://${host}`;
433+
const u = new URL(withProtocol);
434+
return {
435+
hostname: u.hostname,
436+
port: Number.parseInt(u.port, 10)
437+
};
438+
}
439+
370440
function getRandomAppName(): string {
371441
return 'app-' + new Date().getTime() + '-' + Math.random();
372442
}
373443

444+
function getDatabaseUrl(databaseName: string) {
445+
return `http://${getDatabaseHost()}?ns=${databaseName}`;
446+
}
447+
374448
function getAppOptions(
375449
databaseName?: string,
376-
projectId?: string
450+
projectId?: string,
451+
storageBucket?: string
377452
): { [key: string]: string } {
378453
let appOptions: { [key: string]: string } = {};
379454

380455
if (databaseName) {
381-
appOptions[
382-
'databaseURL'
383-
] = `http://${getDatabaseHost()}?ns=${databaseName}`;
456+
appOptions['databaseURL'] = getDatabaseUrl(databaseName);
384457
}
458+
385459
if (projectId) {
386460
appOptions['projectId'] = projectId;
387461
}
388462

463+
if (storageBucket) {
464+
appOptions['storageBucket'] = storageBucket;
465+
}
466+
389467
return appOptions;
390468
}
391469

392470
function initializeApp(
393471
accessToken?: string,
394472
databaseName?: string,
395-
projectId?: string
473+
projectId?: string,
474+
storageBucket?: string
396475
): firebase.app.App {
397-
const appOptions = getAppOptions(databaseName, projectId);
476+
const appOptions = getAppOptions(databaseName, projectId, storageBucket);
398477
const app = firebase.initializeApp(appOptions, getRandomAppName());
399478
if (accessToken) {
400479
const mockAuthComponent = new Component(
@@ -417,17 +496,22 @@ function initializeApp(
417496
);
418497
}
419498
if (databaseName) {
499+
const { hostname, port } = parseHost(getDatabaseHost());
500+
app.database().useEmulator(hostname, port);
501+
420502
// Toggle network connectivity to force a reauthentication attempt.
421503
// This mitigates a minor race condition where the client can send the
422504
// first database request before authenticating.
423505
app.database().goOffline();
424506
app.database().goOnline();
425507
}
426508
if (projectId) {
427-
app.firestore().settings({
428-
host: getFirestoreHost(),
429-
ssl: false
430-
});
509+
const { hostname, port } = parseHost(getFirestoreHost());
510+
app.firestore().useEmulator(hostname, port);
511+
}
512+
if (storageBucket) {
513+
const { hostname, port } = parseHost(getStorageHost());
514+
app.storage().useEmulator(hostname, port);
431515
}
432516
/**
433517
Mute warnings for the previously-created database and whatever other
@@ -498,6 +582,34 @@ export async function loadFirestoreRules(
498582
}
499583
}
500584

585+
export type LoadStorageRulesOptions = {
586+
rules: string;
587+
};
588+
export async function loadStorageRules(
589+
options: LoadStorageRulesOptions
590+
): Promise<void> {
591+
if (!options.rules) {
592+
throw new Error('must provide rules to loadStorageRules');
593+
}
594+
595+
const resp = await requestPromise(request.put, {
596+
method: 'PUT',
597+
uri: `http://${getStorageHost()}/internal/setRules`,
598+
headers: {
599+
'Content-Type': 'application/json'
600+
},
601+
body: JSON.stringify({
602+
rules: {
603+
files: [{ name: 'storage.rules', content: options.rules }]
604+
}
605+
})
606+
});
607+
608+
if (resp.statusCode !== 200) {
609+
throw new Error(resp.body);
610+
}
611+
}
612+
501613
export type ClearFirestoreDataOptions = {
502614
projectId: string;
503615
};

0 commit comments

Comments
 (0)