coverage tests spawn local server with authentication for read-write requests
All checks were successful
Sonarqube Scanner / Build and analyze (push) Successful in 1m57s
All checks were successful
Sonarqube Scanner / Build and analyze (push) Successful in 1m57s
This commit is contained in:
parent
d0046f5693
commit
8df484e77e
@ -97,6 +97,14 @@ test/ automatisierte Tests
|
|||||||
tools/python/ interaktive API-Werkzeuge ohne Node.js-Abhängigkeit
|
tools/python/ interaktive API-Werkzeuge ohne Node.js-Abhängigkeit
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Automatisierte Tests
|
||||||
|
|
||||||
|
Die REST-Integrationstests starten die Anwendung über `npm start` mit isolierten temporären Daten- und Speicherverzeichnissen. Der eigentliche Node.js-Prozess lauscht innerhalb des Testaufbaus auf `127.0.0.1:47146`. Davor läuft ein ausschließlich für die Tests bestimmter Reverse-Proxy auf `http://127.0.0.1:47145`, sodass dieser Loopback-Endpunkt das einzige Ziel der API-Aufrufe bleibt. Der Produktivserver wird von der Testsuite nicht angesprochen.
|
||||||
|
|
||||||
|
Der lokale Proxy lässt `GET`, `HEAD` und `OPTIONS` ohne Anmeldung passieren. Für `POST`, `PUT`, `PATCH` und `DELETE` antwortet er zunächst mit `401 Unauthorized` und `WWW-Authenticate: Basic`, sofern kein gültiger Authorization-Header vorhanden ist. Nach erfolgreicher Basic-Auth-Prüfung entfernt der Proxy den Header wieder und leitet den Request an den lokal per NPM gestarteten Server weiter. Damit wird ein möglicher Nginx-Passwortschutz für schreibende Operationen realistisch simuliert, ohne Authentifizierung in die Anwendung selbst einzubauen.
|
||||||
|
|
||||||
|
Der Integrationstest prüft dabei ausdrücklich, dass lesende Requests nicht präventiv authentifiziert werden und dass für `POST`, `PUT` und `DELETE` jeweils zuerst ein `401` und anschließend ein erfolgreicher authentifizierter Request erfolgt. Zusätzlich bleibt ein kleiner, isolierter HTTP-Mock auf einem zufälligen Loopback-Port bestehen, der die 401-Wiederholungslogik der Python-Request-Schicht unabhängig vom vollständigen REST-Test prüft.
|
||||||
|
|
||||||
## Technische Hinweise
|
## Technische Hinweise
|
||||||
|
|
||||||
- Medienpfade werden relativ zu `storage/` gespeichert. So bleibt das Projekt verschiebbar.
|
- Medienpfade werden relativ zu `storage/` gespeichert. So bleibt das Projekt verschiebbar.
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "wegwichtel-next",
|
"name": "wegwichtel-next",
|
||||||
"version": "0.12.1",
|
"version": "0.12.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "wegwichtel-next",
|
"name": "wegwichtel-next",
|
||||||
"version": "0.12.1",
|
"version": "0.12.3",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@file-type/av": "0.2.0",
|
"@file-type/av": "0.2.0",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wegwichtel-next",
|
"name": "wegwichtel-next",
|
||||||
"version": "0.12.1",
|
"version": "0.12.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "GPS-gestützte Lern- und Wanderwege mit GPX, POIs, Bildern und Audioansagen.",
|
"description": "GPS-gestützte Lern- und Wanderwege mit GPX, POIs, Bildern und Audioansagen.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
110
test-support/local-write-auth-proxy.js
Normal file
110
test-support/local-write-auth-proxy.js
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
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());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,12 +1,17 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import net from 'node:net';
|
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
|
import { startLocalWriteAuthProxy } from '../test-support/local-write-auth-proxy.js';
|
||||||
|
|
||||||
const root = path.resolve(import.meta.dirname, '..');
|
const root = path.resolve(import.meta.dirname, '..');
|
||||||
|
const LOCAL_API_URL = 'http://127.0.0.1:47145';
|
||||||
|
const BACKEND_API_URL = 'http://127.0.0.1:47146';
|
||||||
|
const TEST_API_USER = 'local-write-user';
|
||||||
|
const TEST_API_PASSWORD = 'local-write-password';
|
||||||
|
const WRITE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
||||||
const PNG_FILE = Buffer.from(
|
const PNG_FILE = Buffer.from(
|
||||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9ZQmcAAAAASUVORK5CYII=',
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9ZQmcAAAAASUVORK5CYII=',
|
||||||
'base64'
|
'base64'
|
||||||
@ -21,19 +26,42 @@ const WAV_FILE = Buffer.from(
|
|||||||
'hex'
|
'hex'
|
||||||
);
|
);
|
||||||
|
|
||||||
async function freePort() {
|
async function runProcess(command, args, options = {}) {
|
||||||
return new Promise((resolve, reject) => {
|
const child = spawn(command, args, {
|
||||||
const server = net.createServer();
|
...options,
|
||||||
server.once('error', reject);
|
stdio: ['ignore', 'pipe', 'pipe']
|
||||||
server.listen(0, '127.0.0.1', () => {
|
|
||||||
const { port } = server.address();
|
|
||||||
server.close(error => error ? reject(error) : resolve(port));
|
|
||||||
});
|
});
|
||||||
|
const stdout = [];
|
||||||
|
const stderr = [];
|
||||||
|
child.stdout.on('data', chunk => stdout.push(chunk.toString()));
|
||||||
|
child.stderr.on('data', chunk => stderr.push(chunk.toString()));
|
||||||
|
const status = await new Promise((resolve, reject) => {
|
||||||
|
child.once('error', reject);
|
||||||
|
child.once('exit', resolve);
|
||||||
});
|
});
|
||||||
|
return { status, stdout: stdout.join(''), stderr: stderr.join('') };
|
||||||
|
}
|
||||||
|
|
||||||
|
function authorizationHeader() {
|
||||||
|
const token = Buffer.from(`${TEST_API_USER}:${TEST_API_PASSWORD}`, 'utf8').toString('base64');
|
||||||
|
return `Basic ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchWithWriteAuthRetry(url, options = {}) {
|
||||||
|
const request = new Request(url, options);
|
||||||
|
const response = await fetch(request.clone());
|
||||||
|
if (!WRITE_METHODS.has(request.method) || response.status !== 401) return response;
|
||||||
|
|
||||||
|
assert.match(response.headers.get('www-authenticate') || '', /^Basic\s/i);
|
||||||
|
await response.arrayBuffer();
|
||||||
|
|
||||||
|
const headers = new Headers(request.headers);
|
||||||
|
headers.set('Authorization', authorizationHeader());
|
||||||
|
return fetch(new Request(request, { headers }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestJson(url, options = {}, expectedStatus = 200) {
|
async function requestJson(url, options = {}, expectedStatus = 200) {
|
||||||
const response = await fetch(url, options);
|
const response = await fetchWithWriteAuthRetry(url, options);
|
||||||
const body = await response.json();
|
const body = await response.json();
|
||||||
assert.equal(response.status, expectedStatus, JSON.stringify(body));
|
assert.equal(response.status, expectedStatus, JSON.stringify(body));
|
||||||
return { response, body };
|
return { response, body };
|
||||||
@ -60,43 +88,101 @@ async function waitForServer(baseUrl, child, output) {
|
|||||||
throw new Error(`Serverstart hat das Zeitlimit überschritten.\n${output.join('')}`);
|
throw new Error(`Serverstart hat das Zeitlimit überschritten.\n${output.join('')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
test('pictures and audio are managed as individual REST resources', { timeout: 30000 }, async t => {
|
test('the npm-started local API is exercised through a write-auth proxy on loopback port 47145', { timeout: 30000 }, async t => {
|
||||||
const runtime = await fs.mkdtemp(path.join(os.tmpdir(), 'wegwichtel-media-api-'));
|
const runtime = await fs.mkdtemp(path.join(os.tmpdir(), 'wegwichtel-media-api-'));
|
||||||
const port = await freePort();
|
const baseUrl = LOCAL_API_URL;
|
||||||
const baseUrl = `http://127.0.0.1:${port}`;
|
let authProxy;
|
||||||
const output = [];
|
const output = [];
|
||||||
const child = spawn(process.execPath, ['server.js'], {
|
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
||||||
|
const detached = process.platform !== 'win32';
|
||||||
|
const child = spawn(npmCommand, ['start'], {
|
||||||
cwd: root,
|
cwd: root,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
HOST: '127.0.0.1',
|
HOST: '127.0.0.1',
|
||||||
PORT: String(port),
|
PORT: '47146',
|
||||||
DATA_DIR: path.join(runtime, 'data'),
|
DATA_DIR: path.join(runtime, 'data'),
|
||||||
STORAGE_DIR: path.join(runtime, 'storage')
|
STORAGE_DIR: path.join(runtime, 'storage')
|
||||||
},
|
},
|
||||||
stdio: ['ignore', 'pipe', 'pipe']
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
detached
|
||||||
});
|
});
|
||||||
child.stdout.on('data', chunk => output.push(chunk.toString()));
|
child.stdout.on('data', chunk => output.push(chunk.toString()));
|
||||||
child.stderr.on('data', chunk => output.push(chunk.toString()));
|
child.stderr.on('data', chunk => output.push(chunk.toString()));
|
||||||
|
|
||||||
t.after(async () => {
|
t.after(async () => {
|
||||||
|
if (authProxy) await authProxy.close();
|
||||||
if (child.exitCode == null) {
|
if (child.exitCode == null) {
|
||||||
|
if (detached) {
|
||||||
|
try {
|
||||||
|
process.kill(-child.pid, 'SIGTERM');
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== 'ESRCH') throw error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
child.kill('SIGTERM');
|
child.kill('SIGTERM');
|
||||||
|
}
|
||||||
await new Promise(resolve => child.once('exit', resolve));
|
await new Promise(resolve => child.once('exit', resolve));
|
||||||
}
|
}
|
||||||
await fs.rm(runtime, { recursive: true, force: true });
|
await fs.rm(runtime, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitForServer(baseUrl, child, output);
|
await waitForServer(BACKEND_API_URL, child, output);
|
||||||
|
authProxy = await startLocalWriteAuthProxy({
|
||||||
|
listenPort: 47145,
|
||||||
|
targetPort: 47146,
|
||||||
|
username: TEST_API_USER,
|
||||||
|
password: TEST_API_PASSWORD
|
||||||
|
});
|
||||||
|
|
||||||
const gpx = await fs.readFile(path.join(root, 'examples', 'sample-route.gpx'));
|
const healthResponse = await fetch(`${baseUrl}/api/health`);
|
||||||
const routeForm = new FormData();
|
assert.equal(healthResponse.status, 200);
|
||||||
routeForm.append('name', 'Medien-API-Test');
|
assert.equal(healthResponse.headers.get('www-authenticate'), null);
|
||||||
routeForm.append('gpx', new Blob([gpx], { type: 'application/octet-stream' }), 'route.txt');
|
|
||||||
const { body: route } = await requestJson(`${baseUrl}/api/routes`, {
|
const pythonHealthCheck = await runProcess('python3', ['-c', `
|
||||||
method: 'POST',
|
from common import ApiClient
|
||||||
body: routeForm
|
result = ApiClient('${LOCAL_API_URL}').request('GET', '/api/health')
|
||||||
}, 201);
|
assert result['ok'] is True
|
||||||
|
`], {
|
||||||
|
cwd: root,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
API_USER: '',
|
||||||
|
API_PASSWORD: '',
|
||||||
|
PYTHONPATH: path.join(root, 'tools/python')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
pythonHealthCheck.status,
|
||||||
|
0,
|
||||||
|
[pythonHealthCheck.stdout, pythonHealthCheck.stderr].filter(Boolean).join('\n')
|
||||||
|
);
|
||||||
|
|
||||||
|
const gpxPath = path.join(root, 'examples', 'sample-route.gpx');
|
||||||
|
const gpx = await fs.readFile(gpxPath);
|
||||||
|
const createRoute = await runProcess('python3', [
|
||||||
|
path.join(root, 'tools/python/create_route.py'),
|
||||||
|
'--base-url', LOCAL_API_URL,
|
||||||
|
'--name', 'Medien-API-Test',
|
||||||
|
'--slug', '',
|
||||||
|
'--description', '',
|
||||||
|
'--school-name', '',
|
||||||
|
'--gpx', gpxPath
|
||||||
|
], {
|
||||||
|
cwd: root,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
API_USER: ` ${TEST_API_USER} `,
|
||||||
|
API_PASSWORD: ` ${TEST_API_PASSWORD} `,
|
||||||
|
PYTHONPATH: path.join(root, 'tools/python')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
createRoute.status,
|
||||||
|
0,
|
||||||
|
[createRoute.stdout, createRoute.stderr].filter(Boolean).join('\n')
|
||||||
|
);
|
||||||
|
const route = JSON.parse(createRoute.stdout);
|
||||||
assert.equal(route.gpxUrl, `/api/routes/${route.id}/gpx`);
|
assert.equal(route.gpxUrl, `/api/routes/${route.id}/gpx`);
|
||||||
const { response: gpxResponse, body: downloadedGpx } = await requestBuffer(`${baseUrl}${route.gpxUrl}`);
|
const { response: gpxResponse, body: downloadedGpx } = await requestBuffer(`${baseUrl}${route.gpxUrl}`);
|
||||||
assert.match(gpxResponse.headers.get('content-type') || '', /application\/gpx\+xml/);
|
assert.match(gpxResponse.headers.get('content-type') || '', /application\/gpx\+xml/);
|
||||||
@ -324,4 +410,11 @@ test('pictures and audio are managed as individual REST resources', { timeout: 3
|
|||||||
|
|
||||||
await requestJson(`${baseUrl}/api/routes/${route.id}/pois/${poi.id}/pictures/${picture.id}`, {}, 404);
|
await requestJson(`${baseUrl}/api/routes/${route.id}/pois/${poi.id}/pictures/${picture.id}`, {}, 404);
|
||||||
await requestJson(`${baseUrl}/api/routes/${route.id}/pois/${poi.id}/audio`, {}, 404);
|
await requestJson(`${baseUrl}/api/routes/${route.id}/pois/${poi.id}/audio`, {}, 404);
|
||||||
|
|
||||||
|
for (const method of ['POST', 'PUT', 'DELETE']) {
|
||||||
|
assert.ok(authProxy.stats.unauthorizedWrites[method] > 0, `${method} muss zunächst HTTP 401 auslösen.`);
|
||||||
|
assert.ok(authProxy.stats.authorizedWrites[method] > 0, `${method} muss mit Basic Auth weitergeleitet werden.`);
|
||||||
|
}
|
||||||
|
assert.ok(authProxy.stats.forwardedReads > 0);
|
||||||
|
assert.equal(authProxy.stats.authenticatedReads, 0, 'Lesende Requests dürfen nicht präventiv authentifiziert werden.');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -27,7 +27,7 @@ test('default server socket uses the local Wegwichtel endpoint', async () => {
|
|||||||
|
|
||||||
test('retired internal instance label is absent from authored project files', async () => {
|
test('retired internal instance label is absent from authored project files', async () => {
|
||||||
const extensions = new Set(['.js', '.json', '.md', '.html', '.css', '.env', '.sql', '.gpx', '.svg']);
|
const extensions = new Set(['.js', '.json', '.md', '.html', '.css', '.env', '.sql', '.gpx', '.svg']);
|
||||||
const ignoredDirectories = new Set(['node_modules', 'vendor', '.git']);
|
const ignoredDirectories = new Set(['node_modules', 'vendor', 'coverage', 'data', 'storage', '.git']);
|
||||||
const files = [];
|
const files = [];
|
||||||
|
|
||||||
async function collect(directory) {
|
async function collect(directory) {
|
||||||
|
|||||||
@ -55,9 +55,9 @@ test('Python API tools are syntax-valid and contain reusable 401 authentication
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
test('Python client retries the same request with API_USER and API_PASSWORD after HTTP 401', async t => {
|
test('Python client retries an intentionally simulated reverse-proxy 401 with trimmed environment credentials', async t => {
|
||||||
const apiUser = process.env.API_USER || 'tester';
|
const apiUser = 'local-test-user';
|
||||||
const apiPassword = process.env.API_PASSWORD || 'secret';
|
const apiPassword = 'local-test-password';
|
||||||
const expectedAuthorization = `Basic ${Buffer.from(`${apiUser}:${apiPassword}`).toString('base64')}`;
|
const expectedAuthorization = `Basic ${Buffer.from(`${apiUser}:${apiPassword}`).toString('base64')}`;
|
||||||
const receivedAuthorization = [];
|
const receivedAuthorization = [];
|
||||||
|
|
||||||
@ -83,8 +83,8 @@ assert result == {'ok': True}
|
|||||||
const child = spawn('python3', ['-c', script], {
|
const child = spawn('python3', ['-c', script], {
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
API_USER: apiUser,
|
API_USER: ` ${apiUser} `,
|
||||||
API_PASSWORD: apiPassword,
|
API_PASSWORD: ` ${apiPassword} `,
|
||||||
PYTHONPATH: path.join(root, 'tools/python')
|
PYTHONPATH: path.join(root, 'tools/python')
|
||||||
},
|
},
|
||||||
stdio: ['ignore', 'pipe', 'pipe']
|
stdio: ['ignore', 'pipe', 'pipe']
|
||||||
|
|||||||
@ -4,7 +4,7 @@ Die Skripte benötigen nur Python 3 und die Standardbibliothek. Node.js, `curl`
|
|||||||
|
|
||||||
## Authentifizierung
|
## Authentifizierung
|
||||||
|
|
||||||
Jeder Request wird zunächst ohne vorauseilenden Authorization-Header ausgeführt. Antwortet ein vorgeschalteter Webserver mit `401 Unauthorized`, verwendet die gemeinsame Request-Schicht zuerst die Umgebungsvariablen `API_USER` und `API_PASSWORD`. Sind beide gesetzt, wird derselbe Request genau mit diesen Basic-Auth-Credentials wiederholt. Fehlen sie, fragt das Werkzeug Benutzername und Passwort interaktiv ab. Das Passwort wird mit `getpass` ohne Bildschirmausgabe eingelesen.
|
Jeder Request wird zunächst ohne vorauseilenden Authorization-Header ausgeführt. Antwortet ein vorgeschalteter Webserver mit `401 Unauthorized`, verwendet die gemeinsame Request-Schicht zuerst die Umgebungsvariablen `API_USER` und `API_PASSWORD`. Sind beide gesetzt, wird derselbe Request genau mit diesen Basic-Auth-Credentials wiederholt. Fehlen sie, fragt das Werkzeug Benutzername und Passwort interaktiv ab. Das Passwort wird mit `getpass` ohne Bildschirmausgabe eingelesen. Führende und nachgestellte Whitespaces werden aus beiden Umgebungsvariablen entfernt; reine Whitespace-Werte gelten als nicht gesetzt.
|
||||||
|
|
||||||
Für unbeaufsichtigte Aufrufe und CI-Pipelines:
|
Für unbeaufsichtigte Aufrufe und CI-Pipelines:
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user