511 lines
22 KiB
JavaScript
511 lines
22 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import fsp from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
|
|
const runtime = await fsp.mkdtemp(path.join(os.tmpdir(), 'wegwichtel-services-'));
|
|
process.env.DATA_DIR = path.join(runtime, 'data');
|
|
process.env.STORAGE_DIR = path.join(runtime, 'storage');
|
|
|
|
const { config, ensureRuntimeDirectories } = await import('../src/config.js');
|
|
const { openDatabase, transaction } = await import('../src/database.js');
|
|
const { HttpError, notFoundHandler, errorHandler } = await import('../src/middleware/errors.js');
|
|
const express = (await import('express')).default;
|
|
const { createApiRouter } = await import('../src/routes/api.js');
|
|
const { inspectUpload } = await import('../src/middleware/upload.js');
|
|
const routes = await import('../src/services/routes-service.js');
|
|
const media = await import('../src/services/media-service.js');
|
|
const storage = await import('../src/services/storage.js');
|
|
|
|
ensureRuntimeDirectories();
|
|
|
|
const PNG_FILE = Buffer.from(
|
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9ZQmcAAAAASUVORK5CYII=',
|
|
'base64'
|
|
);
|
|
const MP3_FILE = Buffer.concat([
|
|
Buffer.from('49443304000000000000', 'hex'),
|
|
Buffer.from('fffb9064', 'hex'),
|
|
Buffer.alloc(512)
|
|
]);
|
|
const GPX = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<gpx version="1.1" creator="test"><metadata><name>Teststrecke</name></metadata><trk><trkseg>
|
|
<trkpt lat="50.0000" lon="10.0000"><ele>100</ele></trkpt>
|
|
<trkpt lat="50.0010" lon="10.0010"><ele>110</ele></trkpt>
|
|
</trkseg></trk></gpx>`;
|
|
const GPX_APPEND = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<gpx version="1.1" creator="test"><trk><trkseg>
|
|
<trkpt lat="50.0020" lon="10.0020"><ele>105</ele></trkpt>
|
|
</trkseg></trk></gpx>`;
|
|
|
|
let uploadCounter = 0;
|
|
function uploadedFile(name, contents, extra = {}) {
|
|
const filePath = path.join(config.storageDir, 'uploads', `${Date.now()}-${uploadCounter += 1}-${name}`);
|
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
fs.writeFileSync(filePath, contents);
|
|
return {
|
|
fieldname: extra.fieldname || 'file',
|
|
originalname: name,
|
|
encoding: '7bit',
|
|
mimetype: extra.mimetype || 'application/octet-stream',
|
|
destination: path.dirname(filePath),
|
|
filename: path.basename(filePath),
|
|
path: filePath,
|
|
size: Buffer.byteLength(contents),
|
|
...extra
|
|
};
|
|
}
|
|
|
|
function expectHttpError(callback, status) {
|
|
assert.throws(callback, error => error instanceof HttpError && error.status === status);
|
|
}
|
|
|
|
async function invokeUpload(middleware, req) {
|
|
return new Promise(resolve => {
|
|
middleware(req, {}, error => resolve(error));
|
|
});
|
|
}
|
|
|
|
test.after(async () => {
|
|
await fsp.rm(runtime, { recursive: true, force: true });
|
|
});
|
|
|
|
test('route, POI, media and storage services cover successful and rejected lifecycle paths', async () => {
|
|
const db = openDatabase(path.join(config.dataDir, 'services.sqlite'));
|
|
try {
|
|
assert.equal(transaction(db, () => 42), 42);
|
|
assert.throws(() => transaction(db, () => {
|
|
throw new Error('rollback');
|
|
}), /rollback/);
|
|
|
|
expectHttpError(() => routes.createRoute(db, {}, null), 400);
|
|
|
|
const invalidGpx = uploadedFile('invalid.gpx', '<xml/>');
|
|
expectHttpError(() => routes.createRoute(db, { name: 'Ungültig' }, invalidGpx), 400);
|
|
assert.equal(fs.existsSync(invalidGpx.path), false);
|
|
|
|
const route = routes.createRoute(db, {
|
|
name: 'Äußerst schöne Strecke',
|
|
slug: ' gleiche route ',
|
|
description: 'Beschreibung',
|
|
schoolName: 'Schule'
|
|
}, uploadedFile('route.gpx', GPX));
|
|
assert.equal(route.name, 'Äußerst schöne Strecke');
|
|
assert.equal(route.pointCount, 2);
|
|
assert.ok(fs.existsSync(routes.getRouteGpxFile(db, route.id).absolutePath));
|
|
|
|
const duplicate = routes.createRoute(db, {
|
|
name: 'Zweite Strecke',
|
|
slug: ' gleiche route '
|
|
}, uploadedFile('route-2.gpx', GPX));
|
|
assert.notEqual(duplicate.slug, route.slug);
|
|
|
|
assert.deepEqual(routes.listRoutes(db, {}).map(item => item.id), [route.id, duplicate.id]);
|
|
assert.equal(routes.listRoutes(db, { lat: 90, lon: 90, radiusKm: 0.001 }).length, 0);
|
|
assert.ok(routes.listRoutes(db, {
|
|
lat: route.start.lat,
|
|
lon: route.start.lon,
|
|
radiusKm: 1
|
|
})[0].proximityM <= 1);
|
|
expectHttpError(() => routes.getRoute(db, 999999), 404);
|
|
expectHttpError(() => routes.getRouteGpxFile(db, 999999), 404);
|
|
expectHttpError(() => routes.updateRoute(db, 999999, {}, null), 404);
|
|
|
|
const renamed = routes.updateRoute(db, route.id, {
|
|
name: 'Umbenannt',
|
|
description: 'Neu',
|
|
schoolName: 'Neue Schule'
|
|
}, null);
|
|
assert.equal(renamed.name, 'Umbenannt');
|
|
|
|
const replaced = routes.updateRoute(db, route.id, {}, uploadedFile('replacement.gpx', GPX_APPEND));
|
|
assert.equal(replaced.pointCount, 1);
|
|
|
|
expectHttpError(() => routes.appendRoute(db, route.id, null), 400);
|
|
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);
|
|
|
|
expectHttpError(() => routes.createPoi(db, 999999, { lat: 1, lon: 2 }), 404);
|
|
expectHttpError(() => routes.createPoi(db, route.id, { lat: 'x', lon: 2 }), 400);
|
|
const poi = routes.createPoi(db, route.id, {
|
|
title: '',
|
|
description: 'POI',
|
|
lat: route.start.lat,
|
|
lon: route.start.lon,
|
|
triggerRadiusM: '55',
|
|
sequence: '1'
|
|
});
|
|
assert.equal(poi.title, 'Unbenannter POI');
|
|
const secondPoi = routes.createPoi(db, route.id, {
|
|
title: 'Zweiter POI',
|
|
lat: route.start.lat,
|
|
lon: route.start.lon,
|
|
sequence: '2'
|
|
});
|
|
|
|
expectHttpError(() => routes.getPoi(db, route.id, 999999), 404);
|
|
expectHttpError(() => routes.updatePoi(db, route.id, 999999, {}), 404);
|
|
expectHttpError(() => routes.updatePoi(db, route.id, poi.id, { sequence: -1 }), 400);
|
|
const updatedPoi = routes.updatePoi(db, route.id, poi.id, {
|
|
title: 'Aktualisiert',
|
|
lat: route.start.lat + 0.001,
|
|
lon: route.start.lon + 0.001,
|
|
triggerRadiusM: 60,
|
|
sequence: 0
|
|
});
|
|
assert.equal(updatedPoi.title, 'Aktualisiert');
|
|
assert.equal(routes.listPois(db, route.id)[0].id, poi.id);
|
|
|
|
expectHttpError(() => media.listPictures(db, route.id, 999999), 404);
|
|
expectHttpError(() => media.createPicture(db, route.id, poi.id, {}, null), 400);
|
|
const invalidPicture = uploadedFile('invalid-picture.bin', 'not image', {
|
|
detectedMime: 'text/plain',
|
|
detectedExtension: 'txt'
|
|
});
|
|
expectHttpError(() => media.createPicture(db, route.id, poi.id, {}, invalidPicture), 415);
|
|
assert.equal(fs.existsSync(invalidPicture.path), false);
|
|
|
|
const picture = media.createPicture(db, route.id, poi.id, { caption: 'Bild' }, uploadedFile('picture.png', PNG_FILE, {
|
|
detectedMime: 'image/png',
|
|
detectedExtension: 'png'
|
|
}));
|
|
assert.equal(picture.sequence, 0);
|
|
const secondPicture = media.createPicture(db, route.id, poi.id, { caption: 'Bild 2', sequence: 2 }, uploadedFile('picture-2.png', PNG_FILE, {
|
|
detectedMime: 'image/png',
|
|
detectedExtension: 'png'
|
|
}));
|
|
assert.equal(media.listPictures(db, route.id, poi.id).length, 2);
|
|
assert.ok(fs.existsSync(media.getPictureFile(db, route.id, poi.id, picture.id).absolutePath));
|
|
expectHttpError(() => media.getPicture(db, route.id, poi.id, 999999), 404);
|
|
expectHttpError(() => media.updatePicture(db, route.id, poi.id, picture.id, { sequence: -1 }, null), 400);
|
|
|
|
const metadataUpdate = media.updatePicture(db, route.id, poi.id, picture.id, {
|
|
caption: 'Neue Bildbeschreibung',
|
|
sequence: 1
|
|
}, null);
|
|
assert.equal(metadataUpdate.caption, 'Neue Bildbeschreibung');
|
|
|
|
const replacement = uploadedFile('replacement.webp', PNG_FILE, {
|
|
detectedMime: 'image/png',
|
|
detectedExtension: 'png'
|
|
});
|
|
const replacedPicture = media.updatePicture(db, route.id, poi.id, picture.id, {}, replacement);
|
|
assert.equal(replacedPicture.id, picture.id);
|
|
|
|
expectHttpError(() => media.getAudio(db, route.id, poi.id), 404);
|
|
expectHttpError(() => media.createAudio(db, route.id, poi.id, null), 400);
|
|
const invalidAudio = uploadedFile('invalid-audio.bin', 'bad', {
|
|
detectedMime: 'text/plain',
|
|
detectedExtension: 'txt'
|
|
});
|
|
expectHttpError(() => media.createAudio(db, route.id, poi.id, invalidAudio), 415);
|
|
assert.equal(fs.existsSync(invalidAudio.path), false);
|
|
|
|
const audio = media.createAudio(db, route.id, poi.id, uploadedFile('audio.mp3', MP3_FILE, {
|
|
detectedMime: 'audio/mpeg',
|
|
detectedExtension: 'mp3'
|
|
}));
|
|
assert.equal(audio.poiId, poi.id);
|
|
assert.ok(fs.existsSync(media.getAudioFile(db, route.id, poi.id).absolutePath));
|
|
expectHttpError(() => media.createAudio(db, route.id, poi.id, uploadedFile('duplicate.mp3', MP3_FILE, {
|
|
detectedMime: 'audio/mpeg',
|
|
detectedExtension: 'mp3'
|
|
})), 409);
|
|
expectHttpError(() => media.updateAudio(db, route.id, secondPoi.id, uploadedFile('missing.mp3', MP3_FILE, {
|
|
detectedMime: 'audio/mpeg',
|
|
detectedExtension: 'mp3'
|
|
})), 404);
|
|
|
|
const replacedAudio = media.updateAudio(db, route.id, poi.id, uploadedFile('replacement.mp3', MP3_FILE, {
|
|
detectedMime: 'audio/mpeg',
|
|
detectedExtension: 'mp3'
|
|
}));
|
|
assert.equal(replacedAudio.poiId, poi.id);
|
|
assert.equal(media.deleteAudio(db, route.id, poi.id).deleted, true);
|
|
expectHttpError(() => media.deleteAudio(db, route.id, poi.id), 404);
|
|
|
|
assert.equal(media.deletePicture(db, route.id, poi.id, secondPicture.id).deleted, true);
|
|
expectHttpError(() => media.deletePicture(db, route.id, poi.id, secondPicture.id), 404);
|
|
|
|
const poiPicture = media.createPicture(db, route.id, secondPoi.id, {}, uploadedFile('poi-picture.png', PNG_FILE, {
|
|
detectedMime: 'image/png',
|
|
detectedExtension: 'png'
|
|
}));
|
|
media.createAudio(db, route.id, secondPoi.id, uploadedFile('poi-audio.mp3', MP3_FILE, {
|
|
detectedMime: 'audio/mpeg',
|
|
detectedExtension: 'mp3'
|
|
}));
|
|
assert.equal(routes.deletePoi(db, route.id, secondPoi.id).deleted, true);
|
|
expectHttpError(() => routes.deletePoi(db, route.id, secondPoi.id), 404);
|
|
expectHttpError(() => media.getPicture(db, route.id, secondPoi.id, poiPicture.id), 404);
|
|
|
|
const deleted = routes.softDeleteRoute(db, route.id);
|
|
assert.equal(deleted.status, 'deleted');
|
|
assert.equal(routes.listRoutes(db, {}).some(item => item.id === route.id), false);
|
|
assert.equal(routes.listRoutes(db, { includeDeleted: true }).some(item => item.id === route.id), true);
|
|
expectHttpError(() => routes.getRoute(db, route.id), 404);
|
|
expectHttpError(() => routes.softDeleteRoute(db, route.id), 404);
|
|
const restored = routes.restoreRoute(db, route.id);
|
|
assert.equal(restored.status, 'active');
|
|
expectHttpError(() => routes.restoreRoute(db, route.id), 404);
|
|
|
|
const missingRouteDir = routes.createRoute(db, { name: 'Ohne Verzeichnis' }, uploadedFile('missing-dir.gpx', GPX));
|
|
fs.rmSync(storage.routeDirectory(missingRouteDir.id), { recursive: true, force: true });
|
|
expectHttpError(() => routes.softDeleteRoute(db, missingRouteDir.id), 409);
|
|
|
|
assert.equal(storage.removeStoredFile(null), false);
|
|
assert.equal(storage.removeStoredFile('active/routes/not-there.bin'), false);
|
|
expectHttpError(() => storage.removeStoredFile('../outside'), 500);
|
|
expectHttpError(() => storage.resolveStoredFile('trash/routes/file'), 404);
|
|
expectHttpError(() => storage.resolveStoredFile('active/../../outside'), 500);
|
|
expectHttpError(() => storage.resolveStoredFile('active/routes/no-file'), 404);
|
|
|
|
const orphanUpload = uploadedFile('orphan.bin', 'x');
|
|
storage.removeUpload(orphanUpload);
|
|
assert.equal(fs.existsSync(orphanUpload.path), false);
|
|
} finally {
|
|
db.close();
|
|
}
|
|
});
|
|
|
|
test('upload inspection handles JSON, GPX, binary validation and cleanup paths', async () => {
|
|
const noFileReq = { body: {} };
|
|
assert.equal(await invokeUpload(inspectUpload('picture', 'picture'), noFileReq), undefined);
|
|
|
|
const jsonPictureReq = {
|
|
body: {
|
|
picture: {
|
|
filename: ' strange name.png ',
|
|
dataUrl: `data:image/png;base64,${PNG_FILE.toString('base64')}`
|
|
}
|
|
}
|
|
};
|
|
assert.equal(await invokeUpload(inspectUpload('picture', 'picture', { allowJson: true }), jsonPictureReq), undefined);
|
|
assert.equal(jsonPictureReq.file.detectedMime, 'image/png');
|
|
assert.equal(jsonPictureReq.file.detectedExtension, 'png');
|
|
assert.equal(jsonPictureReq.body.picture, undefined);
|
|
storage.removeUpload(jsonPictureReq.file);
|
|
|
|
const jsonAudioReq = {
|
|
body: {
|
|
audio: {
|
|
filename: 'audio.bin',
|
|
base64: MP3_FILE.toString('base64').replace(/(.{40})/g, '$1\n')
|
|
}
|
|
}
|
|
};
|
|
assert.equal(await invokeUpload(inspectUpload('audio', 'audio', { allowJson: true }), jsonAudioReq), undefined);
|
|
assert.equal(jsonAudioReq.file.detectedMime, 'audio/mpeg');
|
|
storage.removeUpload(jsonAudioReq.file);
|
|
|
|
for (const descriptor of [
|
|
'not-object',
|
|
{ base64: '' },
|
|
{ base64: '!!!' },
|
|
{ dataUrl: 12 },
|
|
{ dataUrl: 'not-a-data-url' }
|
|
]) {
|
|
const req = { body: { picture: descriptor } };
|
|
const error = await invokeUpload(inspectUpload('picture', 'picture', { allowJson: true }), req);
|
|
assert.equal(error.status, 400);
|
|
assert.equal(req.file, undefined);
|
|
}
|
|
|
|
const unsupportedReq = {
|
|
body: { picture: { filename: 'text.txt', base64: Buffer.from('plain text').toString('base64') } }
|
|
};
|
|
const unsupported = await invokeUpload(inspectUpload('picture', 'picture', { allowJson: true }), unsupportedReq);
|
|
assert.equal(unsupported.status, 415);
|
|
assert.equal(unsupportedReq.file, undefined);
|
|
|
|
const validGpxReq = { body: {}, file: uploadedFile('route.xml', `\uFEFF${GPX}`) };
|
|
assert.equal(await invokeUpload(inspectUpload('gpx', 'gpx'), validGpxReq), undefined);
|
|
assert.equal(validGpxReq.file.detectedExtension, 'gpx');
|
|
storage.removeUpload(validGpxReq.file);
|
|
|
|
const namespacedGpx = '<?xml version="1.0"?><!-- route --><!DOCTYPE gpx><ns_1-foo.gpx:gpx></ns_1-foo.gpx:gpx>';
|
|
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',
|
|
'<!-- missing end',
|
|
'<?xml version="1.0"',
|
|
'<!DOCTYPE gpx',
|
|
'</gpx>',
|
|
'< gpx>'
|
|
]) {
|
|
const request = { body: {}, file: uploadedFile('malformed.xml', malformed) };
|
|
const error = await invokeUpload(inspectUpload('gpx', 'gpx'), request);
|
|
assert.equal(error.status, 415);
|
|
assert.equal(request.file, undefined);
|
|
}
|
|
|
|
const invalidGpxReq = { body: {}, file: uploadedFile('route.xml', '<xml/>') };
|
|
const invalidGpx = await invokeUpload(inspectUpload('gpx', 'gpx'), invalidGpxReq);
|
|
assert.equal(invalidGpx.status, 415);
|
|
assert.equal(invalidGpxReq.file, undefined);
|
|
|
|
const invalidUtf8Req = { body: {}, file: uploadedFile('route.gpx', Buffer.from([0xff, 0xfe, 0xfd])) };
|
|
const invalidUtf8 = await invokeUpload(inspectUpload('gpx', 'gpx'), invalidUtf8Req);
|
|
assert.equal(invalidUtf8.status, 415);
|
|
assert.equal(invalidUtf8Req.file, undefined);
|
|
});
|
|
|
|
test('HTTP error middleware preserves details, maps upload errors and delegates sent headers', () => {
|
|
const notFound = [];
|
|
notFoundHandler({}, {}, error => notFound.push(error));
|
|
assert.equal(notFound[0].status, 404);
|
|
|
|
const sent = [];
|
|
const response = {
|
|
headersSent: false,
|
|
statusCode: null,
|
|
status(value) {
|
|
this.statusCode = value;
|
|
return this;
|
|
},
|
|
json(value) {
|
|
sent.push(value);
|
|
return this;
|
|
}
|
|
};
|
|
errorHandler(new HttpError(422, 'Ungültig', { field: 'name' }), {}, response, () => {});
|
|
assert.equal(response.statusCode, 422);
|
|
assert.deepEqual(sent[0].details, { field: 'name' });
|
|
|
|
errorHandler(Object.assign(new Error('large'), { code: 'LIMIT_FILE_SIZE' }), {}, response, () => {});
|
|
assert.equal(response.statusCode, 413);
|
|
errorHandler(Object.assign(new Error('unexpected'), { code: 'LIMIT_UNEXPECTED_FILE' }), {}, response, () => {});
|
|
assert.equal(response.statusCode, 400);
|
|
|
|
const delegated = [];
|
|
errorHandler(new Error('already sent'), {}, { headersSent: true }, error => delegated.push(error));
|
|
assert.equal(delegated[0].message, 'already sent');
|
|
});
|
|
|
|
|
|
test('Express API router executes route, POI, file and restore handlers in-process', async t => {
|
|
const db = openDatabase(path.join(config.dataDir, 'router.sqlite'));
|
|
const app = express();
|
|
app.use(express.json({
|
|
limit: config.maxJsonBodyBytes,
|
|
type: ['application/json', 'application/*+json', 'text/json']
|
|
}));
|
|
app.use(express.urlencoded({ extended: true }));
|
|
app.use('/api', createApiRouter(db));
|
|
app.use(notFoundHandler);
|
|
app.use(errorHandler);
|
|
|
|
const server = await new Promise((resolve, reject) => {
|
|
const listener = app.listen(0, '127.0.0.1', () => resolve(listener));
|
|
listener.once('error', reject);
|
|
});
|
|
const address = server.address();
|
|
const baseUrl = `http://127.0.0.1:${address.port}`;
|
|
|
|
t.after(async () => {
|
|
await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
|
|
db.close();
|
|
});
|
|
|
|
async function json(url, options = {}, status = 200) {
|
|
const response = await fetch(`${baseUrl}${url}`, options);
|
|
const body = await response.json();
|
|
assert.equal(response.status, status, JSON.stringify(body));
|
|
return { response, body };
|
|
}
|
|
|
|
const health = await json('/api/health');
|
|
assert.equal(health.body.ok, true);
|
|
assert.match(health.body.socket, /:/);
|
|
|
|
const createForm = new FormData();
|
|
createForm.append('name', 'Router-Test');
|
|
createForm.append('description', 'API-Abdeckung');
|
|
createForm.append('gpx', new Blob([GPX], { type: 'text/plain' }), 'route.txt');
|
|
const created = await json('/api/routes', { method: 'POST', body: createForm }, 201);
|
|
const routeId = created.body.id;
|
|
assert.equal(created.response.headers.get('location'), `/api/routes/${routeId}`);
|
|
|
|
assert.equal((await json('/api/routes?lat=invalid&lon=&radiusKm=invalid')).body.routes.length, 1);
|
|
assert.equal((await json(`/api/routes/${routeId}`)).body.id, routeId);
|
|
assert.equal((await fetch(`${baseUrl}/api/routes/${routeId}/gpx`)).status, 200);
|
|
assert.equal((await json(`/api/routes/${routeId}/pois`)).body.pois.length, 0);
|
|
|
|
const updateForm = new FormData();
|
|
updateForm.append('name', 'Router-Test aktualisiert');
|
|
const updated = await json(`/api/routes/${routeId}`, { method: 'PUT', body: updateForm });
|
|
assert.equal(updated.body.name, 'Router-Test aktualisiert');
|
|
|
|
const appendForm = new FormData();
|
|
appendForm.append('gpx', new Blob([GPX_APPEND], { type: 'application/octet-stream' }), 'append.bin');
|
|
assert.equal((await json(`/api/routes/${routeId}/append`, { method: 'POST', body: appendForm })).body.pointCount, 3);
|
|
|
|
const poiCreated = await json(`/api/routes/${routeId}/pois`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ title: 'Router-POI', lat: 50, lon: 10, triggerRadiusM: 40, sequence: 0 })
|
|
}, 201);
|
|
const poiId = poiCreated.body.id;
|
|
assert.equal(poiCreated.response.headers.get('location'), `/api/routes/${routeId}/pois/${poiId}`);
|
|
assert.equal((await json(`/api/routes/${routeId}/pois/${poiId}`)).body.title, 'Router-POI');
|
|
assert.equal((await json(`/api/routes/${routeId}/pois/${poiId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ title: 'Router-POI neu' })
|
|
})).body.title, 'Router-POI neu');
|
|
|
|
const pictureForm = new FormData();
|
|
pictureForm.append('caption', 'Router-Bild');
|
|
pictureForm.append('picture', new Blob([PNG_FILE], { type: 'text/plain' }), 'picture.txt');
|
|
const pictureCreated = await json(`/api/routes/${routeId}/pois/${poiId}/pictures`, {
|
|
method: 'POST', body: pictureForm
|
|
}, 201);
|
|
const pictureId = pictureCreated.body.id;
|
|
assert.equal((await json(`/api/routes/${routeId}/pois/${poiId}/pictures`)).body.pictures.length, 1);
|
|
assert.equal((await json(`/api/routes/${routeId}/pois/${poiId}/pictures/${pictureId}?metadata=true`)).body.id, pictureId);
|
|
assert.equal((await fetch(`${baseUrl}/api/routes/${routeId}/pois/${poiId}/pictures/${pictureId}`)).status, 200);
|
|
assert.equal((await json(`/api/routes/${routeId}/pois/${poiId}/pictures/${pictureId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ caption: 'Router-Bild neu' })
|
|
})).body.caption, 'Router-Bild neu');
|
|
|
|
const audioForm = new FormData();
|
|
audioForm.append('audio', new Blob([MP3_FILE], { type: 'text/plain' }), 'audio.txt');
|
|
const audioCreated = await json(`/api/routes/${routeId}/pois/${poiId}/audio`, {
|
|
method: 'POST', body: audioForm
|
|
}, 201);
|
|
assert.equal(audioCreated.response.headers.get('location'), `/api/routes/${routeId}/pois/${poiId}/audio`);
|
|
assert.equal((await json(`/api/routes/${routeId}/pois/${poiId}/audio?metadata=true`)).body.poiId, poiId);
|
|
assert.equal((await fetch(`${baseUrl}/api/routes/${routeId}/pois/${poiId}/audio`)).status, 200);
|
|
|
|
const replaceAudio = {
|
|
audio: {
|
|
filename: 'replacement.mp3',
|
|
base64: MP3_FILE.toString('base64')
|
|
}
|
|
};
|
|
assert.equal((await json(`/api/routes/${routeId}/pois/${poiId}/audio`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(replaceAudio)
|
|
})).body.poiId, poiId);
|
|
|
|
assert.equal((await json(`/api/routes/${routeId}/pois/${poiId}/audio`, { method: 'DELETE' })).body.deleted, true);
|
|
assert.equal((await json(`/api/routes/${routeId}/pois/${poiId}/pictures/${pictureId}`, { method: 'DELETE' })).body.deleted, true);
|
|
assert.equal((await json(`/api/routes/${routeId}/pois/${poiId}`, { method: 'DELETE' })).body.deleted, true);
|
|
|
|
const deleted = await json(`/api/routes/${routeId}`, { method: 'DELETE' });
|
|
assert.equal(deleted.body.softDeleted, true);
|
|
const restored = await json(`/api/routes/${routeId}/restore`, { method: 'POST' });
|
|
assert.equal(restored.body.restored, true);
|
|
|
|
await json('/api/not-found', {}, 404);
|
|
});
|