react-on-rails-pro-node-renderer 16.6.0-rc.0 → 16.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/master.d.ts.map +1 -1
- package/lib/master.js +56 -4
- package/lib/master.js.map +1 -1
- package/lib/shared/configBuilder.d.ts.map +1 -1
- package/lib/shared/configBuilder.js +15 -0
- package/lib/shared/configBuilder.js.map +1 -1
- package/lib/shared/utils.d.ts +9 -2
- package/lib/shared/utils.d.ts.map +1 -1
- package/lib/shared/utils.js +15 -8
- package/lib/shared/utils.js.map +1 -1
- package/lib/shared/workerMessages.d.ts +13 -0
- package/lib/shared/workerMessages.d.ts.map +1 -0
- package/lib/shared/workerMessages.js +23 -0
- package/lib/shared/workerMessages.js.map +1 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/worker/handleRenderRequest.d.ts +2 -2
- package/lib/worker/handleRenderRequest.d.ts.map +1 -1
- package/lib/worker/handleRenderRequest.js +5 -5
- package/lib/worker/handleRenderRequest.js.map +1 -1
- package/lib/worker/startupErrorHandler.d.ts +10 -0
- package/lib/worker/startupErrorHandler.d.ts.map +1 -0
- package/lib/worker/startupErrorHandler.js +57 -0
- package/lib/worker/startupErrorHandler.js.map +1 -0
- package/lib/worker/vm.js +2 -2
- package/lib/worker/vm.js.map +1 -1
- package/lib/worker.d.ts.map +1 -1
- package/lib/worker.js +73 -9
- package/lib/worker.js.map +1 -1
- package/package.json +2 -2
- package/src/master.ts +64 -4
- package/src/shared/configBuilder.ts +18 -0
- package/src/shared/utils.ts +18 -8
- package/src/shared/workerMessages.ts +34 -0
- package/src/worker/handleRenderRequest.ts +18 -7
- package/src/worker/startupErrorHandler.ts +65 -0
- package/src/worker/vm.ts +2 -2
- package/src/worker.ts +86 -15
- package/tests/configBuilder.test.ts +34 -0
- package/tests/masterStartupFailure.test.ts +295 -0
- package/tests/worker.test.ts +148 -0
- package/tests/workerStartupFailure.test.ts +250 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const WORKER_STARTUP_FAILURE = 'NODE_RENDERER_WORKER_STARTUP_FAILURE' as const;
|
|
2
|
+
|
|
3
|
+
export interface WorkerStartupFailureMessage {
|
|
4
|
+
type: typeof WORKER_STARTUP_FAILURE;
|
|
5
|
+
stage: 'listen';
|
|
6
|
+
code?: string;
|
|
7
|
+
errno?: number;
|
|
8
|
+
syscall?: string;
|
|
9
|
+
host: string;
|
|
10
|
+
port: number;
|
|
11
|
+
message: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isWorkerStartupFailureMessage(value: unknown): value is WorkerStartupFailureMessage {
|
|
15
|
+
if (typeof value !== 'object' || value === null) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const message = value as Partial<WorkerStartupFailureMessage>;
|
|
20
|
+
|
|
21
|
+
// stage: 'listen' is the only supported stage today. To handle pre-listen
|
|
22
|
+
// failures (e.g. plugin registration), add a new stage value here and
|
|
23
|
+
// update the master handler accordingly.
|
|
24
|
+
return (
|
|
25
|
+
message.type === WORKER_STARTUP_FAILURE &&
|
|
26
|
+
message.stage === 'listen' &&
|
|
27
|
+
typeof message.host === 'string' &&
|
|
28
|
+
typeof message.port === 'number' &&
|
|
29
|
+
Number.isInteger(message.port) &&
|
|
30
|
+
message.port >= 0 &&
|
|
31
|
+
message.port <= 65535 &&
|
|
32
|
+
typeof message.message === 'string'
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
isReadableStream,
|
|
23
23
|
isErrorRenderResult,
|
|
24
24
|
getRequestBundleFilePath,
|
|
25
|
+
type RequestInfo,
|
|
25
26
|
} from '../shared/utils.js';
|
|
26
27
|
import { getConfig } from '../shared/configBuilder.js';
|
|
27
28
|
import type { TracingContext } from '../shared/tracing.js';
|
|
@@ -41,8 +42,14 @@ async function prepareResult(
|
|
|
41
42
|
|
|
42
43
|
let exceptionMessage = null;
|
|
43
44
|
if (!result) {
|
|
44
|
-
const error = new Error(
|
|
45
|
-
|
|
45
|
+
const error = new Error(
|
|
46
|
+
'INVALID NIL or NULL result for rendering. Ensure renderingRequest is a valid string and returns a value.',
|
|
47
|
+
);
|
|
48
|
+
exceptionMessage = formatExceptionMessage(
|
|
49
|
+
{ renderingRequest },
|
|
50
|
+
error,
|
|
51
|
+
'INVALID result for prepareResult',
|
|
52
|
+
);
|
|
46
53
|
} else if (isErrorRenderResult(result)) {
|
|
47
54
|
({ exceptionMessage } = result);
|
|
48
55
|
}
|
|
@@ -65,7 +72,11 @@ async function prepareResult(
|
|
|
65
72
|
data: result,
|
|
66
73
|
};
|
|
67
74
|
} catch (err) {
|
|
68
|
-
const exceptionMessage = formatExceptionMessage(
|
|
75
|
+
const exceptionMessage = formatExceptionMessage(
|
|
76
|
+
{ renderingRequest },
|
|
77
|
+
err,
|
|
78
|
+
'Unknown error calling runInVM',
|
|
79
|
+
);
|
|
69
80
|
return errorResponseResult(exceptionMessage);
|
|
70
81
|
}
|
|
71
82
|
}
|
|
@@ -77,7 +88,7 @@ async function prepareResult(
|
|
|
77
88
|
* @param assetsToCopy might be null
|
|
78
89
|
*/
|
|
79
90
|
async function handleNewBundleProvided(
|
|
80
|
-
requestContext:
|
|
91
|
+
requestContext: RequestInfo,
|
|
81
92
|
providedNewBundle: ProvidedNewBundle,
|
|
82
93
|
assetsToCopy: Asset[] | null | undefined,
|
|
83
94
|
): Promise<ResponseResult | undefined> {
|
|
@@ -154,7 +165,7 @@ to ${bundleFilePathPerTimestamp})`,
|
|
|
154
165
|
}
|
|
155
166
|
|
|
156
167
|
export async function handleNewBundlesProvided(
|
|
157
|
-
requestContext:
|
|
168
|
+
requestContext: RequestInfo,
|
|
158
169
|
providedNewBundles: ProvidedNewBundle[],
|
|
159
170
|
assetsToCopy: Asset[] | null | undefined,
|
|
160
171
|
): Promise<ResponseResult | undefined> {
|
|
@@ -226,7 +237,7 @@ export async function handleRenderRequest({
|
|
|
226
237
|
|
|
227
238
|
// If gem has posted updated bundle:
|
|
228
239
|
if (providedNewBundles && providedNewBundles.length > 0) {
|
|
229
|
-
const result = await handleNewBundlesProvided(renderingRequest, providedNewBundles, assetsToCopy);
|
|
240
|
+
const result = await handleNewBundlesProvided({ renderingRequest }, providedNewBundles, assetsToCopy);
|
|
230
241
|
if (result) {
|
|
231
242
|
return result;
|
|
232
243
|
}
|
|
@@ -261,7 +272,7 @@ export async function handleRenderRequest({
|
|
|
261
272
|
return await prepareResult(renderingRequest, entryBundleFilePath);
|
|
262
273
|
} catch (error) {
|
|
263
274
|
const msg = formatExceptionMessage(
|
|
264
|
-
renderingRequest,
|
|
275
|
+
{ renderingRequest },
|
|
265
276
|
error,
|
|
266
277
|
'Caught top level error in handleRenderRequest',
|
|
267
278
|
);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import cluster from 'cluster';
|
|
2
|
+
import log from '../shared/log.js';
|
|
3
|
+
import { WORKER_STARTUP_FAILURE, type WorkerStartupFailureMessage } from '../shared/workerMessages.js';
|
|
4
|
+
|
|
5
|
+
export type StartupListenErrorHandlerOptions = {
|
|
6
|
+
err: Error;
|
|
7
|
+
host: string;
|
|
8
|
+
port: number;
|
|
9
|
+
isWorker?: boolean;
|
|
10
|
+
send?: NodeJS.Process['send'];
|
|
11
|
+
exit?: NodeJS.Process['exit'];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function handleStartupListenError({
|
|
15
|
+
err,
|
|
16
|
+
host,
|
|
17
|
+
port,
|
|
18
|
+
isWorker = cluster.isWorker,
|
|
19
|
+
send,
|
|
20
|
+
exit,
|
|
21
|
+
}: StartupListenErrorHandlerOptions) {
|
|
22
|
+
const sendFn = send ?? process.send?.bind(process);
|
|
23
|
+
const exitFn = exit ?? ((code?: number) => process.exit(code));
|
|
24
|
+
|
|
25
|
+
log.error({ err, host, port }, 'Node renderer failed to start');
|
|
26
|
+
|
|
27
|
+
if (isWorker) {
|
|
28
|
+
if (!sendFn) {
|
|
29
|
+
log.error('Cluster worker has no IPC channel; cannot notify master of startup failure');
|
|
30
|
+
exitFn(1);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const startupFailure: WorkerStartupFailureMessage = {
|
|
35
|
+
type: WORKER_STARTUP_FAILURE,
|
|
36
|
+
stage: 'listen',
|
|
37
|
+
code: (err as NodeJS.ErrnoException).code,
|
|
38
|
+
errno: (err as NodeJS.ErrnoException).errno,
|
|
39
|
+
syscall: (err as NodeJS.ErrnoException).syscall,
|
|
40
|
+
host,
|
|
41
|
+
port,
|
|
42
|
+
message: err.message,
|
|
43
|
+
};
|
|
44
|
+
try {
|
|
45
|
+
let exited = false;
|
|
46
|
+
const doExit = (sendErr?: Error | null) => {
|
|
47
|
+
if (exited) return;
|
|
48
|
+
exited = true;
|
|
49
|
+
if (sendErr) log.error({ err: sendErr }, 'Failed to send startup failure message to master');
|
|
50
|
+
exitFn(1);
|
|
51
|
+
};
|
|
52
|
+
sendFn(startupFailure, undefined, undefined, doExit);
|
|
53
|
+
// Safety net: if the IPC channel is half-broken the callback may never
|
|
54
|
+
// fire, leaving this worker alive indefinitely. Force exit after a timeout.
|
|
55
|
+
const IPC_SEND_TIMEOUT_MS = 2000;
|
|
56
|
+
const timer = setTimeout(() => doExit(), IPC_SEND_TIMEOUT_MS);
|
|
57
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
58
|
+
} catch (sendErr) {
|
|
59
|
+
log.error({ err: sendErr as Error }, 'Failed to send startup failure message to master');
|
|
60
|
+
exitFn(1);
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
exitFn(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/worker/vm.ts
CHANGED
|
@@ -153,7 +153,7 @@ ${smartTrim(renderingRequest)}`);
|
|
|
153
153
|
|
|
154
154
|
if (isReadableStream(result)) {
|
|
155
155
|
const newStreamAfterHandlingError = handleStreamError(result, (error) => {
|
|
156
|
-
const msg = formatExceptionMessage(renderingRequest, error, 'Error in a rendering stream');
|
|
156
|
+
const msg = formatExceptionMessage({ renderingRequest }, error, 'Error in a rendering stream');
|
|
157
157
|
errorReporter.message(msg);
|
|
158
158
|
});
|
|
159
159
|
return newStreamAfterHandlingError;
|
|
@@ -172,7 +172,7 @@ ${smartTrim(result)}`);
|
|
|
172
172
|
|
|
173
173
|
return result;
|
|
174
174
|
} catch (exception) {
|
|
175
|
-
const exceptionMessage = formatExceptionMessage(renderingRequest, exception);
|
|
175
|
+
const exceptionMessage = formatExceptionMessage({ renderingRequest }, exception);
|
|
176
176
|
log.debug('Caught exception in rendering request: %s', exceptionMessage);
|
|
177
177
|
return Promise.resolve({ exceptionMessage });
|
|
178
178
|
}
|
package/src/worker.ts
CHANGED
|
@@ -23,7 +23,9 @@ import {
|
|
|
23
23
|
type ProvidedNewBundle,
|
|
24
24
|
} from './worker/handleRenderRequest.js';
|
|
25
25
|
import handleGracefulShutdown from './worker/handleGracefulShutdown.js';
|
|
26
|
+
import { handleStartupListenError } from './worker/startupErrorHandler.js';
|
|
26
27
|
import {
|
|
28
|
+
badRequestResponseResult,
|
|
27
29
|
errorResponseResult,
|
|
28
30
|
formatExceptionMessage,
|
|
29
31
|
ResponseResult,
|
|
@@ -141,6 +143,56 @@ export const disableHttp2 = () => {
|
|
|
141
143
|
|
|
142
144
|
type WithBodyArrayField<T, K extends string> = T & { [P in K | `${K}[]`]?: string | string[] };
|
|
143
145
|
|
|
146
|
+
const INVALID_CONTENT_LENGTH_ERROR_CODE = 'FST_ERR_CTP_INVALID_CONTENT_LENGTH';
|
|
147
|
+
|
|
148
|
+
const errorCode = (error: unknown): string | undefined => {
|
|
149
|
+
const code = (error as { code?: unknown })?.code;
|
|
150
|
+
return typeof code === 'string' ? code : undefined;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const isValidRenderingRequest = (value: unknown): value is string =>
|
|
154
|
+
typeof value === 'string' && value.length > 0;
|
|
155
|
+
|
|
156
|
+
const SENSITIVE_REQUEST_BODY_KEYS = new Set([
|
|
157
|
+
'password',
|
|
158
|
+
'token',
|
|
159
|
+
'secret',
|
|
160
|
+
'api_key',
|
|
161
|
+
'api-key',
|
|
162
|
+
'apikey',
|
|
163
|
+
'authorization',
|
|
164
|
+
'auth_token',
|
|
165
|
+
'auth-token',
|
|
166
|
+
'authtoken',
|
|
167
|
+
'access_token',
|
|
168
|
+
'accesstoken',
|
|
169
|
+
'bearer',
|
|
170
|
+
'credentials',
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
const invalidRenderingRequestMessage = (body: Record<string, unknown>) => {
|
|
174
|
+
const { renderingRequest } = body;
|
|
175
|
+
let renderingRequestType: string = typeof renderingRequest;
|
|
176
|
+
if (renderingRequest === null) {
|
|
177
|
+
renderingRequestType = 'null';
|
|
178
|
+
} else if (Array.isArray(renderingRequest)) {
|
|
179
|
+
renderingRequestType = 'array';
|
|
180
|
+
} else if (renderingRequest === '') {
|
|
181
|
+
renderingRequestType = 'empty string';
|
|
182
|
+
}
|
|
183
|
+
const bodyKeys = Object.keys(body).filter(
|
|
184
|
+
(key) => key !== 'renderingRequest' && !SENSITIVE_REQUEST_BODY_KEYS.has(key.toLowerCase()),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
return [
|
|
188
|
+
'Invalid "renderingRequest" field in render request.',
|
|
189
|
+
'Expected a non-empty string of JavaScript to execute in the SSR VM.',
|
|
190
|
+
`Received type: ${renderingRequestType}.`,
|
|
191
|
+
`Received body keys: ${bodyKeys.length > 0 ? bodyKeys.join(', ') : '(none)'}.`,
|
|
192
|
+
'Likely causes: request body truncation, malformed multipart form data, or Content-Length mismatch in a proxy/client.',
|
|
193
|
+
].join('\n');
|
|
194
|
+
};
|
|
195
|
+
|
|
144
196
|
const extractBodyArrayField = <Key extends string>(
|
|
145
197
|
body: WithBodyArrayField<Record<string, unknown>, Key>,
|
|
146
198
|
key: Key,
|
|
@@ -178,7 +230,17 @@ export default function run(config: Partial<Config>) {
|
|
|
178
230
|
// We shouldn't have unhandled errors here, but just in case
|
|
179
231
|
app.addHook('onError', (req, res, err, done) => {
|
|
180
232
|
// Not errorReporter.error so that integrations can decide how to log the errors.
|
|
181
|
-
|
|
233
|
+
if (errorCode(err) === INVALID_CONTENT_LENGTH_ERROR_CODE) {
|
|
234
|
+
app.log.error({
|
|
235
|
+
msg: 'Invalid request body framing',
|
|
236
|
+
hint: 'Body size did not match Content-Length. Check client/proxy truncation and Content-Length handling.',
|
|
237
|
+
err,
|
|
238
|
+
req,
|
|
239
|
+
res,
|
|
240
|
+
});
|
|
241
|
+
} else {
|
|
242
|
+
app.log.error({ msg: 'Unhandled Fastify error', err, req, res });
|
|
243
|
+
}
|
|
182
244
|
done();
|
|
183
245
|
});
|
|
184
246
|
|
|
@@ -283,12 +345,7 @@ export default function run(config: Partial<Config>) {
|
|
|
283
345
|
// the digest is part of the request URL. Yes, it's not used here, but the
|
|
284
346
|
// server logs might show it to distinguish different requests.
|
|
285
347
|
app.post<{
|
|
286
|
-
Body: WithBodyArrayField<
|
|
287
|
-
{
|
|
288
|
-
renderingRequest: string;
|
|
289
|
-
},
|
|
290
|
-
'dependencyBundleTimestamps'
|
|
291
|
-
>;
|
|
348
|
+
Body: WithBodyArrayField<Record<string, unknown>, 'dependencyBundleTimestamps'>;
|
|
292
349
|
// Can't infer from the route like Express can
|
|
293
350
|
Params: { bundleTimestamp: string; renderRequestDigest: string };
|
|
294
351
|
}>('/bundles/:bundleTimestamp/render/:renderRequestDigest', async (req, res) => {
|
|
@@ -307,12 +364,22 @@ export default function run(config: Partial<Config>) {
|
|
|
307
364
|
// await delay(100000);
|
|
308
365
|
// }
|
|
309
366
|
|
|
310
|
-
const {
|
|
367
|
+
const { body } = req;
|
|
368
|
+
if (!body || typeof body !== 'object') {
|
|
369
|
+
await setResponse(badRequestResponseResult('Invalid or missing request body.'), res);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const { renderingRequest } = body;
|
|
373
|
+
if (!isValidRenderingRequest(renderingRequest)) {
|
|
374
|
+
await setResponse(badRequestResponseResult(invalidRenderingRequestMessage(body)), res);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
311
378
|
const { bundleTimestamp } = req.params;
|
|
312
|
-
const { providedNewBundles, assetsToCopy } = extractBundlesAndAssets(
|
|
379
|
+
const { providedNewBundles, assetsToCopy } = extractBundlesAndAssets(body, bundleTimestamp);
|
|
313
380
|
|
|
314
381
|
try {
|
|
315
|
-
const dependencyBundleTimestamps = extractBodyArrayField(
|
|
382
|
+
const dependencyBundleTimestamps = extractBodyArrayField(body, 'dependencyBundleTimestamps');
|
|
316
383
|
await trace(async (context) => {
|
|
317
384
|
try {
|
|
318
385
|
const result = await handleRenderRequest({
|
|
@@ -326,7 +393,7 @@ export default function run(config: Partial<Config>) {
|
|
|
326
393
|
await setResponse(result, res);
|
|
327
394
|
} catch (err) {
|
|
328
395
|
const exceptionMessage = formatExceptionMessage(
|
|
329
|
-
renderingRequest,
|
|
396
|
+
{ renderingRequest },
|
|
330
397
|
err,
|
|
331
398
|
'UNHANDLED error in handleRenderRequest',
|
|
332
399
|
);
|
|
@@ -334,7 +401,7 @@ export default function run(config: Partial<Config>) {
|
|
|
334
401
|
}
|
|
335
402
|
}, startSsrRequestOptions({ renderingRequest }));
|
|
336
403
|
} catch (theErr) {
|
|
337
|
-
const exceptionMessage = formatExceptionMessage(renderingRequest, theErr);
|
|
404
|
+
const exceptionMessage = formatExceptionMessage({ renderingRequest }, theErr);
|
|
338
405
|
await setResponse(errorResponseResult(`Unhandled top level error: ${exceptionMessage}`), res);
|
|
339
406
|
}
|
|
340
407
|
});
|
|
@@ -371,7 +438,11 @@ export default function run(config: Partial<Config>) {
|
|
|
371
438
|
// endpoint so that concurrent /upload-assets and render requests
|
|
372
439
|
// targeting the same bundle directory are mutually exclusive.
|
|
373
440
|
// See https://github.com/shakacode/react_on_rails/issues/2463
|
|
374
|
-
const result = await handleNewBundlesProvided(
|
|
441
|
+
const result = await handleNewBundlesProvided(
|
|
442
|
+
{ label: 'Request:', content: taskDescription },
|
|
443
|
+
providedNewBundles,
|
|
444
|
+
assetsToCopy,
|
|
445
|
+
);
|
|
375
446
|
if (result) {
|
|
376
447
|
await setResponse(result, res);
|
|
377
448
|
return;
|
|
@@ -449,8 +520,8 @@ export default function run(config: Partial<Config>) {
|
|
|
449
520
|
if (workersCount === 0 || cluster.isWorker) {
|
|
450
521
|
app.listen({ port, host }, (err, address) => {
|
|
451
522
|
if (err) {
|
|
452
|
-
|
|
453
|
-
|
|
523
|
+
handleStartupListenError({ err, host, port });
|
|
524
|
+
return;
|
|
454
525
|
}
|
|
455
526
|
const workerName = worker ? `worker #${worker.id}` : 'master (single-process)';
|
|
456
527
|
log.info({ workerName, address }, 'Node renderer listening');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
describe('configBuilder', () => {
|
|
2
2
|
const envVarsToRestore = [
|
|
3
3
|
'RENDERER_HOST',
|
|
4
|
+
'RENDERER_PORT',
|
|
4
5
|
'NODE_ENV',
|
|
5
6
|
'RENDERER_PASSWORD',
|
|
6
7
|
'RAILS_ENV',
|
|
@@ -113,6 +114,39 @@ describe('configBuilder', () => {
|
|
|
113
114
|
expect(finalSettings.password).toBe('<EMPTY STRING>');
|
|
114
115
|
});
|
|
115
116
|
|
|
117
|
+
describe('port validation', () => {
|
|
118
|
+
it('throws when configured port is outside the valid TCP range', () => {
|
|
119
|
+
process.env.NODE_ENV = 'development';
|
|
120
|
+
process.env.RAILS_ENV = 'development';
|
|
121
|
+
const processExit = mockProcessExit();
|
|
122
|
+
const { buildConfig, error } = loadConfigBuilderWithMockedLogger();
|
|
123
|
+
|
|
124
|
+
expect(() => buildConfig({ port: 70000 })).toThrow('process.exit: 1');
|
|
125
|
+
expect(processExit).toHaveBeenCalledWith(1);
|
|
126
|
+
expect(error).toHaveBeenCalledWith(
|
|
127
|
+
'RENDERER_PORT must be an integer between 0 and 65535. Received: 70000',
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('allows port 0 for ephemeral-port test setups', () => {
|
|
132
|
+
process.env.NODE_ENV = 'development';
|
|
133
|
+
process.env.RAILS_ENV = 'development';
|
|
134
|
+
const { buildConfig } = loadConfigBuilderWithMockedLogger();
|
|
135
|
+
|
|
136
|
+
expect(buildConfig({ port: 0 }).port).toBe(0);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('coerces a string port from env vars to a number', () => {
|
|
140
|
+
process.env.NODE_ENV = 'development';
|
|
141
|
+
process.env.RAILS_ENV = 'development';
|
|
142
|
+
const { buildConfig } = loadConfigBuilderWithMockedLogger();
|
|
143
|
+
|
|
144
|
+
// Simulates `port: env.RENDERER_PORT || 3800` where env var is the string "3800"
|
|
145
|
+
const config = buildConfig({ port: '3800' as unknown as number });
|
|
146
|
+
expect(config.port).toBe(3800);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
116
150
|
describe('password validation in production-like environments', () => {
|
|
117
151
|
it('throws when no password is set in production', () => {
|
|
118
152
|
process.env.NODE_ENV = 'production';
|