From f98020138ab7481c77402f29e086202924e3afec Mon Sep 17 00:00:00 2001
From: Antoine du Hamel <duhamelantoine1995@gmail.com>
Date: Sun, 24 Jul 2022 23:11:16 +0200
Subject: [PATCH] test_runner: validate `timeout` option

PR-URL: https://github.com/nodejs/node/pull/43843
Reviewed-By: Feng Yu <F3n67u@outlook.com>
---
 lib/internal/test_runner/test.js               | 10 ++++++++--
 lib/internal/validators.js                     | 11 ++++++++++-
 test/parallel/test-runner-option-validation.js | 15 +++++++++++++++
 3 files changed, 33 insertions(+), 3 deletions(-)
 create mode 100644 test/parallel/test-runner-option-validation.js

diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js
index d2be83364a0f6d..fadba74d72da6e 100644
--- a/lib/internal/test_runner/test.js
+++ b/lib/internal/test_runner/test.js
@@ -31,8 +31,13 @@ const {
   kEmptyObject,
 } = require('internal/util');
 const { isPromise } = require('internal/util/types');
-const { isUint32, validateAbortSignal } = require('internal/validators');
+const {
+  isUint32,
+  validateAbortSignal,
+  validateNumber,
+} = require('internal/validators');
 const { setTimeout } = require('timers/promises');
+const { TIMEOUT_MAX } = require('internal/timers');
 const { cpus } = require('os');
 const { bigint: hrtime } = process.hrtime;
 const kCallbackAndPromisePresent = 'callbackAndPromisePresent';
@@ -148,7 +153,8 @@ class Test extends AsyncResource {
       this.concurrency = concurrency;
     }
 
-    if (isUint32(timeout)) {
+    if (timeout != null && timeout !== Infinity) {
+      validateNumber(timeout, 'options.timeout', 0, TIMEOUT_MAX);
       this.timeout = timeout;
     }
 
diff --git a/lib/internal/validators.js b/lib/internal/validators.js
index 3bdd5285a39c31..09c836e7258276 100644
--- a/lib/internal/validators.js
+++ b/lib/internal/validators.js
@@ -6,6 +6,7 @@ const {
   ArrayPrototypeJoin,
   ArrayPrototypeMap,
   NumberIsInteger,
+  NumberIsNaN,
   NumberMAX_SAFE_INTEGER,
   NumberMIN_SAFE_INTEGER,
   NumberParseInt,
@@ -115,9 +116,17 @@ function validateString(value, name) {
     throw new ERR_INVALID_ARG_TYPE(name, 'string', value);
 }
 
-function validateNumber(value, name) {
+function validateNumber(value, name, min = undefined, max) {
   if (typeof value !== 'number')
     throw new ERR_INVALID_ARG_TYPE(name, 'number', value);
+
+  if ((min != null && value < min) || (max != null && value > max) ||
+      ((min != null || max != null) && NumberIsNaN(value))) {
+    throw new ERR_OUT_OF_RANGE(
+      name,
+      `${min != null ? `>= ${min}` : ''}${min != null && max != null ? ' && ' : ''}${max != null ? `<= ${max}` : ''}`,
+      value);
+  }
 }
 
 const validateOneOf = hideStackFrames((value, name, oneOf) => {
diff --git a/test/parallel/test-runner-option-validation.js b/test/parallel/test-runner-option-validation.js
new file mode 100644
index 00000000000000..a6b7cb1826b166
--- /dev/null
+++ b/test/parallel/test-runner-option-validation.js
@@ -0,0 +1,15 @@
+'use strict';
+require('../common');
+const assert = require('assert');
+const test = require('node:test');
+
+[Symbol(), {}, [], () => {}, 1n, true, '1'].forEach((timeout) => {
+  assert.throws(() => test({ timeout }), { code: 'ERR_INVALID_ARG_TYPE' });
+});
+[-1, -Infinity, NaN, 2 ** 33, Number.MAX_SAFE_INTEGER].forEach((timeout) => {
+  assert.throws(() => test({ timeout }), { code: 'ERR_OUT_OF_RANGE' });
+});
+[null, undefined, Infinity, 0, 1, 1.1].forEach((timeout) => {
+  // Valid values should not throw.
+  test({ timeout });
+});