Skip to content

Commit a63ba03

Browse files
yuchenshiFeiyang1
authored andcommitted
Implement emulators discovery in RUTv2. (#5334)
* Add new types and function stubs. * Fix types for testEnv.emulators. * Add util functions. * Add withFunctionTriggersDisabled overloads. * Improve typing for EmulatorConfig. * Fix tests. * Rename test_environment.ts to initialize.ts. * Add a dummy test to make CI pass. * Implement emulators discovery. * Use URL object from global. * Fix unreachable error code.
1 parent de643b1 commit a63ba03

File tree

3 files changed

+583
-1
lines changed

3 files changed

+583
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/**
2+
* @license
3+
* Copyright 2021 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { EmulatorConfig, HostAndPort } from '../public_types';
19+
import nodeFetch from 'node-fetch';
20+
21+
/**
22+
* Use the Firebase Emulator hub to discover other running emulators.
23+
*
24+
* @param hub the host and port where the Emulator Hub is running
25+
* @private
26+
*/
27+
export async function discoverEmulators(
28+
hub: HostAndPort,
29+
fetch: typeof nodeFetch = nodeFetch
30+
): Promise<DiscoveredEmulators> {
31+
const res = await fetch(makeUrl(hub, '/emulators'));
32+
if (!res.ok) {
33+
throw new Error(
34+
`HTTP Error ${res.status} when attempting to reach Emulator Hub at ${res.url}, are you sure it is running?`
35+
);
36+
}
37+
38+
const emulators: DiscoveredEmulators = {};
39+
40+
const data = await res.json();
41+
42+
if (data.database) {
43+
emulators.database = {
44+
host: data.database.host,
45+
port: data.database.port
46+
};
47+
}
48+
49+
if (data.firestore) {
50+
emulators.firestore = {
51+
host: data.firestore.host,
52+
port: data.firestore.port
53+
};
54+
}
55+
56+
if (data.storage) {
57+
emulators.storage = {
58+
host: data.storage.host,
59+
port: data.storage.port
60+
};
61+
}
62+
63+
if (data.hub) {
64+
emulators.hub = {
65+
host: data.hub.host,
66+
port: data.hub.port
67+
};
68+
}
69+
return emulators;
70+
}
71+
72+
/**
73+
* @private
74+
*/
75+
export interface DiscoveredEmulators {
76+
database?: HostAndPort;
77+
firestore?: HostAndPort;
78+
storage?: HostAndPort;
79+
hub?: HostAndPort;
80+
}
81+
82+
function makeUrl(hostAndPort: HostAndPort | string, path: string): URL {
83+
if (typeof hostAndPort === 'object') {
84+
const { host, port } = hostAndPort;
85+
if (host.includes(':')) {
86+
hostAndPort = `[${host}]:${port}`;
87+
} else {
88+
hostAndPort = `${host}:${port}`;
89+
}
90+
}
91+
const url = new URL(`http://${hostAndPort}/`);
92+
url.pathname = path;
93+
return url;
94+
}
95+
96+
/**
97+
* @private
98+
*/
99+
export function getEmulatorHostAndPort(
100+
emulator: keyof DiscoveredEmulators,
101+
conf?: EmulatorConfig,
102+
discovered?: DiscoveredEmulators
103+
) {
104+
if (conf && ('host' in conf || 'port' in conf)) {
105+
const { host, port } = conf;
106+
if (host || port) {
107+
if (!host || !port) {
108+
throw new Error(
109+
`Invalid configuration ${emulator}.host=${host} and ${emulator}.port=${port}. ` +
110+
'If either parameter is supplied, both must be defined.'
111+
);
112+
}
113+
if (discovered && !discovered[emulator]) {
114+
console.warn(
115+
`Warning: config for the ${emulator} emulator is specified, but the Emulator hub ` +
116+
'reports it as not running. This may lead to errors such as connection refused.'
117+
);
118+
}
119+
return {
120+
host: fixHostname(conf.host, discovered?.hub?.host),
121+
port: conf.port
122+
};
123+
}
124+
}
125+
const envVar = EMULATOR_HOST_ENV_VARS[emulator];
126+
const fallback = discovered?.[emulator] || emulatorFromEnvVar(envVar);
127+
if (fallback) {
128+
if (discovered && !discovered[emulator]) {
129+
console.warn(
130+
`Warning: the environment variable ${envVar} is set, but the Emulator hub reports the ` +
131+
`${emulator} emulator as not running. This may lead to errors such as connection refused.`
132+
);
133+
}
134+
return {
135+
host: fixHostname(fallback.host, discovered?.hub?.host),
136+
port: fallback.port
137+
};
138+
}
139+
}
140+
141+
// Visible for testing.
142+
export const EMULATOR_HOST_ENV_VARS = {
143+
'database': 'FIREBASE_DATABASE_EMULATOR_HOST',
144+
'firestore': 'FIRESTORE_EMULATOR_HOST',
145+
'hub': 'FIREBASE_EMULATOR_HUB',
146+
'storage': 'FIREBASE_STORAGE_EMULATOR_HOST'
147+
};
148+
149+
function emulatorFromEnvVar(envVar: string): HostAndPort | undefined {
150+
const hostAndPort = process.env[envVar];
151+
if (!hostAndPort) {
152+
return undefined;
153+
}
154+
155+
let parsed: URL;
156+
try {
157+
parsed = new URL(`http://${hostAndPort}`);
158+
} catch {
159+
throw new Error(
160+
`Invalid format in environment variable ${envVar}=${hostAndPort} (expected host:port)`
161+
);
162+
}
163+
let host = parsed.hostname;
164+
const port = Number(parsed.port || '80');
165+
if (!Number.isInteger(port)) {
166+
throw new Error(
167+
`Invalid port in environment variable ${envVar}=${hostAndPort}`
168+
);
169+
}
170+
return { host, port };
171+
}
172+
173+
/**
174+
* Return a connectable hostname, replacing wildcard 0.0.0.0 or :: with loopback
175+
* addresses 127.0.0.1 / ::1 correspondingly. See below for why this is needed:
176+
* https://github.com/firebase/firebase-tools-ui/issues/286
177+
*
178+
* This assumes emulators are running on the same device as the Emulator UI
179+
* server, which should hold if both are started from the same CLI command.
180+
*/
181+
function fixHostname(host: string, fallbackHost?: string): string {
182+
host = host.replace('[', '').replace(']', ''); // Remove IPv6 brackets
183+
if (host === '0.0.0.0') {
184+
host = fallbackHost || '127.0.0.1';
185+
} else if (host === '::') {
186+
host = fallbackHost || '::1';
187+
}
188+
return host;
189+
}

packages/rules-unit-testing/src/initialize.ts

+37-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616
*/
1717

1818
import { RulesTestEnvironment, TestEnvironmentConfig } from './public_types';
19+
import {
20+
DiscoveredEmulators,
21+
discoverEmulators,
22+
getEmulatorHostAndPort
23+
} from './impl/discovery';
1924

2025
/**
2126
* Initializes a test environment for rules unit testing. Call this function first for test setup.
@@ -38,6 +43,37 @@ import { RulesTestEnvironment, TestEnvironmentConfig } from './public_types';
3843
* });
3944
* ```
4045
*/
41-
export async function initializeTestEnvironment(): Promise<RulesTestEnvironment> {
46+
export async function initializeTestEnvironment(
47+
config: TestEnvironmentConfig
48+
): Promise<RulesTestEnvironment> {
49+
const projectId = config.projectId || process.env.GCLOUD_PROJECT;
50+
if (!projectId) {
51+
throw new Error(
52+
'Missing projectId option or env var GCLOUD_PROJECT! Please specify the projectId either ' +
53+
'way.\n(A demo-* projectId is strongly recommended for unit tests, such as "demo-test".)'
54+
);
55+
}
56+
const hub = getEmulatorHostAndPort('hub', config.hub);
57+
let discovered = hub ? { ...(await discoverEmulators(hub)), hub } : undefined;
58+
59+
const emulators: DiscoveredEmulators = {};
60+
if (hub) {
61+
emulators.hub = hub;
62+
}
63+
64+
for (const emulator of SUPPORTED_EMULATORS) {
65+
const hostAndPort = getEmulatorHostAndPort(
66+
emulator,
67+
config[emulator],
68+
discovered
69+
);
70+
if (hostAndPort) {
71+
emulators[emulator] = hostAndPort;
72+
}
73+
}
74+
75+
// TODO: Create test env and set security rules.
4276
throw new Error('unimplemented');
4377
}
78+
79+
const SUPPORTED_EMULATORS = ['database', 'firestore', 'storage'] as const;

0 commit comments

Comments
 (0)