@wppconnect/server-cli 1.3.51 → 1.3.53
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/CHANGELOG.md +8 -0
- package/dist/index.js +19 -1
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
- package/test/auth.test.js +247 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## [1.3.53](https://github.com/wppconnect-team/server-cli/compare/v1.3.52...v1.3.53) (2026-10-18)
|
|
2
|
+
|
|
3
|
+
## [1.3.52](https://github.com/wppconnect-team/server-cli/compare/v1.3.51...v1.3.52) (2026-10-17)
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
|
|
7
|
+
- require a non-default secretKey and add HTTP auth regression tests ([f3859c1](https://github.com/wppconnect-team/server-cli/commit/f3859c1a9a1b8a62990a6bcba93d864a1157a921))
|
|
8
|
+
|
|
1
9
|
## [1.3.51](https://github.com/wppconnect-team/server-cli/compare/v1.3.50...v1.3.51) (2026-10-13)
|
|
2
10
|
|
|
3
11
|
### Bug Fixes
|
package/dist/index.js
CHANGED
|
@@ -32,7 +32,9 @@ var _program = require("./program");function _interopRequireWildcard(e, t) {if (
|
|
|
32
32
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
33
33
|
* See the License for the specific language governing permissions and
|
|
34
34
|
* limitations under the License.
|
|
35
|
-
*/function run() {let serverOptions = {};_program.program.parse();const commandOptions = _program.program.opts();if (commandOptions.config) {try {const json = fs.readFileSync(commandOptions.config, { encoding: 'utf8' });const configOptions = JSON.parse(json);
|
|
35
|
+
*/const DEFAULT_SECRET_KEY = 'THISISMYSECURETOKEN';function run() {let serverOptions = {};_program.program.parse();const commandOptions = _program.program.opts();if (commandOptions.config) {try {const json = fs.readFileSync(commandOptions.config, { encoding: 'utf8' });const configOptions = JSON.parse(json);
|
|
36
|
+
serverOptions = (0, _mergeDeep.default)({}, serverOptions, configOptions);
|
|
37
|
+
} catch (error) {
|
|
36
38
|
console.log(error);
|
|
37
39
|
process.exit(1);
|
|
38
40
|
}
|
|
@@ -58,6 +60,22 @@ var _program = require("./program");function _interopRequireWildcard(e, t) {if (
|
|
|
58
60
|
|
|
59
61
|
serverOptions = (0, _mergeDeep.default)({}, serverOptions, commandOptions);
|
|
60
62
|
|
|
63
|
+
// @wppconnect/server signs tokens with `secretKey` and otherwise falls back to
|
|
64
|
+
// the public default, which would let anyone mint valid tokens. An explicit
|
|
65
|
+
// empty/null value would also override that fallback (mergeDeep keeps it), so
|
|
66
|
+
// blank values count as unset and the validated secret is passed explicitly.
|
|
67
|
+
const isUsable = (value) => typeof value === 'string' && value.trim() !== '';
|
|
68
|
+
const secretKey = isUsable(serverOptions.secretKey) ? serverOptions.secretKey : process.env.SECRET_KEY;
|
|
69
|
+
|
|
70
|
+
if (!isUsable(secretKey) || secretKey === DEFAULT_SECRET_KEY) {
|
|
71
|
+
console.error(
|
|
72
|
+
'Erro: nenhuma "secretKey" segura foi definida. Configure --secretKey, "secretKey" no arquivo de configuração ou a variável SECRET_KEY (o valor padrão público não é aceito).'
|
|
73
|
+
);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
serverOptions.secretKey = secretKey;
|
|
78
|
+
|
|
61
79
|
const { app } = (0, _server.initServer)(serverOptions);
|
|
62
80
|
|
|
63
81
|
if (commandOptions.frontend) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["_server","require","express","_interopRequireWildcard","fs","path","_mergeDeep","_interopRequireDefault","_program","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","run","serverOptions","program","parse","commandOptions","opts","config","json","readFileSync","encoding","configOptions","JSON","mergeDeep","error","console","log","process","exit","subOptions","key","opt","find","startsWith","name","substr","length","newName","toLowerCase","slice","app","initServer","frontend","frontendPath","frontendPackage","resolve","join","dirname","use","req","res","send","static","next","accepts","sendfile"
|
|
1
|
+
{"version":3,"file":"index.js","names":["_server","require","express","_interopRequireWildcard","fs","path","_mergeDeep","_interopRequireDefault","_program","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","DEFAULT_SECRET_KEY","run","serverOptions","program","parse","commandOptions","opts","config","json","readFileSync","encoding","configOptions","JSON","mergeDeep","error","console","log","process","exit","subOptions","key","opt","find","startsWith","name","substr","length","newName","toLowerCase","slice","isUsable","value","trim","secretKey","env","SECRET_KEY","app","initServer","frontend","frontendPath","frontendPackage","resolve","join","dirname","use","req","res","send","static","next","accepts","sendfile"],"sources":["../src/index.js"],"sourcesContent":["/*\n * Copyright 2021 WPPConnect Team\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { initServer } from '@wppconnect/server';\nimport * as express from 'express';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport mergeDeep from 'merge-deep';\nimport { program } from './program';\n\nconst DEFAULT_SECRET_KEY = 'THISISMYSECURETOKEN';\n\nexport function run() {\n let serverOptions = {};\n\n program.parse();\n const commandOptions = program.opts();\n\n if (commandOptions.config) {\n try {\n const json = fs.readFileSync(commandOptions.config, { encoding: 'utf8' });\n const configOptions = JSON.parse(json);\n\n serverOptions = mergeDeep({}, serverOptions, configOptions);\n } catch (error) {\n console.log(error);\n process.exit(1);\n }\n\n delete commandOptions.config;\n }\n\n const subOptions = ['webhook', 'archive', 'log', 'createOptions'];\n\n for (const key in commandOptions) {\n const opt = subOptions.find((opt) => key.startsWith(opt));\n\n if (opt) {\n commandOptions[opt] = commandOptions[opt] || {};\n\n const name = key.substr(opt.length);\n const newName = name[0].toLowerCase() + name.slice(1);\n\n commandOptions[opt][newName] = commandOptions[key];\n delete commandOptions[key];\n }\n }\n\n serverOptions = mergeDeep({}, serverOptions, commandOptions);\n\n // @wppconnect/server signs tokens with `secretKey` and otherwise falls back to\n // the public default, which would let anyone mint valid tokens. An explicit\n // empty/null value would also override that fallback (mergeDeep keeps it), so\n // blank values count as unset and the validated secret is passed explicitly.\n const isUsable = (value) => typeof value === 'string' && value.trim() !== '';\n const secretKey = isUsable(serverOptions.secretKey) ? serverOptions.secretKey : process.env.SECRET_KEY;\n\n if (!isUsable(secretKey) || secretKey === DEFAULT_SECRET_KEY) {\n console.error(\n 'Erro: nenhuma \"secretKey\" segura foi definida. Configure --secretKey, \"secretKey\" no arquivo de configuração ou a variável SECRET_KEY (o valor padrão público não é aceito).'\n );\n process.exit(1);\n }\n\n serverOptions.secretKey = secretKey;\n\n const { app } = initServer(serverOptions);\n\n if (commandOptions.frontend) {\n let frontendPath = commandOptions.frontendPath;\n\n if (!frontendPath) {\n try {\n const frontendPackage = require.resolve('@wppconnect/frontend/package.json');\n frontendPath = path.join(path.dirname(frontendPackage), 'build');\n } catch (error) {\n console.error(\n 'Não foi encontrado o caminho para o frontend, por favor defina com --frontend-path ou instale o pacote @wppconnect/frontend'\n );\n process.exit(1);\n }\n }\n\n if (frontendPath) {\n // Requisição de configuração do frontend\n app.use('/config.js', (req, res) => {\n res.set({\n 'Content-Type': 'application/javascript; charset=UTF-8',\n });\n res.send(`\n// Arquivo gerado automaticamente\nwindow.IP_SERVER = location.protocol + \"//\" + location.host + '/api/';\nwindow.IP_SOCKET_IO = ((location.protocol === 'https:') ? 'wss:' : 'ws:') + \"//\" + location.host;\n`);\n });\n\n app.use(express.static(frontendPath));\n\n app.get('*', function (req, res, next) {\n // Força a renderização do react para requisições do browser\n if (req.accepts('html')) {\n return res.sendfile(path.join(frontendPath, 'index.html'));\n }\n next();\n });\n }\n }\n}\n\nif (process.env['RUN_SERVER']) {\n run();\n}\n"],"mappings":"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","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wppconnect/server-cli",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.53",
|
|
4
4
|
"description": "Servidor de WPPConnect com frontend para ser executado a partir de uma linha de comando",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"prepare": "husky",
|
|
25
25
|
"release": "release-it",
|
|
26
26
|
"start": "cross-env RUN_SERVER=1 babel-node src",
|
|
27
|
-
"watch": "nodemon -w src --exec \"babel-node src\""
|
|
27
|
+
"watch": "nodemon -w src --exec \"babel-node src\"",
|
|
28
|
+
"test": "node --require @babel/register --test --test-force-exit \"test/**/*.test.js\""
|
|
28
29
|
},
|
|
29
30
|
"config": {
|
|
30
31
|
"commitizen": {
|
|
@@ -39,7 +40,7 @@
|
|
|
39
40
|
"prom-client": "^15.1.3"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
|
-
"@aws-sdk/client-s3": "^3.
|
|
43
|
+
"@aws-sdk/client-s3": "^3.1152.0",
|
|
43
44
|
"@babel/cli": "^7.29.7",
|
|
44
45
|
"@babel/core": "^7.29.7",
|
|
45
46
|
"@babel/eslint-parser": "^7.29.9",
|
|
@@ -92,7 +93,7 @@
|
|
|
92
93
|
"@wppconnect/server/**/form-data": "4.0.6",
|
|
93
94
|
"@wppconnect/server/**/multer": "2.4.0",
|
|
94
95
|
"@wppconnect/server/**/path-to-regexp": "0.2.5",
|
|
95
|
-
"@wppconnect/server/**/qs": "6.16.
|
|
96
|
+
"@wppconnect/server/**/qs": "6.16.1",
|
|
96
97
|
"@wppconnect/server/**/tmp": "0.2.7",
|
|
97
98
|
"@wppconnect/server/**/ws": "8.21.3",
|
|
98
99
|
"**/package-json/got": "11.8.6",
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { after, describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import net from 'node:net';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
|
|
9
|
+
// bcrypt is a dependency of @wppconnect/server; resolve it from there instead of
|
|
10
|
+
// declaring a dependency of our own. It is what verifyToken uses to check tokens.
|
|
11
|
+
const bcrypt = require(require.resolve('bcrypt', {
|
|
12
|
+
paths: [require.resolve('@wppconnect/server')],
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
// @wppconnect/server falls back to this public secret when none is configured,
|
|
16
|
+
// which lets anyone mint valid tokens through POST /api/:session/:secretkey/generate-token.
|
|
17
|
+
const DEFAULT_SECRET = 'THISISMYSECURETOKEN';
|
|
18
|
+
const SESSION = 'regression';
|
|
19
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
20
|
+
const BABEL_NODE = path.join(ROOT, 'node_modules', '.bin', 'babel-node');
|
|
21
|
+
|
|
22
|
+
const children = new Set();
|
|
23
|
+
const tmpDirs = new Set();
|
|
24
|
+
|
|
25
|
+
after(() => {
|
|
26
|
+
for (const child of children) child.kill('SIGKILL');
|
|
27
|
+
for (const dir of tmpDirs) fs.rmSync(dir, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function freePort() {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const srv = net.createServer();
|
|
33
|
+
srv.once('error', reject);
|
|
34
|
+
srv.listen(0, () => {
|
|
35
|
+
const { port } = srv.address();
|
|
36
|
+
srv.close(() => resolve(port));
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Runs the real CLI entrypoint, so the effective configuration (flags, config
|
|
42
|
+
// file, SECRET_KEY env) is what the HTTP assertions below are made against.
|
|
43
|
+
// `config`, when given, is written to a temp file and passed as --config.
|
|
44
|
+
async function startCli(args = [], env = {}, config) {
|
|
45
|
+
const port = await freePort();
|
|
46
|
+
if (config) {
|
|
47
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'wpp-cli-test-'));
|
|
48
|
+
tmpDirs.add(dir);
|
|
49
|
+
const file = path.join(dir, 'config.json');
|
|
50
|
+
fs.writeFileSync(file, JSON.stringify(config));
|
|
51
|
+
args = [...args, '--config', file];
|
|
52
|
+
}
|
|
53
|
+
const childEnv = { ...process.env, RUN_SERVER: '1', PORT: String(port), ...env };
|
|
54
|
+
for (const key of Object.keys(childEnv)) {
|
|
55
|
+
if (childEnv[key] === undefined) delete childEnv[key];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const child = spawn(BABEL_NODE, ['src', '--port', String(port), '--no-startAllSession', ...args], {
|
|
59
|
+
cwd: ROOT,
|
|
60
|
+
env: childEnv,
|
|
61
|
+
});
|
|
62
|
+
children.add(child);
|
|
63
|
+
|
|
64
|
+
let output = '';
|
|
65
|
+
child.stdout.on('data', (d) => (output += d));
|
|
66
|
+
child.stderr.on('data', (d) => (output += d));
|
|
67
|
+
|
|
68
|
+
const exited = new Promise((resolve) => child.once('exit', (code) => resolve(code)));
|
|
69
|
+
|
|
70
|
+
const outcome = await new Promise((resolve) => {
|
|
71
|
+
const timer = setTimeout(() => resolve({ listening: false }), 60000);
|
|
72
|
+
const poll = setInterval(() => {
|
|
73
|
+
if (output.includes('Server is running on port')) {
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
clearInterval(poll);
|
|
76
|
+
resolve({ listening: true });
|
|
77
|
+
}
|
|
78
|
+
}, 100);
|
|
79
|
+
exited.then((code) => {
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
clearInterval(poll);
|
|
82
|
+
resolve({ listening: false, code });
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
...outcome,
|
|
88
|
+
port,
|
|
89
|
+
output: () => output,
|
|
90
|
+
stop: () => child.kill('SIGKILL'),
|
|
91
|
+
url: (p) => `http://127.0.0.1:${port}${p}`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function generateToken(server, secret) {
|
|
96
|
+
const res = await fetch(server.url(`/api/${SESSION}/${secret}/generate-token`), { method: 'POST' });
|
|
97
|
+
return { status: res.status, body: await res.json() };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// verifyToken accepts bcrypt(session + secretKey), so a server that ends up with
|
|
101
|
+
// an empty/null secret accepts hashes anyone can compute without knowing a secret.
|
|
102
|
+
function forgedToken(secretSuffix) {
|
|
103
|
+
return bcrypt
|
|
104
|
+
.hashSync(SESSION + secretSuffix, 4)
|
|
105
|
+
.replace(/\//g, '_')
|
|
106
|
+
.replace(/\+/g, '-');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function connectionStatus(server, token) {
|
|
110
|
+
const res = await fetch(server.url(`/api/${SESSION}/check-connection-session`), {
|
|
111
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
112
|
+
});
|
|
113
|
+
return res.status;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Only `secret` may work: it mints a token that is accepted; the public default
|
|
117
|
+
// cannot mint one; tokens forged for an empty/null/undefined secret are rejected.
|
|
118
|
+
async function assertOnlySecretWorks(server, secret) {
|
|
119
|
+
for (const suffix of ['', 'null', 'undefined']) {
|
|
120
|
+
assert.equal(
|
|
121
|
+
await connectionStatus(server, forgedToken(suffix)),
|
|
122
|
+
401,
|
|
123
|
+
`token forged for secret "${suffix}" must be rejected`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
assert.equal((await generateToken(server, DEFAULT_SECRET)).status, 400);
|
|
128
|
+
|
|
129
|
+
const minted = await generateToken(server, secret);
|
|
130
|
+
assert.equal(minted.status, 201);
|
|
131
|
+
assert.equal(await connectionStatus(server, minted.body.token), 200);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
describe('API authentication (real CLI, real HTTP)', () => {
|
|
135
|
+
describe('with a configured secretKey', () => {
|
|
136
|
+
const SECRET = 'a-unique-test-secret-0123456789';
|
|
137
|
+
let server;
|
|
138
|
+
|
|
139
|
+
it('starts', async () => {
|
|
140
|
+
server = await startCli(['--secretKey', SECRET]);
|
|
141
|
+
assert.equal(server.listening, true, server.output());
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
for (const route of ['check-connection-session', 'status-session']) {
|
|
145
|
+
it(`rejects ${route} without credentials`, async () => {
|
|
146
|
+
const res = await fetch(server.url(`/api/${SESSION}/${route}`));
|
|
147
|
+
assert.ok([401, 403].includes(res.status), `got ${res.status}`);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it(`rejects ${route} with an invalid token`, async () => {
|
|
151
|
+
const res = await fetch(server.url(`/api/${SESSION}/${route}`), {
|
|
152
|
+
headers: { Authorization: 'Bearer not-a-valid-token' },
|
|
153
|
+
});
|
|
154
|
+
assert.ok([401, 403].includes(res.status), `got ${res.status}`);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
it('rejects a token minted from the well-known default secret', async () => {
|
|
159
|
+
const res = await fetch(server.url(`/api/${SESSION}/${DEFAULT_SECRET}/generate-token`), { method: 'POST' });
|
|
160
|
+
assert.equal(res.status, 400);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('accepts a token minted with the configured secret', async () => {
|
|
164
|
+
const { status, body } = await generateToken(server, SECRET);
|
|
165
|
+
assert.equal(status, 201);
|
|
166
|
+
|
|
167
|
+
const res = await fetch(server.url(`/api/${SESSION}/check-connection-session`), {
|
|
168
|
+
headers: { Authorization: `Bearer ${body.token}` },
|
|
169
|
+
});
|
|
170
|
+
assert.equal(res.status, 200);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('stops', () => server.stop());
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('without a secretKey (falls back to the public default)', () => {
|
|
177
|
+
it('refuses to start, so no token can be minted with the default', async () => {
|
|
178
|
+
const server = await startCli([], { SECRET_KEY: undefined });
|
|
179
|
+
try {
|
|
180
|
+
assert.equal(server.listening, false, 'server must not listen');
|
|
181
|
+
assert.notEqual(server.code, 0);
|
|
182
|
+
assert.match(server.output(), /secretKey/);
|
|
183
|
+
} finally {
|
|
184
|
+
server.stop();
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('refuses an explicit --secretKey equal to the public default', async () => {
|
|
189
|
+
const server = await startCli(['--secretKey', DEFAULT_SECRET], {
|
|
190
|
+
SECRET_KEY: undefined,
|
|
191
|
+
});
|
|
192
|
+
try {
|
|
193
|
+
assert.equal(server.listening, false, 'server must not listen');
|
|
194
|
+
assert.notEqual(server.code, 0);
|
|
195
|
+
} finally {
|
|
196
|
+
server.stop();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('secret supplied through the SECRET_KEY environment variable', () => {
|
|
202
|
+
it('starts and enforces that secret', async () => {
|
|
203
|
+
const SECRET = 'env-provided-secret-0123456789';
|
|
204
|
+
const server = await startCli([], { SECRET_KEY: SECRET });
|
|
205
|
+
try {
|
|
206
|
+
assert.equal(server.listening, true, server.output());
|
|
207
|
+
await assertOnlySecretWorks(server, SECRET);
|
|
208
|
+
} finally {
|
|
209
|
+
server.stop();
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// merge-deep lets an explicit empty/null value override the server default, so
|
|
215
|
+
// the secret the CLI validates must be the one handed to initServer.
|
|
216
|
+
describe('explicitly empty secretKey combined with SECRET_KEY', () => {
|
|
217
|
+
const SECRET = 'env-provided-secret-0123456789';
|
|
218
|
+
const cases = [
|
|
219
|
+
{ name: 'config file ""', args: [], config: { secretKey: '' } },
|
|
220
|
+
{ name: 'config file null', args: [], config: { secretKey: null } },
|
|
221
|
+
{ name: 'config file whitespace', args: [], config: { secretKey: ' ' } },
|
|
222
|
+
{ name: 'flag ""', args: ['--secretKey', ''], config: undefined },
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
for (const { name, args, config } of cases) {
|
|
226
|
+
it(`uses the environment secret (${name})`, async () => {
|
|
227
|
+
const server = await startCli(args, { SECRET_KEY: SECRET }, config);
|
|
228
|
+
try {
|
|
229
|
+
assert.equal(server.listening, true, server.output());
|
|
230
|
+
await assertOnlySecretWorks(server, SECRET);
|
|
231
|
+
} finally {
|
|
232
|
+
server.stop();
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it(`refuses to start without an environment secret (${name})`, async () => {
|
|
237
|
+
const server = await startCli(args, { SECRET_KEY: undefined }, config);
|
|
238
|
+
try {
|
|
239
|
+
assert.equal(server.listening, false, 'server must not listen');
|
|
240
|
+
assert.notEqual(server.code, 0);
|
|
241
|
+
} finally {
|
|
242
|
+
server.stop();
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
});
|