directory cleanup for tests
This commit is contained in:
@@ -96,13 +96,27 @@ database/schema.sql SQLite-Schema
|
||||
data/ lokale SQLite-Datei
|
||||
storage/active/routes/ aktive GPX-, Bild- und Audiodateien
|
||||
storage/trash/routes/ zum Löschen markierte Strecken
|
||||
examples/ Beispiel-GPX
|
||||
test/ automatisierte Tests
|
||||
test-support/ isolierte Testhilfen und synthetische GPX-Testdaten
|
||||
tools/python/ interaktive API-Werkzeuge ohne Node.js-Abhängigkeit
|
||||
```
|
||||
|
||||
## Automatisierte Tests
|
||||
|
||||
GPX-Testdaten liegen in `test-support/gpx-fixtures.js`. Die API-Tests schreiben sie
|
||||
bei Bedarf in ein temporäres Verzeichnis. Mitgelieferte Beispielstrecken oder
|
||||
Dateien unter `examples/` werden nicht vorausgesetzt.
|
||||
|
||||
Das optionale Präsentations-Uploadskript verwendet ausdrücklich angegebene Dateien:
|
||||
|
||||
```bash
|
||||
bash scripts/upload-presentation-route.sh https://wegwichtel.de/api /pfad/strecke.gpx /pfad/stationen.json
|
||||
```
|
||||
|
||||
Die JSON-Datei enthält `name`, `description` und `pois` mit `title`, `description`,
|
||||
`lat`, `lon`, `triggerRadiusM` und `sequence`. Die Skript-Tests erzeugen ihre Daten
|
||||
selbst und simulieren die HTTP-Aufrufe, einschließlich des Aufräumens bei Fehlern.
|
||||
|
||||
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.
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${1:-https://wegwichtel.de/api}"
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
|
||||
GPX_FILE="$ROOT_DIR/examples/presentation-route.gpx"
|
||||
JSON_FILE="$ROOT_DIR/examples/presentation-route.json"
|
||||
GPX_FILE="${2:-}"
|
||||
JSON_FILE="${3:-}"
|
||||
|
||||
if [[ ! -f "$GPX_FILE" || ! -f "$JSON_FILE" ]]; then
|
||||
echo "Aufruf: $0 <API-Adresse> <GPX-Datei> <JSON-Datei>" >&2
|
||||
echo "Gib vorhandene Dateien an. Die Beispielstrecken werden nicht mehr mitgeliefert." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
for command in curl python3; do
|
||||
command -v "$command" >/dev/null 2>&1 || {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// Synthetic GPX data owned by the tests, independent of repository examples.
|
||||
export const SAMPLE_GPX = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gpx version="1.1" creator="Wegwichtel Test" xmlns="http://www.topografix.com/GPX/1/1">
|
||||
<metadata><name>Testweg</name></metadata>
|
||||
<trk><name>Testweg</name><trkseg>
|
||||
<trkpt lat="52.5200" lon="13.4050"><ele>34</ele></trkpt>
|
||||
<trkpt lat="52.5208" lon="13.4070"><ele>37</ele></trkpt>
|
||||
<trkpt lat="52.5216" lon="13.4090"><ele>35</ele></trkpt>
|
||||
</trkseg></trk>
|
||||
</gpx>`;
|
||||
+8
-28
@@ -1,33 +1,13 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import { parseGpx } from '../src/services/gpx.js';
|
||||
import { SAMPLE_GPX } from '../test-support/gpx-fixtures.js';
|
||||
|
||||
test('parseGpx liest Beispielstrecke', () => {
|
||||
const parsed = parseGpx(fs.readFileSync(new URL('../examples/sample-route.gpx', import.meta.url), 'utf8'));
|
||||
assert.equal(parsed.name, 'Beispielweg'); assert.equal(parsed.points.length, 3); assert.ok(parsed.metrics.distanceM > 0);
|
||||
});
|
||||
|
||||
test('normalization separates waypoints and collapses stationary GPS drift', () => {
|
||||
const xml = '<gpx><metadata><name>Pause</name></metadata><wpt lat="51" lon="11"><name>Station</name></wpt><trk><trkseg>'
|
||||
+ '<trkpt lat="50" lon="10"><time>2026-10-03T10:00:00Z</time></trkpt>'
|
||||
+ '<trkpt lat="50.00001" lon="10.00001"/><trkpt lat="50.00002" lon="10.00002"/>'
|
||||
+ '<trkpt lat="50.001" lon="10.001"/></trkseg></trk></gpx>';
|
||||
const result = parseGpx(xml);
|
||||
assert.equal(result.points.length, 2);
|
||||
assert.equal(result.points[0].recordedAt, '2026-10-03T10:00:00Z');
|
||||
assert.match(result.normalizedXml, /<wpt /);
|
||||
assert.match(result.normalizedXml, /<rtept /);
|
||||
assert.doesNotMatch(result.normalizedXml, /<trkpt /);
|
||||
assert.deepEqual(parseGpx(result.normalizedXml).points, result.points);
|
||||
});
|
||||
|
||||
test('waypoint-only routes and missing-position recordings have no invented route coordinates', () => {
|
||||
const waypoint = parseGpx('<gpx><wpt lat="50" lon="10"/></gpx>');
|
||||
assert.equal(waypoint.points.length, 0); assert.equal(waypoint.metrics.distanceM, 0);
|
||||
const empty = parseGpx('<gpx xmlns:wegwichtel="urn:wegwichtel:poi"><extensions><wegwichtel:gpsGap time="2026-10-03T10:00:00Z"/></extensions></gpx>');
|
||||
assert.equal(empty.points.length, 0); assert.equal(empty.metrics.startLat, null);
|
||||
assert.match(empty.normalizedXml, /gpsGap/);
|
||||
assert.throws(() => parseGpx('<gpx><trk>'));
|
||||
assert.throws(() => parseGpx('<gpx><trk><trkseg><trkpt lat="100" lon="10"/></trkseg></trk></gpx>'));
|
||||
test('parseGpx reads synthetic track data without repository example files', () => {
|
||||
const parsed = parseGpx(SAMPLE_GPX);
|
||||
assert.equal(parsed.name, 'Testweg');
|
||||
assert.equal(parsed.points.length, 3);
|
||||
assert.ok(parsed.metrics.distanceM > 0);
|
||||
assert.equal(parsed.points[0].lat, 52.5200);
|
||||
assert.equal(parsed.points[2].lon, 13.4090);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { parseGpx } from '../src/services/gpx.js';
|
||||
import { SAMPLE_GPX } from '../test-support/gpx-fixtures.js';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
@@ -182,8 +183,9 @@ assert result['ok'] is True
|
||||
[pythonHealthCheck.stdout, pythonHealthCheck.stderr].filter(Boolean).join('\n')
|
||||
);
|
||||
|
||||
const gpxPath = path.join(root, 'examples', 'sample-route.gpx');
|
||||
const gpx = await fs.readFile(gpxPath);
|
||||
const gpxPath = path.join(runtime, 'test-route.gpx');
|
||||
const gpx = Buffer.from(SAMPLE_GPX);
|
||||
await fs.writeFile(gpxPath, gpx);
|
||||
const createRoute = await runProcess('python3', [
|
||||
path.join(root, 'tools/python/create_route.py'),
|
||||
'--base-url', LOCAL_API_URL,
|
||||
|
||||
@@ -1,23 +1,85 @@
|
||||
import { readAppFile } from '../test-support/i18n.js';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import test from 'node:test';
|
||||
import { SAMPLE_GPX } from '../test-support/gpx-fixtures.js';
|
||||
|
||||
const root = path.resolve(import.meta.dirname, '..');
|
||||
const script = path.resolve(import.meta.dirname, '../scripts/upload-presentation-route.sh');
|
||||
|
||||
async function read(relativePath) {
|
||||
return readAppFile(relativePath);
|
||||
async function uploadFixture(t) {
|
||||
const runtime = await fs.mkdtemp(path.join(os.tmpdir(), 'wegwichtel-presentation-'));
|
||||
t.after(() => fs.rm(runtime, { recursive: true, force: true }));
|
||||
const bin = path.join(runtime, 'bin');
|
||||
await fs.mkdir(bin);
|
||||
const gpxPath = path.join(runtime, 'track.gpx');
|
||||
const jsonPath = path.join(runtime, 'stations.json');
|
||||
const logPath = path.join(runtime, 'requests.jsonl');
|
||||
const metadata = {
|
||||
name: 'Testweg', description: 'Synthetic upload data',
|
||||
pois: [
|
||||
{ title: 'Start', description: 'First station', lat: 52.5200, lon: 13.4050, triggerRadiusM: 25, sequence: 0 },
|
||||
{ title: 'Ende', description: 'Last station', lat: 52.5216, lon: 13.4090, triggerRadiusM: 30, sequence: 1 }
|
||||
]
|
||||
};
|
||||
await fs.writeFile(gpxPath, SAMPLE_GPX);
|
||||
await fs.writeFile(jsonPath, JSON.stringify(metadata));
|
||||
// Capture curl arguments locally. No requests are sent to a real server.
|
||||
await fs.writeFile(path.join(bin, 'curl'), `#!/usr/bin/env python3
|
||||
import json, os, sys
|
||||
from pathlib import Path
|
||||
log = Path(os.environ['TEST_REQUEST_LOG'])
|
||||
count = len(log.read_text().splitlines()) if log.exists() else 0
|
||||
with log.open('a') as output:
|
||||
output.write(json.dumps(sys.argv[1:]) + '\\n')
|
||||
if os.environ.get('TEST_FAIL_POI') == '1' and count == 1:
|
||||
sys.exit(22)
|
||||
print(json.dumps({'id': 101}))
|
||||
`, { mode: 0o755 });
|
||||
return {
|
||||
gpxPath, jsonPath, logPath, metadata,
|
||||
env: { ...process.env, PATH: `${bin}${path.delimiter}${process.env.PATH}`, TEST_REQUEST_LOG: logPath }
|
||||
};
|
||||
}
|
||||
|
||||
test('presentation example uses real GPX Streckenpunkte and the existing upload API', async () => {
|
||||
const gpx = await read('examples/presentation-route.gpx');
|
||||
const metadata = JSON.parse(await read('examples/presentation-route.json'));
|
||||
const script = await read('scripts/upload-presentation-route.sh');
|
||||
test('presentation uploader accepts explicit test files and posts route and stations through the API', async t => {
|
||||
const fixture = await uploadFixture(t);
|
||||
const result = spawnSync('bash', [script, 'https://example.invalid/api', fixture.gpxPath, fixture.jsonPath], {
|
||||
env: fixture.env, encoding: 'utf8', timeout: 10000
|
||||
});
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
const requests = (await fs.readFile(fixture.logPath, 'utf8')).trim().split('\n').map(line => JSON.parse(line));
|
||||
assert.equal(requests.length, 3);
|
||||
assert.ok(requests[0].includes('https://example.invalid/api/routes'));
|
||||
assert.ok(requests[0].includes(`gpx=@${fixture.gpxPath};type=application/gpx+xml`));
|
||||
for (const [index, poi] of fixture.metadata.pois.entries()) {
|
||||
assert.ok(requests[index + 1].includes('https://example.invalid/api/routes/101/pois'));
|
||||
assert.ok(requests[index + 1].includes(`title=${poi.title}`));
|
||||
assert.ok(requests[index + 1].includes(`sequence=${poi.sequence}`));
|
||||
assert.ok(requests[index + 1].includes('POST'));
|
||||
}
|
||||
assert.match(result.stdout, /Server-ID: 101/);
|
||||
});
|
||||
|
||||
assert.ok((gpx.match(/<trkpt\b/g) || []).length >= 8);
|
||||
assert.doesNotMatch(gpx, /<rtept\b/);
|
||||
assert.ok(metadata.pois.length >= 3);
|
||||
assert.match(script, /POST "\$BASE_URL\/routes"/);
|
||||
assert.match(script, /POST "\$BASE_URL\/routes\/\$ROUTE_ID\/pois"/);
|
||||
test('presentation uploader reports missing files before any HTTP request', async t => {
|
||||
const fixture = await uploadFixture(t);
|
||||
for (const args of [[], ['https://example.invalid/api', fixture.gpxPath, `${fixture.jsonPath}.missing`]]) {
|
||||
const result = spawnSync('bash', [script, ...args], { env: fixture.env, encoding: 'utf8', timeout: 10000 });
|
||||
assert.equal(result.status, 2);
|
||||
assert.match(result.stderr, /Aufruf:/);
|
||||
}
|
||||
await assert.rejects(fs.access(fixture.logPath), { code: 'ENOENT' });
|
||||
});
|
||||
|
||||
test('presentation uploader removes an incomplete route if a station upload fails', async t => {
|
||||
const fixture = await uploadFixture(t);
|
||||
const result = spawnSync('bash', [script, 'https://example.invalid/api', fixture.gpxPath, fixture.jsonPath], {
|
||||
env: { ...fixture.env, TEST_FAIL_POI: '1' }, encoding: 'utf8', timeout: 10000
|
||||
});
|
||||
assert.notEqual(result.status, 0);
|
||||
const requests = (await fs.readFile(fixture.logPath, 'utf8')).trim().split('\n').map(line => JSON.parse(line));
|
||||
assert.equal(requests.length, 3);
|
||||
assert.ok(requests[2].includes('DELETE'));
|
||||
assert.ok(requests[2].includes('https://example.invalid/api/routes/101'));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user