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 +20 -1
- package/docs/configuration.html +9 -2
- package/docs/getting-started.html +6 -1
- package/index.js +4 -2
- package/package.json +1 -1
- package/router.js +20 -3
- package/tests/builtinMiddleware-cors.node-test.js +3 -3
- package/tests/builtinMiddleware.node-test.js +8 -8
- package/tests/config-flag-cli.node-test.js +259 -0
- package/tests/config-flag.node-test.js +321 -0
- package/tests/example-middleware.node-test.js +5 -5
- package/tests/getFiles.node-test.js +5 -5
- package/tests/index.node-test.js +3 -3
- package/tests/middlewareRunner.node-test.js +2 -2
- package/tests/requestWrapper.node-test.js +9 -9
- package/tests/responseWrapper.node-test.js +14 -14
- package/tests/router-middleware.node-test.js +3 -3
- package/tests/router.node-test.js +1 -1
- package/tests/serveFile.node-test.js +9 -9
- package/tests/test-utils.js +0 -4
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:
|
package/docs/configuration.html
CHANGED
|
@@ -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)
|
|
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 <path></code> - Set the document root directory (required)</li>
|
|
52
52
|
<li><code>--port <number></code> - Set the port number (default: 3000)</li>
|
|
53
53
|
<li><code>--host <address></code> - Set the host address (default: localhost)</li>
|
|
54
|
+
<li><code>--config <path></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>
|
|
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
package/router.js
CHANGED
|
@@ -21,17 +21,34 @@ export default async (flags, log) => {
|
|
|
21
21
|
|
|
22
22
|
let config = defaultConfig;
|
|
23
23
|
try {
|
|
24
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
13
|
-
|
|
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,
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
33
|
+
if(res.getHeader('Content-Encoding') !== 'gzip') return fail('should gzip when beneficial');
|
|
34
34
|
}
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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); }
|
package/tests/index.node-test.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {startNode,
|
|
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
|
-
|
|
16
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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,
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
+
if(!body3.includes('"data"')) return fail('buffer equal');
|
|
43
43
|
|
|
44
44
|
const res4 = createMockRes();
|
|
45
45
|
createResponseWrapper(res4).send(null);
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
+
if(!(cookies.length === 1 && cookies[0].includes('a=b'))) return fail('cookie added');
|
|
69
69
|
w.redirect('/next', 301);
|
|
70
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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); }
|
package/tests/test-utils.js
CHANGED
|
@@ -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) => {
|