Files
TREK/server/tests/unit/db/tour-imported-difficulty-migration.test.ts
Maurice 98cc57942b feat(tours): 3D terrain view with flyover and grade colours, imported tours carry no hiking difficulty
The tours map gets a 3D mode on the same map surface between the side panels:
a "3D" switch in the layer pill drapes the route over open Terrarium elevation
tiles (AWS Open Data), colours it by grade, plays a flyover tied to the
elevation profile in the rail, and keeps planning working by clicking and
dragging waypoints on the terrain. The corridor around the route is preloaded
before the flyover so the landscape never loads in front of the camera.

GPX imports no longer get a made-up hiking difficulty: max_hiking_difficulty
becomes nullable (new migration, waypoints kept), the import writes null and
the client shows no T badge for it. Drawn tours still default to T2.

Info hints in the planner show their tooltip only, and the discard
confirmation reads "Discard changes".
2026-10-14 14:02:27 +02:00

122 lines
5.0 KiB
TypeScript

/**
* `Migration20200101043100_an_imported_tour_has_no_hiking_difficulty`:
* `tours.max_hiking_difficulty` turns nullable through a rebuild of `tours`
* and `tour_waypoints`, without losing a row of either, and the constraints
* around it still hold.
*/
import { createMigrationOrm, migrateTo, pendingNames, rawExec, rawQuery } from '../../helpers/migration-step';
import type { MikroORM } from '@mikro-orm/sqlite';
import { describe, expect, it } from 'vitest';
const TARGET = 'Migration20200101043100_an_imported_tour_has_no_hiking_difficulty';
async function ormBeforeTarget(): Promise<MikroORM> {
const orm = await createMigrationOrm();
const names = await pendingNames(orm);
const index = names.indexOf(TARGET);
expect(index).toBeGreaterThan(0);
await migrateTo(orm, names[index - 1]);
return orm;
}
async function seedTripWithPlaces(orm: MikroORM, placeIds: number[]): Promise<void> {
await rawExec(orm, "INSERT INTO users (id, username, email, password_hash) VALUES (1, 'owner', 'owner@test', 'x')");
await rawExec(orm, "INSERT INTO trips (id, user_id, title) VALUES (10, 1, 'Alps')");
const rows = placeIds.map((id) => `(${id}, 10, 'Place ${id}')`).join(', ');
await rawExec(orm, `INSERT INTO places (id, trip_id, name) VALUES ${rows}`);
}
describe('Imported tour hiking difficulty migration', () => {
it('TOURDIFFMIG-001: keeps every tour and waypoint while the difficulty becomes optional', async () => {
const orm = await ormBeforeTarget();
try {
await seedTripWithPlaces(orm, [100, 101]);
await rawExec(
orm,
`INSERT INTO tours (place_id, tour_type, distance, difficulty, wanderer_ref, match_confidence, created_at,
max_hiking_difficulty, planned_duration_minutes, break_additional_minutes)
VALUES (100, 'hike', 12.5, 'T3', 'w-1', 0.3, '2026-01-02 10:00:00', 4, 125, 35)`,
);
await rawExec(
orm,
`INSERT INTO tour_waypoints (place_id, lat, lng, role, sequence)
VALUES (100, 47, 11, 'start', 0), (100, 47.1, 11.1, 'end', 1)`,
);
await migrateTo(orm, TARGET);
expect(await rawQuery(orm, 'SELECT * FROM tours')).toEqual([
{
place_id: 100,
tour_type: 'hike',
distance: 12.5,
elevation_gain: null,
elevation_loss: null,
duration: null,
difficulty: 'T3',
wanderer_ref: 'w-1',
match_confidence: 0.3,
created_at: '2026-01-02 10:00:00',
max_hiking_difficulty: 4,
planned_duration_minutes: 125,
break_additional_minutes: 35,
},
]);
expect(await rawQuery(orm, 'SELECT id, place_id, role, sequence FROM tour_waypoints ORDER BY id')).toEqual([
{ id: 1, place_id: 100, role: 'start', sequence: 0 },
{ id: 2, place_id: 100, role: 'end', sequence: 1 },
]);
const column = await rawQuery<{ notnull: number; dflt_value: string | null }>(
orm,
"SELECT \"notnull\", dflt_value FROM pragma_table_info('tours') WHERE name = 'max_hiking_difficulty'",
);
expect(column).toEqual([{ notnull: 0, dflt_value: '2' }]);
await rawExec(orm, "INSERT INTO tours (place_id, tour_type, max_hiking_difficulty) VALUES (101, 'hike', NULL)");
expect(await rawQuery(orm, 'SELECT max_hiking_difficulty FROM tours WHERE place_id = 101')).toEqual([
{ max_hiking_difficulty: null },
]);
await expect(rawExec(orm, 'UPDATE tours SET max_hiking_difficulty = 7 WHERE place_id = 101')).rejects.toThrow(
/CHECK/,
);
} finally {
await orm.close(true);
}
}, 30000);
it('TOURDIFFMIG-002: a drawn tour still defaults to 2 and the cascades and index survive the rebuild', async () => {
const orm = await ormBeforeTarget();
try {
await seedTripWithPlaces(orm, [100]);
await migrateTo(orm, TARGET);
await rawExec(orm, "INSERT INTO tours (place_id, tour_type) VALUES (100, 'hike')");
await rawExec(
orm,
"INSERT INTO tour_waypoints (place_id, lat, lng, role, sequence) VALUES (100, 47, 11, 'start', 0)",
);
expect(await rawQuery(orm, 'SELECT max_hiking_difficulty FROM tours')).toEqual([{ max_hiking_difficulty: 2 }]);
await expect(
rawExec(orm, "INSERT INTO tour_waypoints (place_id, lat, lng, role, sequence) VALUES (999, 47, 11, 'end', 1)"),
).rejects.toThrow(/FOREIGN KEY/);
const indexes = await rawQuery<{ name: string }>(
orm,
"SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = 'tours' AND name = 'idx_tours_tour_type'",
);
expect(indexes).toEqual([{ name: 'idx_tours_tour_type' }]);
await rawExec(orm, 'DELETE FROM places WHERE id = 100');
expect(await rawQuery(orm, 'SELECT COUNT(*) AS n FROM tours')).toEqual([{ n: 0 }]);
expect(await rawQuery(orm, 'SELECT COUNT(*) AS n FROM tour_waypoints')).toEqual([{ n: 0 }]);
expect(await rawQuery(orm, 'SELECT * FROM pragma_foreign_key_check')).toEqual([]);
} finally {
await orm.close(true);
}
}, 30000);
});