kempo-server 1.4.6 → 1.4.7

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/README.md CHANGED
@@ -211,7 +211,7 @@ export default async function(request, response) {
211
211
 
212
212
  ## Configuration
213
213
 
214
- To configure the server create a `.config.json` within the root directory of your server (`public` in the start example [above](#getting-started)).
214
+ To configure the server, create a configuration file (by default `.config.json`) within the root directory of your server (`public` in the start example [above](#getting-started)). You can specify a different configuration file using the `--config` flag.
215
215
 
216
216
  This json file can have any of the following properties, any property not defined will use the "Default Config".
217
217
 
@@ -620,12 +620,31 @@ Kempo Server supports several command line options to customize its behavior:
620
620
  - `--root <path>` - Set the document root directory (required)
621
621
  - `--port <number>` - Set the port number (default: 3000)
622
622
  - `--host <address>` - Set the host address (default: localhost)
623
+ - `--config <path>` - Set the configuration file path (default: `.config.json`)
623
624
  - `--verbose` - Enable verbose logging
624
625
 
625
626
  ```bash
626
627
  kempo-server --root public --port 8080 --host 0.0.0.0 --verbose
627
628
  ```
628
629
 
630
+ ### Configuration File Examples
631
+
632
+ You can specify different configuration files for different environments:
633
+
634
+ ```bash
635
+ # Development
636
+ kempo-server --root public --config dev.config.json
637
+
638
+ # Staging
639
+ kempo-server --root public --config staging.config.json
640
+
641
+ # Production with absolute path
642
+ kempo-server --root public --config /etc/kempo/production.config.json
643
+
644
+ # Mix with other options
645
+ kempo-server --root dist --port 8080 --config production.config.json --scan
646
+ ```
647
+
629
648
  ## Testing
630
649
 
631
650
  This project uses the Kempo Testing Framework. Tests live in the `tests/` folder and follow these naming conventions:
@@ -15,7 +15,8 @@
15
15
  <p>Customize Kempo Server's behavior with a simple JSON configuration file.</p>
16
16
 
17
17
  <h2>Configuration File</h2>
18
- <p>To configure the server create a <code>.config.json</code> within the root directory of your server (<code>public</code> in the start example).</p>
18
+ <p>To configure the server, create a configuration file (by default <code>.config.json</code>) within the root directory of your server (<code>public</code> in the start example). You can specify a different configuration file using the <code>--config</code> flag:</p>
19
+ <pre><code class="hljs bash"># Use default .config.json<br />kempo-server --root public<br /><br /># Use custom config file<br />kempo-server --root public --config staging.config.json<br /><br /># Use absolute path<br />kempo-server --root public --config /etc/kempo/production.config.json</code></pre>
19
20
  <p>This json file can have any of the following properties. Any property not defined will use the default configuration.</p>
20
21
 
21
22
  <h2>Configuration Options</h2>
@@ -78,15 +79,21 @@
78
79
  <pre><code class="hljs json">{<br /> <span class="hljs-attr">"allowedMimes"</span>: {<br /> <span class="hljs-attr">"html"</span>: <span class="hljs-string">"text/html"</span>,<br /> <span class="hljs-attr">"css"</span>: <span class="hljs-string">"text/css"</span>,<br /> <span class="hljs-attr">"js"</span>: <span class="hljs-string">"application/javascript"</span>,<br /> <span class="hljs-attr">"json"</span>: <span class="hljs-string">"application/json"</span>,<br /> <span class="hljs-attr">"png"</span>: <span class="hljs-string">"image/png"</span>,<br /> <span class="hljs-attr">"jpg"</span>: <span class="hljs-string">"image/jpeg"</span>,<br /> <span class="hljs-attr">"jpeg"</span>: <span class="hljs-string">"image/jpeg"</span>,<br /> <span class="hljs-attr">"gif"</span>: <span class="hljs-string">"image/gif"</span>,<br /> <span class="hljs-attr">"svg"</span>: <span class="hljs-string">"image/svg+xml"</span>,<br /> <span class="hljs-attr">"woff"</span>: <span class="hljs-string">"font/woff"</span>,<br /> <span class="hljs-attr">"woff2"</span>: <span class="hljs-string">"font/woff2"</span><br /> },<br /> <span class="hljs-attr">"disallowedRegex"</span>: [<br /> <span class="hljs-string">"^/\\..*"</span>,<br /> <span class="hljs-string">"\\.env$"</span>,<br /> <span class="hljs-string">"\\.config$"</span>,<br /> <span class="hljs-string">"password"</span>,<br /> <span class="hljs-string">"node_modules"</span>,<br /> <span class="hljs-string">"\\.git"</span><br /> ],<br /> <span class="hljs-attr">"routeFiles"</span>: [<br /> <span class="hljs-string">"GET.js"</span>,<br /> <span class="hljs-string">"POST.js"</span>,<br /> <span class="hljs-string">"PUT.js"</span>,<br /> <span class="hljs-string">"DELETE.js"</span>,<br /> <span class="hljs-string">"PATCH.js"</span>,<br /> <span class="hljs-string">"HEAD.js"</span>,<br /> <span class="hljs-string">"OPTIONS.js"</span>,<br /> <span class="hljs-string">"index.js"</span><br /> ],<br /> <span class="hljs-attr">"noRescanPaths"</span>: [<br /> <span class="hljs-string">"/favicon\\.ico$"</span>,<br /> <span class="hljs-string">"/robots\\.txt$"</span>,<br /> <span class="hljs-string">"\\.map$"</span>,<br /> <span class="hljs-string">"\\.css$"</span>,<br /> <span class="hljs-string">"\\.js$"</span>,<br /> <span class="hljs-string">"\\.png$"</span>,<br /> <span class="hljs-string">"\\.jpg$"</span>,<br /> <span class="hljs-string">"\\.jpeg$"</span>,<br /> <span class="hljs-string">"\\.gif$"</span><br /> ],<br /> <span class="hljs-attr">"customRoutes"</span>: {<br /> <span class="hljs-attr">"/vendor/bootstrap.css"</span>: <span class="hljs-string">"./node_modules/bootstrap/dist/css/bootstrap.min.css"</span>,<br /> <span class="hljs-attr">"/vendor/jquery.js"</span>: <span class="hljs-string">"./node_modules/jquery/dist/jquery.min.js"</span>,<br /> <span class="hljs-attr">"assets/*"</span>: <span class="hljs-string">"./static-files/*"</span>,<br /> <span class="hljs-attr">"docs/*"</span>: <span class="hljs-string">"./documentation/*"</span><br /> },<br /> <span class="hljs-attr">"maxRescanAttempts"</span>: <span class="hljs-number">3</span>,<br /> <span class="hljs-attr">"middleware"</span>: {<br /> <span class="hljs-attr">"cors"</span>: {<br /> <span class="hljs-attr">"enabled"</span>: <span class="hljs-literal">true</span>,<br /> <span class="hljs-attr">"origin"</span>: <span class="hljs-string">"*"</span>,<br /> <span class="hljs-attr">"methods"</span>: [<span class="hljs-string">"GET"</span>, <span class="hljs-string">"POST"</span>, <span class="hljs-string">"PUT"</span>, <span class="hljs-string">"DELETE"</span>],<br /> <span class="hljs-attr">"headers"</span>: [<span class="hljs-string">"Content-Type"</span>, <span class="hljs-string">"Authorization"</span>]<br /> },<br /> <span class="hljs-attr">"compression"</span>: {<br /> <span class="hljs-attr">"enabled"</span>: <span class="hljs-literal">true</span>,<br /> <span class="hljs-attr">"threshold"</span>: <span class="hljs-number">1024</span><br /> },<br /> <span class="hljs-attr">"security"</span>: {<br /> <span class="hljs-attr">"enabled"</span>: <span class="hljs-literal">true</span>,<br /> <span class="hljs-attr">"headers"</span>: {<br /> <span class="hljs-attr">"X-Content-Type-Options"</span>: <span class="hljs-string">"nosniff"</span>,<br /> <span class="hljs-attr">"X-Frame-Options"</span>: <span class="hljs-string">"DENY"</span>,<br /> <span class="hljs-attr">"X-XSS-Protection"</span>: <span class="hljs-string">"1; mode=block"</span><br /> }<br /> },<br /> <span class="hljs-attr">"custom"</span>: [<br /> <span class="hljs-string">"./middleware/auth.js"</span>,<br /> <span class="hljs-string">"./middleware/logging.js"</span><br /> ]<br /> }<br />}</code></pre>
79
80
 
80
81
  <h2>Environment-Specific Configuration</h2>
81
- <p>You can create different configuration files for different environments:</p>
82
+ <p>You can create different configuration files for different environments and specify them using the <code>--config</code> flag:</p>
82
83
 
83
84
  <h3>Development Configuration</h3>
84
85
  <p>Create <code>.config.dev.json</code> for development:</p>
85
86
  <pre><code class="hljs json">{<br /> <span class="hljs-attr">"allowedMimes"</span>: {<br /> <span class="hljs-attr">"html"</span>: <span class="hljs-string">"text/html"</span>,<br /> <span class="hljs-attr">"css"</span>: <span class="hljs-string">"text/css"</span>,<br /> <span class="hljs-attr">"js"</span>: <span class="hljs-string">"application/javascript"</span>,<br /> <span class="hljs-attr">"json"</span>: <span class="hljs-string">"application/json"</span>,<br /> <span class="hljs-attr">"map"</span>: <span class="hljs-string">"application/json"</span><br /> },<br /> <span class="hljs-attr">"middleware"</span>: {<br /> <span class="hljs-attr">"cors"</span>: {<br /> <span class="hljs-attr">"enabled"</span>: <span class="hljs-literal">true</span>,<br /> <span class="hljs-attr">"origin"</span>: <span class="hljs-string">"*"</span><br /> },<br /> <span class="hljs-attr">"compression"</span>: {<br /> <span class="hljs-attr">"enabled"</span>: <span class="hljs-literal">false</span><br /> }<br /> }<br />}</code></pre>
87
+ <p>Use with: <code>kempo-server --root public --config .config.dev.json</code></p>
86
88
 
87
89
  <h3>Production Configuration</h3>
88
90
  <p>Create <code>.config.prod.json</code> for production:</p>
89
91
  <pre><code class="hljs json">{<br /> <span class="hljs-attr">"allowedMimes"</span>: {<br /> <span class="hljs-attr">"html"</span>: <span class="hljs-string">"text/html"</span>,<br /> <span class="hljs-attr">"css"</span>: <span class="hljs-string">"text/css"</span>,<br /> <span class="hljs-attr">"js"</span>: <span class="hljs-string">"application/javascript"</span>,<br /> <span class="hljs-attr">"json"</span>: <span class="hljs-string">"application/json"</span>,<br /> <span class="hljs-attr">"png"</span>: <span class="hljs-string">"image/png"</span>,<br /> <span class="hljs-attr">"jpg"</span>: <span class="hljs-string">"image/jpeg"</span><br /> },<br /> <span class="hljs-attr">"disallowedRegex"</span>: [<br /> <span class="hljs-string">"^/\\..*"</span>,<br /> <span class="hljs-string">"\\.env$"</span>,<br /> <span class="hljs-string">"\\.config$"</span>,<br /> <span class="hljs-string">"password"</span>,<br /> <span class="hljs-string">"node_modules"</span>,<br /> <span class="hljs-string">"\\.git"</span>,<br /> <span class="hljs-string">"\\.map$"</span><br /> ],<br /> <span class="hljs-attr">"middleware"</span>: {<br /> <span class="hljs-attr">"cors"</span>: {<br /> <span class="hljs-attr">"enabled"</span>: <span class="hljs-literal">true</span>,<br /> <span class="hljs-attr">"origin"</span>: <span class="hljs-string">"https://yourdomain.com"</span><br /> },<br /> <span class="hljs-attr">"compression"</span>: {<br /> <span class="hljs-attr">"enabled"</span>: <span class="hljs-literal">true</span>,<br /> <span class="hljs-attr">"threshold"</span>: <span class="hljs-number">1024</span><br /> },<br /> <span class="hljs-attr">"security"</span>: {<br /> <span class="hljs-attr">"enabled"</span>: <span class="hljs-literal">true</span>,<br /> <span class="hljs-attr">"headers"</span>: {<br /> <span class="hljs-attr">"X-Content-Type-Options"</span>: <span class="hljs-string">"nosniff"</span>,<br /> <span class="hljs-attr">"X-Frame-Options"</span>: <span class="hljs-string">"DENY"</span>,<br /> <span class="hljs-attr">"X-XSS-Protection"</span>: <span class="hljs-string">"1; mode=block"</span>,<br /> <span class="hljs-attr">"Strict-Transport-Security"</span>: <span class="hljs-string">"max-age=31536000; includeSubDomains"</span><br /> }<br /> }<br /> }<br />}</code></pre>
92
+ <p>Use with: <code>kempo-server --root public --config .config.prod.json</code></p>
93
+
94
+ <h3>Package.json Scripts</h3>
95
+ <p>Add environment-specific scripts to your <code>package.json</code>:</p>
96
+ <pre><code class="hljs json">{<br /> <span class="hljs-attr">"scripts"</span>: {<br /> <span class="hljs-attr">"start"</span>: <span class="hljs-string">"kempo-server --root public"</span>,<br /> <span class="hljs-attr">"start:dev"</span>: <span class="hljs-string">"kempo-server --root public --config .config.dev.json --verbose"</span>,<br /> <span class="hljs-attr">"start:staging"</span>: <span class="hljs-string">"kempo-server --root public --config .config.staging.json"</span>,<br /> <span class="hljs-attr">"start:prod"</span>: <span class="hljs-string">"kempo-server --root public --config .config.prod.json"</span><br /> }<br />}</code></pre>
90
97
 
91
98
  <h2>Configuration Tips</h2>
92
99
 
@@ -51,12 +51,17 @@
51
51
  <li><code>--root &lt;path&gt;</code> - Set the document root directory (required)</li>
52
52
  <li><code>--port &lt;number&gt;</code> - Set the port number (default: 3000)</li>
53
53
  <li><code>--host &lt;address&gt;</code> - Set the host address (default: localhost)</li>
54
+ <li><code>--config &lt;path&gt;</code> - Set the configuration file path (default: .config.json)</li>
54
55
  <li><code>--verbose</code> - Enable verbose logging</li>
55
56
  </ul>
56
57
 
57
- <p>Example:</p>
58
+ <p>Basic example:</p>
58
59
  <pre><code>kempo-server --root public --port 8080 --host 0.0.0.0 --verbose</code></pre>
59
60
 
61
+ <h3>Configuration File Examples</h3>
62
+ <p>You can specify different configuration files for different environments:</p>
63
+ <pre><code class="hljs bash"># Development<br />kempo-server --root public --config dev.config.json<br /><br /># Staging<br />kempo-server --root public --config staging.config.json<br /><br /># Production with absolute path<br />kempo-server --root public --config /etc/kempo/production.config.json<br /><br /># Mix with other options<br />kempo-server --root dist --port 8080 --config production.config.json --scan</code></pre>
64
+
60
65
  <h2>What's Next?</h2>
61
66
  <p>Now that you have Kempo Server running, explore these topics:</p>
62
67
  <ul>
package/index.js CHANGED
@@ -7,12 +7,14 @@ const flags = getFlags(process.argv.slice(2), {
7
7
  port: 3000,
8
8
  logging: 2,
9
9
  root: './',
10
- scan: false
10
+ scan: false,
11
+ config: '.config.json'
11
12
  }, {
12
13
  p: 'port',
13
14
  l: 'logging',
14
15
  r: 'root',
15
- s: 'scan'
16
+ s: 'scan',
17
+ c: 'config'
16
18
  });
17
19
 
18
20
  if(typeof(flags.logging) === 'string'){
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kempo-server",
3
3
  "type": "module",
4
- "version": "1.4.6",
4
+ "version": "1.4.7",
5
5
  "description": "A lightweight, zero-dependency, file based routing server.",
6
6
  "main": "index.js",
7
7
  "bin": {
package/router.js CHANGED
@@ -21,17 +21,34 @@ export default async (flags, log) => {
21
21
 
22
22
  let config = defaultConfig;
23
23
  try {
24
- const configPath = path.join(rootPath, '.config.json');
24
+ // Use the provided config path or fallback to .config.json in rootPath
25
+ const configFileName = flags.config || '.config.json';
26
+ const configPath = path.isAbsolute(configFileName)
27
+ ? configFileName
28
+ : path.join(rootPath, configFileName);
25
29
  log(`Loading config from: ${configPath}`, 2);
26
30
  const configContent = await readFile(configPath, 'utf8');
27
31
  const userConfig = JSON.parse(configContent);
28
32
  config = {
29
33
  ...defaultConfig,
30
- ...userConfig
34
+ ...userConfig,
35
+ // Deep merge nested objects
36
+ allowedMimes: {
37
+ ...defaultConfig.allowedMimes,
38
+ ...userConfig.allowedMimes
39
+ },
40
+ middleware: {
41
+ ...defaultConfig.middleware,
42
+ ...userConfig.middleware
43
+ },
44
+ customRoutes: {
45
+ ...defaultConfig.customRoutes,
46
+ ...userConfig.customRoutes
47
+ }
31
48
  };
32
49
  log('User config loaded and merged with defaults', 2);
33
50
  } catch (e){
34
- log('Using default config (no .config.json found)', 2);
51
+ log('Using default config (no config file found)', 2);
35
52
  }
36
53
 
37
54
  /*
@@ -1,5 +1,5 @@
1
1
  import {corsMiddleware} from '../builtinMiddleware.js';
2
- import {createMockReq, createMockRes, expect} from './test-utils.js';
2
+ import {createMockReq, createMockRes} from './test-utils.js';
3
3
 
4
4
  export default {
5
5
  'cors origin array and non-OPTIONS continues to next': async ({pass, fail}) => {
@@ -9,8 +9,8 @@ export default {
9
9
  const req = createMockReq({method: 'GET', headers: {origin: 'http://b'}});
10
10
  let called = false;
11
11
  await mw(req, res, async () => { called = true; });
12
- expect(called, 'next not called');
13
- expect(res.getHeader('Access-Control-Allow-Origin') === 'http://b', 'allowed origin');
12
+ if(!called) return fail('next not called');
13
+ if(res.getHeader('Access-Control-Allow-Origin') !== 'http://b') return fail('allowed origin');
14
14
  pass('cors array');
15
15
  } catch(e){ fail(e.message); }
16
16
  }
@@ -1,5 +1,5 @@
1
1
  import http from 'http';
2
- import {createMockReq, createMockRes, expect, bigString, gzipSize, setEnv} from './test-utils.js';
2
+ import {createMockReq, createMockRes, bigString, gzipSize, setEnv} from './test-utils.js';
3
3
  import {corsMiddleware, compressionMiddleware, rateLimitMiddleware, securityMiddleware, loggingMiddleware} from '../builtinMiddleware.js';
4
4
 
5
5
  export default {
@@ -9,8 +9,8 @@ export default {
9
9
  const mw = corsMiddleware({origin: '*', methods: ['GET'], headers: ['X']});
10
10
  const req = createMockReq({method: 'OPTIONS', headers: {origin: 'http://x'}});
11
11
  await mw(req, res, async () => {});
12
- expect(res.isEnded(), 'preflight should end');
13
- expect(res.getHeader('Access-Control-Allow-Origin') === 'http://x' || res.getHeader('Access-Control-Allow-Origin') === '*', 'origin header');
12
+ if(!res.isEnded()) return fail('preflight should end');
13
+ if(!(res.getHeader('Access-Control-Allow-Origin') === 'http://x' || res.getHeader('Access-Control-Allow-Origin') === '*')) return fail('origin header');
14
14
  pass('cors');
15
15
  } catch(e){ fail(e.message); }
16
16
  },
@@ -30,9 +30,9 @@ export default {
30
30
  const gzLen = await gzipSize(original);
31
31
  // If gzipped is smaller, we expect gzip header. Otherwise, implementation may send uncompressed.
32
32
  if(gzLen < original.length){
33
- expect(res.getHeader('Content-Encoding') === 'gzip', 'should gzip when beneficial');
33
+ if(res.getHeader('Content-Encoding') !== 'gzip') return fail('should gzip when beneficial');
34
34
  }
35
- expect(body.length > 0, 'has body');
35
+ if(body.length <= 0) return fail('has body');
36
36
  pass('compression');
37
37
  } catch(e){ fail(e.message); }
38
38
  },
@@ -47,7 +47,7 @@ export default {
47
47
  await mw(req, res2, async () => {});
48
48
  const res3 = createMockRes();
49
49
  await mw(req, res3, async () => {});
50
- expect(res3.statusCode === 429, 'should rate limit');
50
+ if(res3.statusCode !== 429) return fail('should rate limit');
51
51
  pass('rateLimit');
52
52
  } catch(e){ fail(e.message); }
53
53
  },
@@ -56,7 +56,7 @@ export default {
56
56
  const res = createMockRes();
57
57
  const mw = securityMiddleware({headers: {'X-Test': '1'}});
58
58
  await mw(createMockReq(), res, async () => {});
59
- expect(res.getHeader('X-Test') === '1', 'header set');
59
+ if(res.getHeader('X-Test') !== '1') return fail('header set');
60
60
  pass('security');
61
61
  } catch(e){ fail(e.message); }
62
62
  },
@@ -67,7 +67,7 @@ export default {
67
67
  const mw = loggingMiddleware({includeUserAgent: true, includeResponseTime: true}, logger);
68
68
  const res = createMockRes();
69
69
  await mw(createMockReq({headers: {'user-agent': 'UA'}}), res, async () => { res.end('x'); });
70
- expect(logs.length === 1 && logs[0].includes('GET /') && logs[0].includes('UA'), 'logged');
70
+ if(!(logs.length === 1 && logs[0].includes('GET /') && logs[0].includes('UA'))) return fail('logged');
71
71
  pass('logging');
72
72
  } catch(e){ fail(e.message); }
73
73
  }
@@ -0,0 +1,259 @@
1
+ import {startNode, randomPort, httpGet, withTempDir, write} from './test-utils.js';
2
+ import path from 'path';
3
+
4
+ export default {
5
+ 'CLI uses default config file when no --config flag provided': async ({pass, fail}) => {
6
+ await withTempDir(async (dir) => {
7
+ // Create default config and test file
8
+ const defaultConfig = {
9
+ allowedMimes: {
10
+ html: "text/html",
11
+ default: "text/default"
12
+ }
13
+ };
14
+ await write(dir, '.config.json', JSON.stringify(defaultConfig));
15
+ await write(dir, 'test.default', 'default config content');
16
+
17
+ const port = randomPort();
18
+ const args = [path.join(process.cwd(), 'index.js'), '-r', '.', '-p', String(port), '-l', '0'];
19
+ const child = await startNode(args, {cwd: dir});
20
+
21
+ // Wait briefly for server to start
22
+ await new Promise(r => setTimeout(r, 400));
23
+
24
+ try {
25
+ const {res, body} = await httpGet(`http://localhost:${port}/test.default`);
26
+ if (res.statusCode !== 200) {
27
+ return fail('server should serve file with default config');
28
+ }
29
+ if (res.headers['content-type'] !== 'text/default') {
30
+ return fail('should use default config mime type');
31
+ }
32
+ if (body.toString() !== 'default config content') {
33
+ return fail('should serve correct content');
34
+ }
35
+ pass('CLI default config usage');
36
+ } finally {
37
+ child.kill();
38
+ await new Promise(r => setTimeout(r, 50));
39
+ }
40
+ });
41
+ },
42
+
43
+ 'CLI uses custom config file with --config flag': async ({pass, fail}) => {
44
+ await withTempDir(async (dir) => {
45
+ // Create custom config and test file
46
+ const customConfig = {
47
+ allowedMimes: {
48
+ html: "text/html",
49
+ custom: "text/custom"
50
+ }
51
+ };
52
+ await write(dir, 'dev.config.json', JSON.stringify(customConfig));
53
+ await write(dir, 'test.custom', 'custom config content');
54
+
55
+ const port = randomPort();
56
+ const args = [
57
+ path.join(process.cwd(), 'index.js'),
58
+ '-r', '.',
59
+ '-p', String(port),
60
+ '-l', '0',
61
+ '--config', 'dev.config.json'
62
+ ];
63
+ const child = await startNode(args, {cwd: dir});
64
+
65
+ // Wait briefly for server to start
66
+ await new Promise(r => setTimeout(r, 400));
67
+
68
+ try {
69
+ const {res, body} = await httpGet(`http://localhost:${port}/test.custom`);
70
+ if (res.statusCode !== 200) {
71
+ return fail('server should serve file with custom config');
72
+ }
73
+ if (res.headers['content-type'] !== 'text/custom') {
74
+ return fail('should use custom config mime type');
75
+ }
76
+ if (body.toString() !== 'custom config content') {
77
+ return fail('should serve correct content');
78
+ }
79
+ pass('CLI custom config usage');
80
+ } finally {
81
+ child.kill();
82
+ await new Promise(r => setTimeout(r, 50));
83
+ }
84
+ });
85
+ },
86
+
87
+ 'CLI uses custom config file with -c short flag': async ({pass, fail}) => {
88
+ await withTempDir(async (dir) => {
89
+ // Create custom config and test file
90
+ const customConfig = {
91
+ allowedMimes: {
92
+ html: "text/html",
93
+ short: "text/short"
94
+ }
95
+ };
96
+ await write(dir, 'short.config.json', JSON.stringify(customConfig));
97
+ await write(dir, 'test.short', 'short flag content');
98
+
99
+ const port = randomPort();
100
+ const args = [
101
+ path.join(process.cwd(), 'index.js'),
102
+ '-r', '.',
103
+ '-p', String(port),
104
+ '-l', '0',
105
+ '-c', 'short.config.json'
106
+ ];
107
+ const child = await startNode(args, {cwd: dir});
108
+
109
+ // Wait briefly for server to start
110
+ await new Promise(r => setTimeout(r, 400));
111
+
112
+ try {
113
+ const {res, body} = await httpGet(`http://localhost:${port}/test.short`);
114
+ if (res.statusCode !== 200) {
115
+ return fail('server should serve file with short flag config');
116
+ }
117
+ if (res.headers['content-type'] !== 'text/short') {
118
+ return fail('should use short flag config mime type');
119
+ }
120
+ if (body.toString() !== 'short flag content') {
121
+ return fail('should serve correct content');
122
+ }
123
+ pass('CLI short config flag usage');
124
+ } finally {
125
+ child.kill();
126
+ await new Promise(r => setTimeout(r, 50));
127
+ }
128
+ });
129
+ },
130
+
131
+ 'CLI uses absolute path config file': async ({pass, fail}) => {
132
+ await withTempDir(async (dir) => {
133
+ // Create custom config in subdirectory
134
+ const configDir = path.join(dir, 'configs');
135
+ const customConfig = {
136
+ allowedMimes: {
137
+ html: "text/html",
138
+ absolute: "text/absolute"
139
+ }
140
+ };
141
+ const configPath = await write(configDir, 'prod.config.json', JSON.stringify(customConfig));
142
+ await write(dir, 'test.absolute', 'absolute path content');
143
+
144
+ const port = randomPort();
145
+ const args = [
146
+ path.join(process.cwd(), 'index.js'),
147
+ '-r', '.',
148
+ '-p', String(port),
149
+ '-l', '0',
150
+ '--config', configPath
151
+ ];
152
+ const child = await startNode(args, {cwd: dir});
153
+
154
+ // Wait briefly for server to start
155
+ await new Promise(r => setTimeout(r, 400));
156
+
157
+ try {
158
+ const {res, body} = await httpGet(`http://localhost:${port}/test.absolute`);
159
+ if (res.statusCode !== 200) {
160
+ return fail('server should serve file with absolute path config');
161
+ }
162
+ if (res.headers['content-type'] !== 'text/absolute') {
163
+ return fail('should use absolute path config mime type');
164
+ }
165
+ if (body.toString() !== 'absolute path content') {
166
+ return fail('should serve correct content');
167
+ }
168
+ pass('CLI absolute path config usage');
169
+ } finally {
170
+ child.kill();
171
+ await new Promise(r => setTimeout(r, 50));
172
+ }
173
+ });
174
+ },
175
+
176
+ 'CLI gracefully handles missing config file': async ({pass, fail}) => {
177
+ await withTempDir(async (dir) => {
178
+ // Create test file but no config
179
+ await write(dir, 'index.html', '<h1>Home</h1>');
180
+
181
+ const port = randomPort();
182
+ const args = [
183
+ path.join(process.cwd(), 'index.js'),
184
+ '-r', '.',
185
+ '-p', String(port),
186
+ '-l', '0',
187
+ '--config', 'nonexistent.config.json'
188
+ ];
189
+ const child = await startNode(args, {cwd: dir});
190
+
191
+ // Wait briefly for server to start
192
+ await new Promise(r => setTimeout(r, 400));
193
+
194
+ try {
195
+ const {res, body} = await httpGet(`http://localhost:${port}/index.html`);
196
+ if (res.statusCode !== 200) {
197
+ return fail('server should start with missing config');
198
+ }
199
+ if (res.headers['content-type'] !== 'text/html') {
200
+ return fail('should use default mime types');
201
+ }
202
+ if (!body.toString().includes('<h1>Home</h1>')) {
203
+ return fail('should serve HTML content');
204
+ }
205
+ pass('CLI missing config file handling');
206
+ } finally {
207
+ child.kill();
208
+ await new Promise(r => setTimeout(r, 50));
209
+ }
210
+ });
211
+ },
212
+
213
+ 'CLI config flag works with custom routes': async ({pass, fail}) => {
214
+ await withTempDir(async (dir) => {
215
+ // Create custom config with custom routes
216
+ await write(dir, 'source.txt', 'source file content');
217
+ const customConfig = {
218
+ allowedMimes: {
219
+ html: "text/html",
220
+ txt: "text/plain"
221
+ },
222
+ customRoutes: {
223
+ "/custom-route": "./source.txt"
224
+ }
225
+ };
226
+ await write(dir, 'routes.config.json', JSON.stringify(customConfig));
227
+
228
+ const port = randomPort();
229
+ const args = [
230
+ path.join(process.cwd(), 'index.js'),
231
+ '-r', '.',
232
+ '-p', String(port),
233
+ '-l', '0',
234
+ '--config', 'routes.config.json'
235
+ ];
236
+ const child = await startNode(args, {cwd: dir});
237
+
238
+ // Wait briefly for server to start
239
+ await new Promise(r => setTimeout(r, 400));
240
+
241
+ try {
242
+ const {res, body} = await httpGet(`http://localhost:${port}/custom-route`);
243
+ if (res.statusCode !== 200) {
244
+ return fail('custom route should work with config flag');
245
+ }
246
+ if (res.headers['content-type'] !== 'text/plain') {
247
+ return fail('should use config mime type');
248
+ }
249
+ if (body.toString() !== 'source file content') {
250
+ return fail('should serve custom route content');
251
+ }
252
+ pass('CLI config flag with custom routes');
253
+ } finally {
254
+ child.kill();
255
+ await new Promise(r => setTimeout(r, 50));
256
+ }
257
+ });
258
+ }
259
+ };
@@ -0,0 +1,321 @@
1
+ import http from 'http';
2
+ import path from 'path';
3
+ import {withTempDir, write, randomPort, httpGet} from './test-utils.js';
4
+ import router from '../router.js';
5
+ import getFlags from '../getFlags.js';
6
+
7
+ export default {
8
+ 'getFlags includes config flag with default value': async ({pass, fail}) => {
9
+ const args = ['--root', 'public', '--port', '8080'];
10
+ const flags = getFlags(args, {
11
+ port: 3000,
12
+ logging: 2,
13
+ root: './',
14
+ scan: false,
15
+ config: '.config.json'
16
+ }, {
17
+ p: 'port',
18
+ l: 'logging',
19
+ r: 'root',
20
+ s: 'scan',
21
+ c: 'config'
22
+ });
23
+
24
+ if (flags.config !== '.config.json') {
25
+ return fail('default config should be .config.json');
26
+ }
27
+ if (flags.port !== '8080') {
28
+ return fail('other flags should still work');
29
+ }
30
+ if (flags.root !== 'public') {
31
+ return fail('root flag should work');
32
+ }
33
+
34
+ pass('config flag has correct default');
35
+ },
36
+
37
+ 'getFlags parses custom config flag with long form': async ({pass, fail}) => {
38
+ const args = ['--root', 'public', '--config', 'dev.config.json'];
39
+ const flags = getFlags(args, {
40
+ port: 3000,
41
+ logging: 2,
42
+ root: './',
43
+ scan: false,
44
+ config: '.config.json'
45
+ }, {
46
+ p: 'port',
47
+ l: 'logging',
48
+ r: 'root',
49
+ s: 'scan',
50
+ c: 'config'
51
+ });
52
+
53
+ if (flags.config !== 'dev.config.json') {
54
+ return fail('should parse custom config file');
55
+ }
56
+ if (flags.root !== 'public') {
57
+ return fail('other flags should still work');
58
+ }
59
+
60
+ pass('long form config flag parsing');
61
+ },
62
+
63
+ 'getFlags parses custom config flag with short form': async ({pass, fail}) => {
64
+ const args = ['--root', 'public', '-c', 'production.config.json'];
65
+ const flags = getFlags(args, {
66
+ port: 3000,
67
+ logging: 2,
68
+ root: './',
69
+ scan: false,
70
+ config: '.config.json'
71
+ }, {
72
+ p: 'port',
73
+ l: 'logging',
74
+ r: 'root',
75
+ s: 'scan',
76
+ c: 'config'
77
+ });
78
+
79
+ if (flags.config !== 'production.config.json') {
80
+ return fail('should parse short form config flag');
81
+ }
82
+ if (flags.root !== 'public') {
83
+ return fail('other flags should still work');
84
+ }
85
+
86
+ pass('short form config flag parsing');
87
+ },
88
+
89
+ 'router uses default config file when none specified': async ({pass, fail}) => {
90
+ await withTempDir(async (dir) => {
91
+ // Create a custom config file as .config.json (default name)
92
+ const customConfig = {
93
+ allowedMimes: {
94
+ html: "text/html",
95
+ custom: "text/custom"
96
+ }
97
+ };
98
+ await write(dir, '.config.json', JSON.stringify(customConfig));
99
+ await write(dir, 'test.custom', 'custom content');
100
+
101
+ const prev = process.cwd();
102
+ process.chdir(dir);
103
+ const flags = {root: '.', logging: 0, scan: false, config: '.config.json'};
104
+ const logFn = () => {};
105
+ const handler = await router(flags, logFn);
106
+ const server = http.createServer(handler);
107
+ const port = randomPort();
108
+
109
+ await new Promise(r => server.listen(port, r));
110
+ await new Promise(r => setTimeout(r, 50));
111
+
112
+ try {
113
+ const response = await httpGet(`http://localhost:${port}/test.custom`);
114
+ if (response.res.statusCode !== 200) {
115
+ return fail('custom mime type should be served');
116
+ }
117
+ if (response.res.headers['content-type'] !== 'text/custom') {
118
+ return fail('should use custom mime type');
119
+ }
120
+ pass('default config file usage');
121
+ } finally {
122
+ server.close();
123
+ process.chdir(prev);
124
+ }
125
+ });
126
+ },
127
+
128
+ 'router uses custom config file with relative path': async ({pass, fail}) => {
129
+ await withTempDir(async (dir) => {
130
+ // Create a custom config file with different name
131
+ const customConfig = {
132
+ allowedMimes: {
133
+ html: "text/html",
134
+ special: "text/special"
135
+ }
136
+ };
137
+ await write(dir, 'dev.config.json', JSON.stringify(customConfig));
138
+ await write(dir, 'test.special', 'special content');
139
+
140
+ const prev = process.cwd();
141
+ process.chdir(dir);
142
+ const flags = {root: '.', logging: 0, scan: false, config: 'dev.config.json'};
143
+ const logFn = () => {};
144
+ const handler = await router(flags, logFn);
145
+ const server = http.createServer(handler);
146
+ const port = randomPort();
147
+
148
+ await new Promise(r => server.listen(port, r));
149
+ await new Promise(r => setTimeout(r, 50));
150
+
151
+ try {
152
+ const response = await httpGet(`http://localhost:${port}/test.special`);
153
+ if (response.res.statusCode !== 200) {
154
+ return fail('custom config should be loaded');
155
+ }
156
+ if (response.res.headers['content-type'] !== 'text/special') {
157
+ return fail('should use custom config mime type');
158
+ }
159
+ pass('relative path config file usage');
160
+ } finally {
161
+ server.close();
162
+ process.chdir(prev);
163
+ }
164
+ });
165
+ },
166
+
167
+ 'router uses custom config file with absolute path': async ({pass, fail}) => {
168
+ await withTempDir(async (dir) => {
169
+ // Create a custom config file in different location
170
+ const configDir = path.join(dir, 'configs');
171
+ const customConfig = {
172
+ allowedMimes: {
173
+ html: "text/html",
174
+ absolute: "text/absolute"
175
+ }
176
+ };
177
+ const configPath = await write(configDir, 'prod.config.json', JSON.stringify(customConfig));
178
+ await write(dir, 'test.absolute', 'absolute content');
179
+
180
+ const prev = process.cwd();
181
+ process.chdir(dir);
182
+ const flags = {root: '.', logging: 0, scan: false, config: configPath};
183
+ const logFn = () => {};
184
+ const handler = await router(flags, logFn);
185
+ const server = http.createServer(handler);
186
+ const port = randomPort();
187
+
188
+ await new Promise(r => server.listen(port, r));
189
+ await new Promise(r => setTimeout(r, 50));
190
+
191
+ try {
192
+ const response = await httpGet(`http://localhost:${port}/test.absolute`);
193
+ if (response.res.statusCode !== 200) {
194
+ return fail('absolute config path should work');
195
+ }
196
+ if (response.res.headers['content-type'] !== 'text/absolute') {
197
+ return fail('should use absolute config mime type');
198
+ }
199
+ pass('absolute path config file usage');
200
+ } finally {
201
+ server.close();
202
+ process.chdir(prev);
203
+ }
204
+ });
205
+ },
206
+
207
+ 'router falls back to default config when custom config file missing': async ({pass, fail}) => {
208
+ await withTempDir(async (dir) => {
209
+ await write(dir, 'index.html', '<h1>Home</h1>');
210
+
211
+ const prev = process.cwd();
212
+ process.chdir(dir);
213
+ // Point to non-existent config file
214
+ const flags = {root: '.', logging: 0, scan: false, config: 'nonexistent.config.json'};
215
+ const logFn = () => {};
216
+ const handler = await router(flags, logFn);
217
+ const server = http.createServer(handler);
218
+ const port = randomPort();
219
+
220
+ await new Promise(r => server.listen(port, r));
221
+ await new Promise(r => setTimeout(r, 50));
222
+
223
+ try {
224
+ const response = await httpGet(`http://localhost:${port}/index.html`);
225
+ if (response.res.statusCode !== 200) {
226
+ return fail('should fall back to default config and serve HTML');
227
+ }
228
+ if (!response.body.toString().includes('<h1>Home</h1>')) {
229
+ return fail('should serve the file content');
230
+ }
231
+ pass('fallback to default config when file missing');
232
+ } finally {
233
+ server.close();
234
+ process.chdir(prev);
235
+ }
236
+ });
237
+ },
238
+
239
+ 'router handles malformed config file gracefully': async ({pass, fail}) => {
240
+ await withTempDir(async (dir) => {
241
+ // Create malformed JSON config
242
+ await write(dir, 'bad.config.json', '{ invalid json }');
243
+ await write(dir, 'index.html', '<h1>Home</h1>');
244
+
245
+ const prev = process.cwd();
246
+ process.chdir(dir);
247
+ const flags = {root: '.', logging: 0, scan: false, config: 'bad.config.json'};
248
+ const logFn = () => {};
249
+ const handler = await router(flags, logFn);
250
+ const server = http.createServer(handler);
251
+ const port = randomPort();
252
+
253
+ await new Promise(r => server.listen(port, r));
254
+ await new Promise(r => setTimeout(r, 50));
255
+
256
+ try {
257
+ const response = await httpGet(`http://localhost:${port}/index.html`);
258
+ if (response.res.statusCode !== 200) {
259
+ return fail('should fall back to default config with malformed JSON');
260
+ }
261
+ if (!response.body.toString().includes('<h1>Home</h1>')) {
262
+ return fail('should serve the file content');
263
+ }
264
+ pass('graceful handling of malformed config');
265
+ } finally {
266
+ server.close();
267
+ process.chdir(prev);
268
+ }
269
+ });
270
+ },
271
+
272
+ 'router merges custom config with default config': async ({pass, fail}) => {
273
+ await withTempDir(async (dir) => {
274
+ // Create partial config that only overrides some settings
275
+ const partialConfig = {
276
+ allowedMimes: {
277
+ custom: "text/custom"
278
+ },
279
+ maxRescanAttempts: 5
280
+ };
281
+ await write(dir, 'partial.config.json', JSON.stringify(partialConfig));
282
+ await write(dir, 'test.js', 'console.log("test");'); // JS should still work from default config
283
+ await write(dir, 'test.custom', 'custom content');
284
+
285
+ const prev = process.cwd();
286
+ process.chdir(dir);
287
+ const flags = {root: '.', logging: 0, scan: false, config: 'partial.config.json'};
288
+ const logFn = () => {};
289
+ const handler = await router(flags, logFn);
290
+ const server = http.createServer(handler);
291
+ const port = randomPort();
292
+
293
+ await new Promise(r => server.listen(port, r));
294
+ await new Promise(r => setTimeout(r, 50));
295
+
296
+ try {
297
+ // Test that default config is still used for JS files
298
+ const jsResponse = await httpGet(`http://localhost:${port}/test.js`);
299
+ if (jsResponse.res.statusCode !== 200) {
300
+ return fail('JS files should still be served from default config');
301
+ }
302
+ if (jsResponse.res.headers['content-type'] !== 'application/javascript') {
303
+ return fail('should use default JS mime type');
304
+ }
305
+
306
+ // Test that custom config overrides work
307
+ const customResponse = await httpGet(`http://localhost:${port}/test.custom`);
308
+ if (customResponse.res.statusCode !== 200) {
309
+ return fail('custom mime type should work');
310
+ }
311
+ if (customResponse.res.headers['content-type'] !== 'text/custom') {
312
+ return fail('should use custom mime type');
313
+ }
314
+ pass('config merging with defaults');
315
+ } finally {
316
+ server.close();
317
+ process.chdir(prev);
318
+ }
319
+ });
320
+ }
321
+ };
@@ -1,6 +1,6 @@
1
1
  import path from 'path';
2
2
  import url from 'url';
3
- import {createMockReq, createMockRes, expect, setEnv} from './test-utils.js';
3
+ import {createMockReq, createMockRes, setEnv} from './test-utils.js';
4
4
 
5
5
  // import the middleware module by file path to avoid executing index.js
6
6
  const examplePath = path.join(process.cwd(), 'example-middleware.js');
@@ -12,18 +12,18 @@ export default {
12
12
  await setEnv({API_KEY: 'abc'}, async () => {
13
13
  const res1 = createMockRes();
14
14
  await authMiddleware(createMockReq({url:'/private'}), res1, async ()=>{});
15
- expect(res1.statusCode === 401, 'should 401 without key');
15
+ if(res1.statusCode !== 401) return fail('should 401 without key');
16
16
 
17
17
  const res2 = createMockRes();
18
18
  const req2 = createMockReq({headers: {'x-api-key': 'abc'}, url:'/private'});
19
19
  let called = false;
20
20
  await authMiddleware(req2, res2, async ()=>{ called = true; });
21
- expect(called, 'should call next');
22
- expect(req2.user && req2.user.authenticated, 'user attached');
21
+ if(!called) return fail('should call next');
22
+ if(!(req2.user && req2.user.authenticated)) return fail('user attached');
23
23
 
24
24
  const res3 = createMockRes();
25
25
  await authMiddleware(createMockReq({url:'/public/file'}), res3, async ()=>{});
26
- expect(res3.isEnded() === false, 'public should not end');
26
+ if(res3.isEnded() === true) return fail('public should not end');
27
27
  });
28
28
  pass('auth middleware');
29
29
  } catch(e){ fail(e.message); }
@@ -1,7 +1,7 @@
1
1
  import getFiles from '../getFiles.js';
2
2
  import defaultConfig from '../defaultConfig.js';
3
3
  import path from 'path';
4
- import {withTempDir, write, expect, log} from './test-utils.js';
4
+ import {withTempDir, write, log} from './test-utils.js';
5
5
 
6
6
  export default {
7
7
  'scans directories recursively and filters by mime and disallowed': async ({pass, fail}) => {
@@ -14,10 +14,10 @@ export default {
14
14
  await write(dir, 'sub/app.js', 'console.log(1)');
15
15
  const files = await getFiles(dir, cfg, log);
16
16
  const rel = files.map(f => path.relative(dir, f).replace(/\\/g, '/'));
17
- expect(rel.includes('index.html'), 'includes html');
18
- expect(rel.includes('sub/app.js'), 'includes js');
19
- expect(!rel.includes('.env'), 'excludes disallowed');
20
- expect(!rel.includes('notes.xyz'), 'excludes unknown ext');
17
+ if(!rel.includes('index.html')) return fail('includes html');
18
+ if(!rel.includes('sub/app.js')) return fail('includes js');
19
+ if(rel.includes('.env')) return fail('excludes disallowed');
20
+ if(rel.includes('notes.xyz')) return fail('excludes unknown ext');
21
21
  });
22
22
  pass('scan and filter');
23
23
  } catch(e){ fail(e.message); }
@@ -1,4 +1,4 @@
1
- import {startNode, expect, randomPort, httpGet, withTempDir, write} from './test-utils.js';
1
+ import {startNode, randomPort, httpGet, withTempDir, write} from './test-utils.js';
2
2
  import path from 'path';
3
3
 
4
4
  export default {
@@ -12,8 +12,8 @@ export default {
12
12
  // wait briefly for server to start
13
13
  await new Promise(r => setTimeout(r, 400));
14
14
  const {res, body} = await httpGet(`http://localhost:${port}/index.html`);
15
- expect(res.statusCode === 200, 'server running');
16
- expect(body.toString() === 'home', 'served');
15
+ if(res.statusCode !== 200) return fail('server running');
16
+ if(body.toString() !== 'home') return fail('served');
17
17
  child.kill();
18
18
  await new Promise(r => setTimeout(r, 50));
19
19
  });
@@ -1,5 +1,5 @@
1
1
  import MiddlewareRunner from '../middlewareRunner.js';
2
- import {createMockReq, createMockRes, expect} from './test-utils.js';
2
+ import {createMockReq, createMockRes} from './test-utils.js';
3
3
 
4
4
  export default {
5
5
  'runs middleware in order and calls finalHandler': async ({pass, fail}) => {
@@ -11,7 +11,7 @@ export default {
11
11
  const req = createMockReq();
12
12
  const res = createMockRes();
13
13
  await mr.run(req, res, async () => { calls.push('final'); });
14
- expect(calls.join(',') === 'a,b,final,b:after,a:after', 'order incorrect');
14
+ if(calls.join(',') !== 'a,b,final,b:after,a:after') return fail('order incorrect');
15
15
  pass('middleware order');
16
16
  } catch(e){ fail(e.message); }
17
17
  }
@@ -1,4 +1,4 @@
1
- import {createMockReq, expect} from './test-utils.js';
1
+ import {createMockReq} from './test-utils.js';
2
2
  import createRequestWrapper from '../requestWrapper.js';
3
3
 
4
4
  export default {
@@ -6,9 +6,9 @@ export default {
6
6
  try {
7
7
  const req = createMockReq({url: '/user/123?x=1&y=2', headers: {host: 'localhost'}});
8
8
  const wrapped = createRequestWrapper(req, {id: '123'});
9
- expect(wrapped.path === '/user/123', 'path');
10
- expect(wrapped.query.x === '1' && wrapped.query.y === '2', 'query');
11
- expect(wrapped.params.id === '123', 'params');
9
+ if(wrapped.path !== '/user/123') return fail('path');
10
+ if(!(wrapped.query.x === '1' && wrapped.query.y === '2')) return fail('query');
11
+ if(wrapped.params.id !== '123') return fail('params');
12
12
  pass('parsed url');
13
13
  } catch(e){ fail(e.message); }
14
14
  },
@@ -18,15 +18,15 @@ export default {
18
18
  // Each body reader must have its own stream instance
19
19
  const reqText = createMockReq({method: 'POST', url: '/', headers: {host: 'x', 'content-type': 'application/json'}, body: JSON.stringify(payload)});
20
20
  const text = await createRequestWrapper(reqText).text();
21
- expect(text === JSON.stringify(payload), 'text');
21
+ if(text !== JSON.stringify(payload)) return fail('text');
22
22
 
23
23
  const reqJson = createMockReq({method: 'POST', url: '/', headers: {host: 'x', 'content-type': 'application/json'}, body: JSON.stringify(payload)});
24
24
  const obj = await createRequestWrapper(reqJson).json();
25
- expect(obj.a === 1, 'json');
25
+ if(obj.a !== 1) return fail('json');
26
26
 
27
27
  const reqBuf = createMockReq({url: '/', headers: {host: 'x'}, body: 'abc'});
28
28
  const buf = await createRequestWrapper(reqBuf).buffer();
29
- expect(Buffer.isBuffer(buf) && buf.toString() === 'abc', 'buffer');
29
+ if(!(Buffer.isBuffer(buf) && buf.toString() === 'abc')) return fail('buffer');
30
30
  pass('helpers');
31
31
  } catch(e){ fail(e.message); }
32
32
  },
@@ -43,8 +43,8 @@ export default {
43
43
  try {
44
44
  const req = createMockReq({url: '/', headers: {'content-type': 'text/plain', host: 'x'}});
45
45
  const w = createRequestWrapper(req);
46
- expect(w.get('content-type') === 'text/plain', 'get');
47
- expect(w.is('text/plain') === true, 'is');
46
+ if(w.get('content-type') !== 'text/plain') return fail('get');
47
+ if(w.is('text/plain') !== true) return fail('is');
48
48
  pass('header helpers');
49
49
  } catch(e){ fail(e.message); }
50
50
  }
@@ -1,4 +1,4 @@
1
- import {createMockRes, expect, parseCookies} from './test-utils.js';
1
+ import {createMockRes, parseCookies} from './test-utils.js';
2
2
  import createResponseWrapper from '../responseWrapper.js';
3
3
 
4
4
  export default {
@@ -7,9 +7,9 @@ export default {
7
7
  const res = createMockRes();
8
8
  const w = createResponseWrapper(res);
9
9
  w.status(201).set('X-Test', '1').type('json');
10
- expect(res.statusCode === 201, 'status');
11
- expect(res.getHeader('X-Test') === '1', 'set/get');
12
- expect(res.getHeader('Content-Type') === 'application/json', 'type');
10
+ if(res.statusCode !== 201) return fail('status');
11
+ if(res.getHeader('X-Test') !== '1') return fail('set/get');
12
+ if(res.getHeader('Content-Type') !== 'application/json') return fail('type');
13
13
  pass('status+headers+type');
14
14
  } catch(e){ fail(e.message); }
15
15
  },
@@ -18,7 +18,7 @@ export default {
18
18
  const res = createMockRes();
19
19
  const w = createResponseWrapper(res);
20
20
  w.json({a: 1});
21
- expect(res.isEnded(), 'ended');
21
+ if(!res.isEnded()) return fail('ended');
22
22
  try { w.set('X', 'y'); fail('should not set after send'); } catch(_){ /* ok */ }
23
23
  pass('json');
24
24
  } catch(e){ fail(e.message); }
@@ -28,22 +28,22 @@ export default {
28
28
  const res1 = createMockRes();
29
29
  createResponseWrapper(res1).send('hello');
30
30
  // Content-Type defaults to text/html for string when not set
31
- expect(res1.getHeader('Content-Type') === 'text/html', 'string content-type');
32
- expect(res1.getBody().toString() === 'hello', 'string body');
31
+ if(res1.getHeader('Content-Type') !== 'text/html') return fail('string content-type');
32
+ if(res1.getBody().toString() !== 'hello') return fail('string body');
33
33
 
34
34
  const res2 = createMockRes();
35
35
  createResponseWrapper(res2).send({a:1});
36
- expect(res2.getHeader('Content-Type') === 'application/json', 'object content-type');
36
+ if(res2.getHeader('Content-Type') !== 'application/json') return fail('object content-type');
37
37
 
38
38
  const res3 = createMockRes();
39
39
  const buf = Buffer.from('abc');
40
40
  createResponseWrapper(res3).send(buf);
41
41
  const body3 = res3.getBody().toString();
42
- expect(body3.includes('"data"'), 'buffer equal');
42
+ if(!body3.includes('"data"')) return fail('buffer equal');
43
43
 
44
44
  const res4 = createMockRes();
45
45
  createResponseWrapper(res4).send(null);
46
- expect(res4.isEnded(), 'null ended');
46
+ if(!res4.isEnded()) return fail('null ended');
47
47
  pass('send variants');
48
48
  } catch(e){ fail(e.message); }
49
49
  },
@@ -51,11 +51,11 @@ export default {
51
51
  try {
52
52
  const r1 = createMockRes();
53
53
  createResponseWrapper(r1).html('<h1>Ok</h1>');
54
- expect(r1.getHeader('Content-Type') === 'text/html', 'html type');
54
+ if(r1.getHeader('Content-Type') !== 'text/html') return fail('html type');
55
55
 
56
56
  const r2 = createMockRes();
57
57
  createResponseWrapper(r2).text('plain');
58
- expect(r2.getHeader('Content-Type') === 'text/plain', 'text type');
58
+ if(r2.getHeader('Content-Type') !== 'text/plain') return fail('text type');
59
59
  pass('helpers');
60
60
  } catch(e){ fail(e.message); }
61
61
  },
@@ -65,9 +65,9 @@ export default {
65
65
  const w = createResponseWrapper(r);
66
66
  w.cookie('a', 'b', {httpOnly: true, path: '/'});
67
67
  const cookies = parseCookies(r.getHeader('Set-Cookie'));
68
- expect(cookies.length === 1 && cookies[0].includes('a=b'), 'cookie added');
68
+ if(!(cookies.length === 1 && cookies[0].includes('a=b'))) return fail('cookie added');
69
69
  w.redirect('/next', 301);
70
- expect(r.statusCode === 301 && r.getHeader('Location') === '/next', 'redirect');
70
+ if(!(r.statusCode === 301 && r.getHeader('Location') === '/next')) return fail('redirect');
71
71
  pass('redirect+cookie');
72
72
  } catch(e){ fail(e.message); }
73
73
  }
@@ -1,5 +1,5 @@
1
1
  import http from 'http';
2
- import {withTempDir, write, expect, randomPort} from './test-utils.js';
2
+ import {withTempDir, write, randomPort} from './test-utils.js';
3
3
  import router from '../router.js';
4
4
 
5
5
  export default {
@@ -31,13 +31,13 @@ export default {
31
31
  const chunks = []; r.on('data', c => chunks.push(c)); r.on('end', ()=> res({r, body: Buffer.concat(chunks)}));
32
32
  });
33
33
  });
34
- expect(one.r.statusCode === 200, 'first ok');
34
+ if(one.r.statusCode !== 200) return fail('first ok');
35
35
  const two = await new Promise((res)=>{
36
36
  get(`http://localhost:${port}/index.html`, r =>{
37
37
  const chunks = []; r.on('data', c => chunks.push(c)); r.on('end', ()=> res({r, body: Buffer.concat(chunks)}));
38
38
  });
39
39
  });
40
- expect(two.r.statusCode === 429, 'rate limited');
40
+ if(two.r.statusCode !== 429) return fail('rate limited');
41
41
  } finally { server.close(); process.chdir(prev); }
42
42
  });
