renew schema and tests for new dtabases
This commit is contained in:
@@ -95,6 +95,21 @@ test('route, POI, media and storage services cover successful and rejected lifec
|
||||
assert.equal(route.name, 'Äußerst schöne Strecke');
|
||||
assert.equal(route.pointCount, 2);
|
||||
assert.ok(fs.existsSync(routes.getRouteGpxFile(db, route.id).absolutePath));
|
||||
assert.equal(db.prepare("SELECT name FROM sqlite_master WHERE name='route_points'").get(), undefined);
|
||||
const routeColumns = db.prepare('PRAGMA table_info(routes)').all().map(column => column.name);
|
||||
for (const name of ['start_lat', 'center_lat', 'distance_m', 'elevation_gain_m', 'point_count']) {
|
||||
assert.equal(routeColumns.includes(name), false);
|
||||
}
|
||||
const gpxFilename = routes.getRouteGpxFile(db, route.id).absolutePath;
|
||||
const originalGpx = fs.readFileSync(gpxFilename, 'utf8');
|
||||
fs.writeFileSync(gpxFilename, GPX_APPEND);
|
||||
const fromFile = routes.getRoute(db, route.id);
|
||||
assert.equal(fromFile.pointCount, 1);
|
||||
assert.equal(fromFile.points[0].lat, 50.002);
|
||||
assert.equal(fromFile.start.lat, 50.002);
|
||||
assert.equal(fromFile.distanceM, 0);
|
||||
assert.equal(routes.listRoutes(db, {})[0].pointCount, 1);
|
||||
fs.writeFileSync(gpxFilename, originalGpx);
|
||||
|
||||
const duplicate = routes.createRoute(db, {
|
||||
name: 'Zweite Strecke',
|
||||
@@ -127,6 +142,17 @@ test('route, POI, media and storage services cover successful and rejected lifec
|
||||
expectHttpError(() => routes.appendRoute(db, 999999, uploadedFile('missing.gpx', GPX)), 404);
|
||||
const appended = routes.appendRoute(db, route.id, uploadedFile('append.gpx', GPX));
|
||||
assert.equal(appended.pointCount, 3);
|
||||
assert.equal(db.prepare('SELECT max_lat FROM routes WHERE id=?').get(route.id).max_lat, 50.002);
|
||||
|
||||
const indoor = routes.createRoute(db, { name: 'Drinnen' }, uploadedFile('indoor.gpx',
|
||||
'<gpx xmlns:wegwichtel="urn:wegwichtel:poi"><extensions><wegwichtel:gpsGap time="2026-10-03T10:00:00Z"/></extensions></gpx>'));
|
||||
const indoorStation = routes.createPoi(db, indoor.id, { title: 'Ohne GPS', lat: null, lon: null });
|
||||
assert.equal(routes.getRoute(db, indoor.id).pois[0].lat, null);
|
||||
routes.updatePoi(db, indoor.id, indoorStation.id, { lat: 50, lon: 10 });
|
||||
assert.equal(db.prepare('SELECT min_lat FROM routes WHERE id=?').get(indoor.id).min_lat, 50);
|
||||
routes.deletePoi(db, indoor.id, indoorStation.id);
|
||||
assert.equal(routes.getRoute(db, indoor.id).pointCount, 0);
|
||||
assert.equal(db.prepare('SELECT min_lat FROM routes WHERE id=?').get(indoor.id).min_lat, null);
|
||||
|
||||
expectHttpError(() => routes.createPoi(db, 999999, { lat: 1, lon: 2 }), 404);
|
||||
expectHttpError(() => routes.createPoi(db, route.id, { lat: 'x', lon: 2 }), 400);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
import test from 'node:test';
|
||||
|
||||
const root = path.resolve(import.meta.dirname, '..');
|
||||
|
||||
test('prestart validates a real database, GPX bounds and nullable station coordinates', () => {
|
||||
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'wegwichtel-prestart-'));
|
||||
const dataDir = path.join(temporary, 'data');
|
||||
const storageDir = path.join(temporary, 'storage');
|
||||
fs.mkdirSync(dataDir);
|
||||
const routeDir = path.join(storageDir, 'active/routes/1');
|
||||
fs.mkdirSync(routeDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(routeDir, 'route.gpx'), '<gpx><rte><rtept lat="50" lon="10"/></rte></gpx>');
|
||||
const db = new DatabaseSync(path.join(dataDir, 'wegwichtel.sqlite'));
|
||||
const environment = { ...process.env, DATA_DIR: dataDir, STORAGE_DIR: storageDir };
|
||||
delete environment.NODE_TEST_CONTEXT;
|
||||
const check = () => spawnSync(process.execPath, ['--test', 'test/runtime-database-integrity.test.js'], {
|
||||
cwd: root, encoding: 'utf8', env: environment
|
||||
});
|
||||
try {
|
||||
db.exec(fs.readFileSync(path.join(root, 'database/schema.sql'), 'utf8'));
|
||||
db.prepare(`INSERT INTO routes (name, slug, gpx_path, min_lat, min_lon, max_lat, max_lon)
|
||||
VALUES ('Test', 'test', 'active/routes/1/route.gpx', 50, 10, 50, 10)`).run();
|
||||
db.prepare("INSERT INTO pois (route_id, title, lat, lon) VALUES (1, 'Ohne GPS', NULL, NULL)").run();
|
||||
let result = check();
|
||||
assert.equal(result.status, 0, result.stdout + result.stderr);
|
||||
db.exec('UPDATE routes SET min_lat=49');
|
||||
result = check();
|
||||
assert.notEqual(result.status, 0, result.stdout + result.stderr);
|
||||
assert.match(result.stdout + result.stderr, /min_lat stimmt nicht mit der GPX-Datei/);
|
||||
db.exec('UPDATE routes SET min_lat=50');
|
||||
fs.writeFileSync(path.join(routeDir, 'route.gpx'), '<gpx><rte>');
|
||||
result = check();
|
||||
assert.notEqual(result.status, 0);
|
||||
assert.match(result.stdout + result.stderr, /kein gültiges XML/);
|
||||
db.exec('DROP TABLE pois; CREATE TABLE pois (id INTEGER PRIMARY KEY, route_id INTEGER NOT NULL, title TEXT NOT NULL, lat REAL NOT NULL, lon REAL NOT NULL) STRICT');
|
||||
result = check();
|
||||
assert.notEqual(result.status, 0);
|
||||
assert.match(result.stdout + result.stderr, /Falsche NOT NULL-Eigenschaft für pois.lat/);
|
||||
} finally {
|
||||
db.close();
|
||||
fs.rmSync(temporary, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user