Wegwichtel/test-support/local-write-auth-proxy.js
Florian Zumpe 8df484e77e
All checks were successful
Sonarqube Scanner / Build and analyze (push) Successful in 1m57s
coverage tests spawn local server with authentication for read-write requests
2026-06-17 13:38:05 +02:00

111 lines
3.0 KiB
JavaScript

import http from 'node:http';
const PROTECTED_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
function increment(counter, method) {
counter[method] = (counter[method] || 0) + 1;
}
function basicAuthorization(username, password) {
const token = Buffer.from(`${username}:${password}`, 'utf8').toString('base64');
return `Basic ${token}`;
}
function unauthorized(response) {
response.writeHead(401, {
'Content-Type': 'application/json; charset=utf-8',
'WWW-Authenticate': 'Basic realm="Wegwichtel local test"'
});
response.end(JSON.stringify({
error: 'Unauthorized',
message: 'Für schreibende Operationen sind Zugangsdaten erforderlich.'
}));
}
function badGateway(response, error) {
if (response.headersSent) {
response.destroy(error);
return;
}
response.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
response.end(JSON.stringify({
error: 'BadGateway',
message: `Lokaler Testserver nicht erreichbar: ${error.message}`
}));
}
export async function startLocalWriteAuthProxy({
listenHost = '127.0.0.1',
listenPort = 47145,
targetHost = '127.0.0.1',
targetPort,
username,
password
}) {
if (!targetPort) throw new TypeError('targetPort ist erforderlich.');
if (!username || !password) throw new TypeError('Test-Credentials sind erforderlich.');
const expectedAuthorization = basicAuthorization(username, password);
const stats = {
unauthorizedWrites: {},
authorizedWrites: {},
forwardedReads: 0,
authenticatedReads: 0
};
const server = http.createServer((request, response) => {
const method = (request.method || 'GET').toUpperCase();
const isProtected = PROTECTED_METHODS.has(method);
const suppliedAuthorization = request.headers.authorization;
if (isProtected && suppliedAuthorization !== expectedAuthorization) {
increment(stats.unauthorizedWrites, method);
request.resume();
unauthorized(response);
return;
}
if (isProtected) {
increment(stats.authorizedWrites, method);
} else {
stats.forwardedReads += 1;
if (suppliedAuthorization) stats.authenticatedReads += 1;
}
const headers = { ...request.headers, host: `${targetHost}:${targetPort}` };
delete headers.authorization;
const upstream = http.request({
host: targetHost,
port: targetPort,
method,
path: request.url,
headers
}, upstreamResponse => {
response.writeHead(upstreamResponse.statusCode || 502, upstreamResponse.headers);
upstreamResponse.pipe(response);
});
upstream.on('error', error => badGateway(response, error));
request.pipe(upstream);
});
await new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(listenPort, listenHost, () => {
server.off('error', reject);
resolve();
});
});
return {
stats,
async close() {
if (!server.listening) return;
await new Promise((resolve, reject) => {
server.close(error => error ? reject(error) : resolve());
});
}
};
}