43
43
  pass('router middleware');
@@ -1,6 +1,6 @@
1
1
  import http from 'http';
2
2
  import path from 'path';
3
- import {withTempDir, write, expect, randomPort, httpGet, log} from './test-utils.js';
3
+ import {withTempDir, write, randomPort, httpGet, log} from './test-utils.js';
4
4
  import router from '../router.js';
5
5
  import defaultConfig from '../defaultConfig.js';
6
6
 
@@ -2,7 +2,7 @@ import serveFile from '../serveFile.js';
2
2
  import findFile from '../findFile.js';
3
3
  import defaultConfig from '../defaultConfig.js';
4
4
  import path from 'path';
5
- import {createMockReq, createMockRes, withTempDir, write, expect, log} from './test-utils.js';
5
+ import {createMockReq, createMockRes, withTempDir, write, log} from './test-utils.js';
6
6
 
7
7
  export default {
8
8
  'serves static file with correct mime': async ({pass, fail}) => {
@@ -13,9 +13,9 @@ export default {
13
13
  const files = [path.join(dir, 'index.html')];
14
14
  const res = createMockRes();
15
15
  const ok = await serveFile(files, dir, '/index.html', 'GET', cfg, createMockReq(), res, log);
16
- expect(ok === true, 'should serve');
17
- expect(res.statusCode === 200, 'status');
18
- expect(res.getHeader('Content-Type') === 'text/html', 'mime');
16
+ if(ok !== true) return fail('should serve');
17
+ if(res.statusCode !== 200) return fail('status');
18
+ if(res.getHeader('Content-Type') !== 'text/html') return fail('mime');
19
19
  });
20
20
  pass('static');
21
21
  } catch(e){ fail(e.message); }
