Skip to content

Commit 65abed5

Browse files
committed
Storage Emulator support in rules-unit-testing
1 parent d095ad3 commit 65abed5

File tree

2 files changed

+114
-11
lines changed

2 files changed

+114
-11
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/src/api/index.ts

Lines changed: 109 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,16 @@ const DATABASE_ADDRESS_ENV: string = 'FIREBASE_DATABASE_EMULATOR_HOST';
3131
/** The default address for the local database emulator. */
3232
const DATABASE_ADDRESS_DEFAULT: string = 'localhost:9000';
3333

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

39+
/** If this environment variable is set, use it for the Storage emulator. */
40+
const STORAGE_ADDRESS_ENV: string = 'STORAGE_EMULATOR_HOST';
41+
/** The default address for the local Firestore emulator. */
42+
const STORAGE_ADDRESS_DEFAULT: string = 'localhost:9199';
43+
3944
/** Environment variable to locate the Emulator Hub */
4045
const HUB_HOST_ENV: string = 'FIREBASE_EMULATOR_HUB';
4146
/** The default address for the Emulator Hub */
@@ -47,6 +52,9 @@ let _databaseHost: string | undefined = undefined;
4752
/** The actual address for the Firestore emulator */
4853
let _firestoreHost: string | undefined = undefined;
4954

55+
/** The actual address for the Storage emulator */
56+
let _storageHost: string | undefined = undefined;
57+
5058
/** The actual address for the Emulator Hub */
5159
let _hubHost: string | undefined = undefined;
5260

