From 95026aac6e4565c850b60ba7a7cd317f571f0558 Mon Sep 17 00:00:00 2001 From: Florian Zumpe Date: Wed, 17 Jun 2026 15:12:46 +0200 Subject: [PATCH] fixed sonarqube scan issues --- .coveragerc | 11 ++ .gitignore | 4 + README.md | 12 +- package-lock.json | 4 +- package.json | 11 +- requirements-dev.txt | 1 + sonar-project.properties | 2 + src/middleware/upload.js | 87 ++++++++-- test/backend-services.test.js | 19 +++ test/python/test_commands.py | 175 ++++++++++++++++++++ test/python/test_common.py | 237 ++++++++++++++++++++++++++++ test/upload-detection-tools.test.js | 11 +- tools/python/README.md | 16 +- tools/python/append_route.py | 27 +++- tools/python/common.py | 141 ++++++++++++----- tools/python/create_audio.py | 29 +++- tools/python/create_picture.py | 40 +++-- tools/python/create_poi.py | 38 +++-- tools/python/create_route.py | 31 ++-- tools/python/delete_audio.py | 27 +++- tools/python/delete_picture.py | 29 +++- tools/python/delete_poi.py | 27 +++- tools/python/delete_route.py | 25 ++- tools/python/restore_route.py | 25 ++- tools/python/update_audio.py | 29 +++- tools/python/update_picture.py | 35 ++-- tools/python/update_poi.py | 32 ++-- tools/python/update_route.py | 32 ++-- 28 files changed, 973 insertions(+), 184 deletions(-) create mode 100644 .coveragerc create mode 100644 requirements-dev.txt create mode 100644 sonar-project.properties create mode 100644 test/python/test_commands.py create mode 100644 test/python/test_common.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..3b3d1dd --- /dev/null +++ b/.coveragerc @@ -0,0 +1,11 @@ +[run] +branch = True +relative_files = True +source = tools/python + +[report] +show_missing = True +skip_empty = True + +[xml] +output = coverage/python/coverage.xml diff --git a/.gitignore b/.gitignore index d92857e..792848a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,7 @@ npm-debug.log* coverage/ .gitea/workflows/ + +.coverage +__pycache__/ +*.pyc diff --git a/README.md b/README.md index eae4176..82bd2c3 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ Die vollständige REST-API ist einschließlich aller Pfad-, Query-, Formular- un ## Python-Werkzeuge -Unter [`tools/python/`](tools/python/README.md) liegen eigenständige Python-3-Skripte zum Anlegen, Ändern, Erweitern, Wiederherstellen und Löschen von Routen, POIs, Bildern und Audiodateien. Sie verwenden ausschließlich die Python-Standardbibliothek. Fehlende Parameter werden interaktiv abgefragt. Antwortet ein vorgeschalteter Passwortschutz mit HTTP 401, fragt die gemeinsame Request-Schicht Benutzername und Passwort ab und wiederholt den Request. +Unter [`tools/python/`](tools/python/README.md) liegen eigenständige Python-3-Skripte zum Anlegen, Ändern, Erweitern, Wiederherstellen und Löschen von Routen, POIs, Bildern und Audiodateien. Die Werkzeuge selbst verwenden ausschließlich die Python-Standardbibliothek. Fehlende Parameter werden interaktiv abgefragt. Antwortet ein vorgeschalteter Passwortschutz mit HTTP 401, fragt die gemeinsame Request-Schicht Benutzername und Passwort ab und wiederholt den Request. Fehler werden mit einer verständlichen Zusammenfassung und dem vollständigen Python-Stacktrace auf `stderr` ausgegeben. ## Dateistruktur @@ -105,6 +105,16 @@ Der lokale Proxy lässt `GET`, `HEAD` und `OPTIONS` ohne Anmeldung passieren. F 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. +Die kombinierte Coverage wird mit folgenden Befehlen erzeugt: + +```bash +python3 -m pip install -r requirements-dev.txt +npm ci +npm run test:coverage +``` + +Dabei entstehen `coverage/js/lcov.info` für JavaScript und `coverage/python/coverage.xml` für die Python-Werkzeuge. Die zugehörigen SonarQube-Pfade stehen in `sonar-project.properties`. Die Coverage-Skripte erzwingen selbst keinen Mindestwert; die Bewertung und die Hinweise zu nicht abgedeckten Stellen bleiben Aufgabe des SonarQube Quality Gates. + ## Technische Hinweise - Medienpfade werden relativ zu `storage/` gespeichert. So bleibt das Projekt verschiebbar. diff --git a/package-lock.json b/package-lock.json index bf00abe..0ac6f64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wegwichtel-next", - "version": "0.12.5", + "version": "0.12.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wegwichtel-next", - "version": "0.12.5", + "version": "0.12.6", "hasInstallScript": true, "dependencies": { "@file-type/av": "0.2.0", diff --git a/package.json b/package.json index 2316b06..ded0b3b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wegwichtel-next", - "version": "0.12.5", + "version": "0.12.6", "private": true, "description": "GPS-gestützte Lern- und Wanderwege mit GPX, POIs, Bildern und Audioansagen.", "type": "module", @@ -13,9 +13,12 @@ "start": "node --env-file-if-exists=.env server.js", "dev": "node --env-file-if-exists=.env --watch server.js", "init-db": "node --env-file-if-exists=.env scripts/init-db.js", - "check": "node --check server.js && node --test", - "test": "node --test", - "test:coverage": "c8 --exclude='test/**' --exclude='test-support/**' --reporter=text-summary --reporter=lcov --reports-dir=coverage/js node --test" + "check": "node --check server.js && npm test", + "test": "node --test && npm run test:python", + "test:coverage": "npm run test:coverage:js && npm run test:coverage:python", + "test:python": "python3 -m unittest discover -s test/python -p 'test_*.py'", + "test:coverage:js": "c8 --exclude='test/**' --exclude='test-support/**' --reporter=text-summary --reporter=lcov --reports-dir=coverage/js node --test", + "test:coverage:python": "python3 -m coverage erase && python3 -m coverage run -m unittest discover -s test/python -p 'test_*.py' && python3 -m coverage xml -o coverage/python/coverage.xml && python3 -m coverage report" }, "dependencies": { "@file-type/av": "0.2.0", diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..18f2c70 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1 @@ +coverage==7.13.3 diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..0df5e09 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,2 @@ +sonar.javascript.lcov.reportPaths=coverage/js/lcov.info +sonar.python.coverage.reportPaths=coverage/python/coverage.xml diff --git a/src/middleware/upload.js b/src/middleware/upload.js index 795cbf3..e2a221b 100644 --- a/src/middleware/upload.js +++ b/src/middleware/upload.js @@ -8,10 +8,72 @@ import { detectAv } from '@file-type/av'; import { config } from '../config.js'; import { HttpError } from './errors.js'; +function sanitizeFilename(value) { + return String(value).replaceAll(/[^a-zA-Z0-9._-]/g, '_') || 'upload'; +} + +function stripBase64Padding(value) { + let end = value.length; + while (end > 0 && value[end - 1] === '=') end -= 1; + return value.slice(0, end); +} + +function base64FromDataUrl(value, fieldName) { + const dataUrl = value.trim(); + const marker = ';base64,'; + const markerIndex = dataUrl.indexOf(marker); + if (!dataUrl.startsWith('data:') || markerIndex < 5) { + throw new HttpError(400, `${fieldName}.dataUrl muss eine Base64-Data-URL sein.`); + } + return dataUrl.slice(markerIndex + marker.length); +} + +function isXmlNameCharacter(character, first = false) { + const code = character.codePointAt(0); + const letter = (code >= 65 && code <= 90) || (code >= 97 && code <= 122); + if (letter || character === '_') return true; + return !first && ((code >= 48 && code <= 57) || character === '.' || character === '-' || character === ':'); +} + +function rootElementName(xml) { + let cursor = 0; + while (cursor < xml.length) { + const opening = xml.indexOf('<', cursor); + if (opening < 0 || opening + 1 >= xml.length) return null; + + if (xml.startsWith('', opening + 4); + if (end < 0) return null; + cursor = end + 3; + continue; + } + if (xml.startsWith('', opening + 2); + if (end < 0) return null; + cursor = end + 2; + continue; + } + if (xml.startsWith('', opening + 2); + if (end < 0) return null; + cursor = end + 1; + continue; + } + if (xml.startsWith(')/i.test(start)) { + const prefix = xml.slice(0, 131072); + const start = prefix.startsWith('\uFEFF') ? prefix.slice(1) : prefix; + const rootName = rootElementName(start); + const localName = rootName?.split(':').at(-1); + if (localName !== 'gpx') { throw new HttpError(415, 'Der Upload enthält kein GPX-Dokument.'); } return { ext: 'gpx', mime: 'application/gpx+xml' }; @@ -146,7 +207,7 @@ async function detectAndValidate(file, kind) { const rules = ALLOWED_TYPES[kind]; const baseMime = String(detected.mime).split(';', 1)[0].toLowerCase(); const extension = String(detected.ext).toLowerCase(); - if (!rules || !rules.mime.has(baseMime) || !rules.ext.has(extension)) { + if (!rules?.mime.has(baseMime) || !rules?.ext.has(extension)) { throw new HttpError(415, kind === 'picture' ? `Nicht unterstütztes Bildformat: ${detected.mime}` : `Nicht unterstütztes Audioformat: ${detected.mime}`); diff --git a/test/backend-services.test.js b/test/backend-services.test.js index b7440b8..e4ce756 100644 --- a/test/backend-services.test.js +++ b/test/backend-services.test.js @@ -326,6 +326,25 @@ test('upload inspection handles JSON, GPX, binary validation and cleanup paths', assert.equal(validGpxReq.file.detectedExtension, 'gpx'); storage.removeUpload(validGpxReq.file); + const namespacedGpx = ''; + const namespacedGpxReq = { body: {}, file: uploadedFile('namespaced.xml', namespacedGpx) }; + assert.equal(await invokeUpload(inspectUpload('gpx', 'gpx'), namespacedGpxReq), undefined); + storage.removeUpload(namespacedGpxReq.file); + + for (const malformed of [ + 'plain text', + '