renew schema and tests for new dtabases
Sonarqube Scanner / Prepare environment (push) Successful in 10s
Sonarqube Scanner / Build and test (push) Successful in 32s
Sonarqube Scanner / SonarQube analysis (push) Successful in 1m41s
Sonarqube Scanner / Deploy to Server (push) Successful in 10s

This commit is contained in:
Florian Zumpe
2026-10-03 13:57:17 +02:00
parent edfe227641
commit 5fe79067b0
9 changed files with 189 additions and 108 deletions
+46 -25
View File
@@ -5,6 +5,7 @@ import test from 'node:test';
import { DatabaseSync } from 'node:sqlite';
import { config } from '../src/config.js';
import { parseGpx } from '../src/services/gpx.js';
const databasePath = path.join(config.dataDir, 'wegwichtel.sqlite');
const activeRoutesDirectory = path.join(config.storageDir, 'active', 'routes');
@@ -17,26 +18,22 @@ const EXPECTED_SCHEMA = Object.freeze({
name: { type: 'TEXT', notnull: 1 },
status: { type: 'TEXT', notnull: 1 },
gpx_path: { type: 'TEXT', notnull: 1 },
point_count: { type: 'INTEGER', notnull: 1 },
min_lat: { type: 'REAL', notnull: 0 },
min_lon: { type: 'REAL', notnull: 0 },
max_lat: { type: 'REAL', notnull: 0 },
max_lon: { type: 'REAL', notnull: 0 },
created_at: { type: 'TEXT', notnull: 1 },
updated_at: { type: 'TEXT', notnull: 1 },
deleted_at: { type: 'TEXT' },
activated_at: { type: 'TEXT' },
trash_path: { type: 'TEXT' }
},
route_points: {
id: { type: 'INTEGER', pk: 1 },
route_id: { type: 'INTEGER', notnull: 1 },
sequence: { type: 'INTEGER', notnull: 1 },
lat: { type: 'REAL', notnull: 1 },
lon: { type: 'REAL', notnull: 1 }
},
pois: {
id: { type: 'INTEGER', pk: 1 },
route_id: { type: 'INTEGER', notnull: 1 },
title: { type: 'TEXT', notnull: 1 },
lat: { type: 'REAL', notnull: 1 },
lon: { type: 'REAL', notnull: 1 },
lat: { type: 'REAL', notnull: 0 },
lon: { type: 'REAL', notnull: 0 },
trigger_radius_m: { type: 'REAL', notnull: 1 },
sequence: { type: 'INTEGER', notnull: 1 },
audio_path: { type: 'TEXT' },
@@ -132,21 +129,23 @@ function assertDatabaseIntegrity(db) {
);
}
function assertRoutePointCounts(db) {
const rows = db.prepare(`
SELECT r.id, r.point_count AS expected_count, COUNT(rp.id) AS actual_count
FROM routes r
LEFT JOIN route_points rp ON rp.route_id = r.id
GROUP BY r.id, r.point_count
ORDER BY r.id
`).all();
function assertGpxRouteStorage(db) {
const tables = db.prepare('PRAGMA table_list').all();
assert.equal(tables.some(table => table.name === 'route_points'), false,
'Veraltete Tabelle route_points vorhanden; Datenbank mit dem aktuellen Schema neu erstellen.');
const columns = db.prepare('PRAGMA table_info(routes)').all().map(column => column.name);
for (const column of ['start_lat', 'start_lon', 'center_lat', 'center_lon', 'distance_m', 'elevation_gain_m', 'point_count']) {
assert.equal(columns.includes(column), false, `Veraltete Routenspalte ${column} vorhanden.`);
}
}
for (const row of rows) {
assert.equal(
row.actual_count,
row.expected_count,
`Strecke ${row.id}: point_count=${row.expected_count}, tatsächlich vorhanden=${row.actual_count}.`
);
function assertRouteBounds(db) {
for (const route of db.prepare('SELECT id, gpx_path, min_lat, min_lon, max_lat, max_lon FROM routes').all()) {
const filename = assertInsideStorage(route.gpx_path, `Strecke ${route.id} GPX`);
const { metrics } = parseGpx(fs.readFileSync(filename, 'utf8'), null, { allowEmpty: true });
for (const [column, metric] of [['min_lat', 'minLat'], ['min_lon', 'minLon'], ['max_lat', 'maxLat'], ['max_lon', 'maxLon']]) {
assert.equal(route[column], metrics[metric], `Strecke ${route.id}: ${column} stimmt nicht mit der GPX-Datei überein.`);
}
}
}
@@ -265,11 +264,33 @@ test('Laufzeitdatenbank und gespeicherte Streckendaten sind konsistent', () => {
assertTableSchema(db, tableName, columns);
}
assertStrictTables(db);
assertRoutePointCounts(db);
assertGpxRouteStorage(db);
assertActiveRoutes(db);
assertDeletedRoutes(db);
assertRouteBounds(db);
assertMediaPaths(db);
} finally {
db.close();
}
});
test('fresh schema passes the start check and permits stations without coordinates', () => {
const db = new DatabaseSync(':memory:', { enableForeignKeyConstraints: true });
try {
db.exec(fs.readFileSync(path.join(config.projectRoot, 'database/schema.sql'), 'utf8'));
for (const [name, columns] of Object.entries(EXPECTED_SCHEMA)) assertTableSchema(db, name, columns);
assertStrictTables(db);
assertGpxRouteStorage(db);
db.prepare("INSERT INTO routes (name, slug, gpx_path) VALUES ('Ohne GPS', 'ohne-gps', 'test.gpx')").run();
db.prepare("INSERT INTO pois (route_id, title, lat, lon) VALUES (1, 'Drinnen', NULL, NULL)").run();
assertDatabaseIntegrity(db);
assert.equal(db.prepare('SELECT lat FROM pois').get().lat, null);
db.exec('CREATE TABLE route_points (id INTEGER)');
assert.throws(() => assertGpxRouteStorage(db), /Veraltete Tabelle/);
db.exec('DROP TABLE route_points; ALTER TABLE routes ADD COLUMN point_count INTEGER');
assert.throws(() => assertGpxRouteStorage(db), /Veraltete Routenspalte/);
} finally {
db.close();
}
});