@@ -28,9 +28,9 @@ export default {
28
28
  const files = [path.join(dir, 'api/GET.js')];
29
29
  const res = createMockRes();
30
30
  const ok = await serveFile(files, dir, '/api', 'GET', cfg, createMockReq(), res, log);
31
- expect(ok === true, 'served route');
32
- expect(res.statusCode === 201, 'route status');
33
- expect(res.getBody().toString().includes('ok'), 'body contains ok');
31
+ if(ok !== true) return fail('served route');
32
+ if(res.statusCode !== 201) return fail('route status');
33
+ if(!res.getBody().toString().includes('ok')) return fail('body contains ok');
34
34
  });
35
35
  pass('route exec');
36
36
  } catch(e){ fail(e.message); }
@@ -43,8 +43,8 @@ export default {
43
43
  const files = [path.join(dir, 'api/GET.js')];
44
44
  const res = createMockRes();
45
45
  const ok = await serveFile(files, dir, '/api', 'GET', cfg, createMockReq(), res, log);
46
- expect(ok === true, 'handled');
47
- expect(res.statusCode === 500, '500');
46
+ if(ok !== true) return fail('handled');
47
+ if(res.statusCode !== 500) return fail('500');
48
48
  });
49
49
  pass('route no default');
50
50
  } catch(e){ fail(e.message); }
@@ -92,10 +92,6 @@ export const setEnv = (pairs, fn) => {
92
92
  return fn().finally(restore);
93
93
  };
94
94
 
95
- export const expect = (condition, message) => {
96
- if(!condition){ throw new Error(message); }
97
- };
98
-
99
95
  export const toString = (buf) => Buffer.isBuffer(buf) ? buf.toString() : String(buf);
100
96
 
101
97
  export const gzipSize = async (buf) => {