Nettoyage et construction micro services
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
||||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
COPY server ./server
|
||||
COPY server/server.mjs server/services.mjs server/telemetry.mjs ./server/
|
||||
USER node
|
||||
ENV PORT=8080
|
||||
EXPOSE 8080
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createServer, serviceRoutes } from './server.mjs';
|
||||
|
||||
test('independent services expose only their own API and preserve response contracts', async () => {
|
||||
const servers = {};
|
||||
const addresses = {};
|
||||
try {
|
||||
for (const role of Object.keys(serviceRoutes)) {
|
||||
const server = createServer({ role, env: {}, fetcher: async () => ({ok:true,json:async()=>({
|
||||
current: {temperature_2m:18, wind_speed_10m:8, weather_code:2, time:'2026-09-13T10:00'},
|
||||
daily: {temperature_2m_min:[12],temperature_2m_max:[22]}
|
||||
})}) });
|
||||
await new Promise(resolve => server.listen(0,'127.0.0.1',resolve));
|
||||
servers[role] = server; addresses[role] = `http://127.0.0.1:${server.address().port}`;
|
||||
assert.equal((await fetch(addresses[role]+'/api/health')).status,200);
|
||||
assert.equal((await fetch(addresses[role]+'/')).status,404);
|
||||
for (const [other, paths] of Object.entries(serviceRoutes)) {
|
||||
if (other !== role) for (const path of paths) assert.equal((await fetch(addresses[role]+path)).status,404);
|
||||
}
|
||||
}
|
||||
const data = await (await fetch(addresses.telemetry+'/api/telemetry?profile=arret')).json();
|
||||
assert.equal(data.speedKph,0); assert.equal(data.source,'demo');
|
||||
assert.equal((await (await fetch(addresses.weather+'/api/weather?lat=48&lon=2')).json()).temperature,18);
|
||||
assert.match((await (await fetch(addresses.navigation+'/api/map-config')).json()).tileUrl,/openstreetmap/);
|
||||
assert.equal((await fetch(addresses.navigation+'/api/search?q=a')).status,400);
|
||||
assert.equal((await fetch(addresses.weather+'/api/weather',{method:'POST'})).status,405);
|
||||
await new Promise(resolve=>servers.weather.close(resolve)); delete servers.weather;
|
||||
assert.equal((await fetch(addresses.telemetry+'/api/telemetry')).status,200);
|
||||
assert.equal((await fetch(addresses.navigation+'/api/map-config')).status,200);
|
||||
} finally {
|
||||
await Promise.all(Object.values(servers).map(server=>new Promise(resolve=>server.close(resolve))));
|
||||
}
|
||||
});
|
||||
|
||||
test('invalid service roles cannot silently expose the combined development API', () => {
|
||||
assert.throws(()=>createServer({role:'unknown'}),/Unknown service/);
|
||||
});
|
||||
+16
-3
@@ -8,14 +8,21 @@ import { createServices } from './services.mjs';
|
||||
const assets = new Map([
|
||||
['/', ['index.html', 'text/html']], ['/index.html', ['index.html', 'text/html']],
|
||||
['/app.js', ['app.js', 'text/javascript']], ['/styles.css', ['styles.css', 'text/css']],
|
||||
...['journey.js', 'geo.js', 'theme.js', 'applications.js', 'cockpit.js', 'embeds.js'].map(name => [`/${name}`, [name, 'text/javascript']]),
|
||||
...['journey.js', 'geo.js', 'theme.js', 'applications.js', 'cockpit.js', 'embeds.js', 'instruments.js', 'music.js'].map(name => [`/${name}`, [name, 'text/javascript']]),
|
||||
['/cockpit.css', ['cockpit.css', 'text/css']],
|
||||
['/applications.css', ['applications.css', 'text/css']],
|
||||
['/journey.css', ['journey.css', 'text/css']],
|
||||
['/vendor/leaflet/leaflet.js', ['vendor/leaflet/leaflet.js', 'text/javascript']],
|
||||
['/vendor/leaflet/leaflet.css', ['vendor/leaflet/leaflet.css', 'text/css']]
|
||||
]);
|
||||
export function createServer(options) {
|
||||
export const serviceRoutes = {
|
||||
telemetry: ['/api/telemetry'],
|
||||
weather: ['/api/weather'],
|
||||
navigation: ['/api/map-config', '/api/search', '/api/route']
|
||||
};
|
||||
export function createServer(options = {}) {
|
||||
const role = options.role || 'all';
|
||||
if (role !== 'all' && !Object.hasOwn(serviceRoutes, role)) throw new Error(`Unknown service: ${role}`);
|
||||
const services = createServices(options);
|
||||
return http.createServer(async (req, res) => {
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
@@ -27,6 +34,7 @@ export function createServer(options) {
|
||||
if (req.method !== 'GET' && req.method !== 'HEAD') return send(405, { error: 'Lecture seule' });
|
||||
const url = new URL(req.url, 'http://localhost');
|
||||
if (url.pathname === '/api/health') return send(200, { status: 'ok', source: 'demo' });
|
||||
if (role !== 'all' && !serviceRoutes[role].includes(url.pathname)) return send(404, { error: 'Introuvable' });
|
||||
if (url.pathname === '/api/telemetry') {
|
||||
const profile = url.searchParams.get('profile') || 'route';
|
||||
return profiles.includes(profile) ? send(200, telemetry(profile)) : send(400, { error: 'Profil inconnu' });
|
||||
@@ -46,5 +54,10 @@ export function createServer(options) {
|
||||
}
|
||||
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
|
||||
const port = Number(process.env.PORT || 8080);
|
||||
createServer().listen(port, process.env.HOST || '0.0.0.0', () => console.log(`CARIA : http://localhost:${port}`));
|
||||
const server = createServer({ role: process.env.SERVICE_ROLE || 'all' });
|
||||
server.listen(port, process.env.HOST || '0.0.0.0', () => console.log(`CARIA ${process.env.SERVICE_ROLE || 'development'} : http://localhost:${port}`));
|
||||
for (const signal of ['SIGTERM', 'SIGINT']) process.once(signal, () => {
|
||||
server.close(() => process.exit(0));
|
||||
setTimeout(() => process.exit(1), 10000).unref();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user