@mostajs/smoke 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.md +22 -0
- package/bin/cli.mjs +23 -0
- package/llms.txt +6 -0
- package/package.json +37 -0
- package/src/index.js +63 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# Changelog — @mostajs/smoke
|
|
2
|
+
## [0.1.0] — 2026-06-22
|
|
3
|
+
### Ajouté
|
|
4
|
+
- Sonde HTTP via node:http (statut réel, sans suivre les redirections) ; `smoke`/`checksFrom`/`waitForHealth` ; expect number|'2xx'/'3xx'|[codes] ; CLI `--wait`. 5 tests. Extrait du smoke-curl de `deploy.sh`.
|
package/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# @mostajs/smoke
|
|
2
|
+
|
|
3
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com>
|
|
4
|
+
|
|
5
|
+
Sonde HTTP **zéro-dépendance** des routes d'une app (publiques→200, protégées→303/401), **sans suivre les redirections** (node:http — `fetch` en `redirect:'manual'` renverrait un statut 0 « opaqueredirect » et masquerait le 303). Pré-déploiement & test. Membre de `mosta-deploy-stack`.
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
// smoke.config.mjs
|
|
9
|
+
export default { health: '/api/health', public: ['/', '/login'], gated: ['/dashboard', '/m/companies'] };
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
mosta-smoke --config smoke.config.mjs --base http://127.0.0.1:7931 --wait
|
|
14
|
+
```
|
|
15
|
+
`--wait` attend que `health` réponde 2xx avant de sonder ; sortie ≠ 0 si une route n'est pas conforme.
|
|
16
|
+
|
|
17
|
+
## API
|
|
18
|
+
- `smoke({ base, checks?, public?, gated?, publicExpect?, gatedExpect? })` → `{ ok, pass, fail, results }`
|
|
19
|
+
- `checksFrom({ public, gated })` · `waitForHealth(base, path, { timeoutMs })`
|
|
20
|
+
- `expect` accepte un nombre (`200`), une famille (`'2xx'`/`'3xx'`) ou un tableau (`[303,401]`).
|
|
21
|
+
|
|
22
|
+
Tests : `npm test`. Licence : AGPL-3.0-or-later.
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @mostajs/smoke — CLI : sonde les routes d'une app. Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
3
|
+
// Usage : mosta-smoke [--config smoke.config.mjs] [--base URL] [--wait]
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import { smoke, waitForHealth } from '../src/index.js';
|
|
7
|
+
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const val = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : d; };
|
|
10
|
+
const cfgPath = resolve(process.cwd(), val('--config', 'smoke.config.mjs'));
|
|
11
|
+
const mod = await import(pathToFileURL(cfgPath).href).catch((e) => { console.error(`✗ config introuvable : ${cfgPath}\n ${e.message}`); process.exit(2); });
|
|
12
|
+
const config = { ...(mod.default || mod.config || mod) };
|
|
13
|
+
if (val('--base')) config.base = val('--base');
|
|
14
|
+
|
|
15
|
+
if (args.includes('--wait')) {
|
|
16
|
+
const ready = await waitForHealth(config.base, config.health || '/api/health');
|
|
17
|
+
if (!ready) { console.error(`✗ serveur non prêt (${config.base}${config.health || '/api/health'})`); process.exit(1); }
|
|
18
|
+
}
|
|
19
|
+
const r = await smoke(config);
|
|
20
|
+
for (const x of r.results) if (!x.ok) console.error(` ✗ ${x.path} → ${x.error || x.code} (attendu ${JSON.stringify(x.expect)})`);
|
|
21
|
+
if (r.ok) console.log(`✓ smoke OK — ${r.pass}/${r.results.length} routes conformes`);
|
|
22
|
+
else console.error(`✗ smoke KO — ${r.fail} route(s) non conforme(s)`);
|
|
23
|
+
process.exit(r.ok ? 0 : 1);
|
package/llms.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# @mostajs/smoke
|
|
2
|
+
Sonde HTTP zéro-dép des routes (publiques→200, protégées→303/401), SANS suivre les redirections (node:http, pas fetch). Membre de mosta-deploy-stack.
|
|
3
|
+
## API
|
|
4
|
+
smoke({base, checks?:[{path,expect}], public?, gated?, publicExpect?=200, gatedExpect?=[303,401]}) → {ok,pass,fail,results}
|
|
5
|
+
checksFrom({public,gated}) · waitForHealth(base,path,{timeoutMs}) · expect = number | '2xx'/'3xx' | [codes]
|
|
6
|
+
CLI: mosta-smoke --config smoke.config.mjs --base URL [--wait]
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mostajs/smoke",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Sonde HTTP zéro-dépendance des routes d'une app (pré-déploiement / test) : publiques → 200, protégées → 303/401, sans suivre les redirections. Piloté par un manifeste. Membre de mosta-deploy-stack.",
|
|
5
|
+
"license": "AGPL-3.0-or-later",
|
|
6
|
+
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "src/index.js",
|
|
9
|
+
"bin": {
|
|
10
|
+
"mosta-smoke": "bin/cli.mjs"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src",
|
|
14
|
+
"bin",
|
|
15
|
+
"llms.txt",
|
|
16
|
+
"README.md",
|
|
17
|
+
"CHANGELOG.md"
|
|
18
|
+
],
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./src/index.js"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"mostajs",
|
|
24
|
+
"smoke",
|
|
25
|
+
"http",
|
|
26
|
+
"healthcheck",
|
|
27
|
+
"deploy",
|
|
28
|
+
"test",
|
|
29
|
+
"routes"
|
|
30
|
+
],
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@mostajs/mjs-unit": "^0.3.0"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"test": "bash test-scripts/run-tests.sh"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// @mostajs/smoke — sonde HTTP zéro-dépendance des routes d'une app (pré-déploiement / test). Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
2
|
+
// Vérifie qu'un ensemble de routes répond avec le code attendu : PUBLIQUES → 200, PROTÉGÉES → 303/401 (gating). Sans suivre les redirections.
|
|
3
|
+
// Remplace les boucles `curl` bespoke des deploy.sh. Membre de mosta-deploy-stack.
|
|
4
|
+
// NB : on utilise node:http/https (pas fetch) — fetch en mode redirect:'manual' renvoie un statut 0 « opaqueredirect » et masque le 303.
|
|
5
|
+
import http from 'node:http';
|
|
6
|
+
import https from 'node:https';
|
|
7
|
+
|
|
8
|
+
/** GET brut qui NE SUIT PAS les redirections → renvoie le vrai statusCode (200/303/401/…). */
|
|
9
|
+
function get(url, timeoutMs = 8000) {
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
let u; try { u = new URL(url); } catch (e) { return resolve({ status: 0, error: e.message }); }
|
|
12
|
+
const lib = u.protocol === 'https:' ? https : http;
|
|
13
|
+
const req = lib.request(u, { method: 'GET' }, (res) => { res.resume(); resolve({ status: res.statusCode }); });
|
|
14
|
+
req.on('error', (e) => resolve({ status: 0, error: e.message }));
|
|
15
|
+
req.setTimeout(timeoutMs, () => { req.destroy(); resolve({ status: 0, error: 'timeout' }); });
|
|
16
|
+
req.end();
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Le code `c` satisfait-il l'attente `e` (number | '2xx'/'3xx' | tableau de valeurs acceptées) ? */
|
|
21
|
+
function matches(c, e) {
|
|
22
|
+
if (Array.isArray(e)) return e.some((x) => matches(c, x));
|
|
23
|
+
if (typeof e === 'string' && /^\dxx$/.test(e)) return Math.floor(c / 100) === Number(e[0]);
|
|
24
|
+
return c === Number(e);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Construit la liste de checks depuis { public, gated } (raccourci courant). */
|
|
28
|
+
export function checksFrom({ public: pub = [], gated = [], publicExpect = 200, gatedExpect = [303, 401] } = {}) {
|
|
29
|
+
return [
|
|
30
|
+
...pub.map((path) => ({ path, expect: publicExpect })),
|
|
31
|
+
...gated.map((path) => ({ path, expect: gatedExpect })),
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Attend que `base+health` réponde 2xx (démarrage serveur). Renvoie true/false. */
|
|
36
|
+
export async function waitForHealth(base, health = '/api/health', { timeoutMs = 10000, intervalMs = 250 } = {}) {
|
|
37
|
+
const until = Date.now() + timeoutMs;
|
|
38
|
+
while (Date.now() < until) {
|
|
39
|
+
const r = await get(base + health, intervalMs + 500);
|
|
40
|
+
if (Math.floor(r.status / 100) === 2) return true;
|
|
41
|
+
await new Promise((res) => setTimeout(res, intervalMs));
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Sonde les routes. config = { base, checks?:[{path,expect}], public?:[], gated?:[], publicExpect?, gatedExpect? }.
|
|
48
|
+
* Ne SUIT PAS les redirections (redirect:'manual') pour distinguer 200 vs 303. Renvoie { ok, pass, fail, results }.
|
|
49
|
+
*/
|
|
50
|
+
export async function smoke(config = {}) {
|
|
51
|
+
const { base } = config;
|
|
52
|
+
if (!base) throw new Error('@mostajs/smoke: config.base requis');
|
|
53
|
+
const checks = config.checks || checksFrom(config);
|
|
54
|
+
const results = [];
|
|
55
|
+
for (const { path, expect } of checks) {
|
|
56
|
+
const r = await get(base + path);
|
|
57
|
+
results.push({ path, code: r.status, expect, ok: r.error ? false : matches(r.status, expect), error: r.error || null });
|
|
58
|
+
}
|
|
59
|
+
const pass = results.filter((r) => r.ok).length;
|
|
60
|
+
return { ok: pass === results.length, pass, fail: results.length - pass, results };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default { smoke, checksFrom, waitForHealth };
|