@@ -133,6 +141,10 @@ export type FirebaseEmulatorOptions = {
133141
host: string;
134142
port: number;
135143
};
144+
storage?: {
145+
host: string;
146+
port: number;
147+
};
136148
hub?: {
137149
host: string;
138150
port: number;
@@ -193,6 +205,7 @@ export function apps(): firebase.app.App[] {
193205
export type AppOptions = {
194206
databaseName?: string;
195207
projectId?: string;
208+
storageBucket?: string;
196209
auth?: TokenOptions;
197210
};
198211
/** Construct an App authenticated with options.auth. */
@@ -201,19 +214,20 @@ export function initializeTestApp(options: AppOptions): firebase.app.App {
201214
? createUnsecuredJwt(options.auth, options.projectId)
202215
: undefined;
203216

204-
return initializeApp(jwt, options.databaseName, options.projectId);
217+
return initializeApp(jwt, options.databaseName, options.projectId, options.storageBucket);
205218
}
206219

207220
export type AdminAppOptions = {
208221
databaseName?: string;
209222
projectId?: string;
223+
storageBucket?: string;
210224
};
211225
/** Construct an App authenticated as an admin user. */
212226
export function initializeAdminApp(options: AdminAppOptions): firebase.app.App {
213227
const admin = require('firebase-admin');
214228

215229
const app = admin.initializeApp(
216-
getAppOptions(options.databaseName, options.projectId),
230+
getAppOptions(options.databaseName, options.projectId, options.storageBucket),
217231
getRandomAppName()
218232
);
219233

@@ -248,6 +262,10 @@ export function useEmulators(options: FirebaseEmulatorOptions): void {
248262
_firestoreHost = getAddress(options.firestore.host, options.firestore.port);
249263
}
250264

265+
if (options.storage) {
266+
_storageHost = getAddress(options.storage.host, options.storage.port);
267+
}
268+
251269
if (options.hub) {
252270
_hubHost = getAddress(options.hub.host, options.hub.port);
253271
}
@@ -301,6 +319,13 @@ export async function discoverEmulators(
301319
};
302320
}
303321

322+
if (data.storage) {
323+
options.storage = {
324+
host: data.storage.host,
325+
port: data.storage.port
326+
};
327+
}
328+
304329
if (data.hub) {
305330
options.hub = {
306331
host: data.hub.host,
@@ -351,6 +376,22 @@ function getFirestoreHost() {
351376
return _firestoreHost;
352377
}
353378

379+
function getStorageHost() {
380+
if (!_storageHost) {
381+
const fromEnv = process.env[STORAGE_ADDRESS_ENV];
382+
if (fromEnv) {
383+
_storageHost = fromEnv;
384+
} else {
385+
console.warn(
386+
`Warning: ${STORAGE_ADDRESS_ENV} not set, using default value ${STORAGE_ADDRESS_DEFAULT}`
387+
);
388+
_storageHost = STORAGE_ADDRESS_DEFAULT;
389+
}
390+
}
391+
392+
return _storageHost;
393+
}
394+
354395
function getHubHost() {
355396
if (!_hubHost) {
356397
const fromEnv = process.env[HUB_HOST_ENV];
@@ -367,34 +408,53 @@ function getHubHost() {
367408
return _hubHost;
368409
}
369410

411+
function parseHost(host: string): { hostname: string, port: number } {
412+
const u = new URL(host);
413+
return {
414+
hostname: u.hostname,
415+
port: Number.parseInt(u.port, 10);
416+
}
417+
}
418+
370419
function getRandomAppName(): string {
371420
return 'app-' + new Date().getTime() + '-' + Math.random();
372421
}
373422

423+
function getDatabaseUrl(databaseName: string) {
424+
return `http://${getDatabaseHost()}?ns=${databaseName}`;
425+
}
426+
374427
function getAppOptions(
375428
databaseName?: string,
376-
projectId?: string
429+
projectId?: string,
430+
storageBucket?: string,
377431
): { [key: string]: string } {
378432
let appOptions: { [key: string]: string } = {};
379433

380434
if (databaseName) {
381435
appOptions[
382436
'databaseURL'
383-
] = `http://${getDatabaseHost()}?ns=${databaseName}`;
437+
] = getDatabaseUrl(databaseName);
384438
}
439+
385440
if (projectId) {
386441
appOptions['projectId'] = projectId;
387442
}
388443

444+
if (storageBucket) {
445+
appOptions['storgeBucket'] = storageBucket;
446+
}
447+
389448
return appOptions;
390449
}
391450

392451
function initializeApp(
393452
accessToken?: string,
394453
databaseName?: string,
395-
projectId?: string
454+
projectId?: string,
455+
storageBucket?: string,
396456
): firebase.app.App {
397-
const appOptions = getAppOptions(databaseName, projectId);
457+
const appOptions = getAppOptions(databaseName, projectId, storageBucket);
398458
const app = firebase.initializeApp(appOptions, getRandomAppName());
399459
if (accessToken) {
400460
const mockAuthComponent = new Component(
@@ -417,17 +477,22 @@ function initializeApp(
417477
);
418478
}
419479
if (databaseName) {
480+
const { hostname, port } = parseHost(getDatabaseHost());
481+
app.database().useEmulator(hostname, port);
482+
420483
// Toggle network connectivity to force a reauthentication attempt.
421484
// This mitigates a minor race condition where the client can send the
422485
// first database request before authenticating.
423486
app.database().goOffline();
424487
app.database().goOnline();
425488
}
426489
if (projectId) {
427-
app.firestore().settings({
428-
host: getFirestoreHost(),
429-
ssl: false
430-
});
490+
const { hostname, port } = parseHost(getFirestoreHost());
491+
app.firestore().useEmulator(hostname, port);
492+
}
493+
if (storageBucket) {
494+
const { hostname, port } = parseHost(getStorageHost());
495+
app.storage().useEmulator(hostname, port);
431496
}
432497
/**
433498
Mute warnings for the previously-created database and whatever other
@@ -498,6 +563,39 @@ export async function loadFirestoreRules(
498563
}
499564
}
500565

566+
export type LoadStorageRulesOptions = {
567+
storageBucket: string;
568+
rules: string;
569+
};
570+
export async function loadStorageRules(
571+
options: LoadStorageRulesOptions
572+
): Promise<void> {
573+
if (!options.storageBucket) {
574+
throw new Error('storageBucket not specified');
575+
}
576+
577+
if (!options.rules) {
578+
throw new Error('must provide rules to loadStorageRules');
579+
}
580+
581+
// TODO: This endpoint is not yet implemented! Will need a change in firebase-tools
582+
// to make this real.
583+
const resp = await requestPromise(request.put, {
584+
method: 'PUT',
585+
uri: `http://${getStorageHost()}/internal/setRules`,
586+
body: JSON.stringify({
587+
storageBucket: options.storageBucket,
588+
rules: {
589+
files: [{ content: options.rules }]
590+
}
591+
})
592+
});
593+
594+
if (resp.statusCode !== 200) {
595+
throw new Error(JSON.parse(resp.body.error));
596+
}
597+
}
598+
501599
export type ClearFirestoreDataOptions = {
502600
projectId: string;
503601
};

0 commit comments

Comments
 (0)