/pictures und /audio endpoints moved under /poi

This commit is contained in:
Florian Zumpe
2026-06-17 01:14:19 +02:00
parent 833b5ad3a4
commit 5c85a0194b
9 changed files with 225 additions and 267 deletions
+20 -18
View File
@@ -26,16 +26,15 @@ test('complete REST API documentation is kept outside the README', async () => {
'POST /api/routes/:id/append',
'POST /api/routes/:id/pois',
'PUT /api/pois/:id',
'GET /api/routes/:id/pictures',
'GET /api/routes/:id/pictures/:pictureId',
'POST /api/routes/:id/pictures',
'PUT /api/routes/:id/pictures/:pictureId',
'DELETE /api/routes/:id/pictures/:pictureId',
'GET /api/routes/:id/audio',
'GET /api/routes/:id/audio/:poiId',
'POST /api/routes/:id/audio',
'PUT /api/routes/:id/audio/:poiId',
'DELETE /api/routes/:id/audio/:poiId',
'GET /api/routes/:routeId/pois/:poiId/pictures',
'GET /api/routes/:routeId/pois/:poiId/pictures/:pictureId',
'POST /api/routes/:routeId/pois/:poiId/pictures',
'PUT /api/routes/:routeId/pois/:poiId/pictures/:pictureId',
'DELETE /api/routes/:routeId/pois/:poiId/pictures/:pictureId',
'GET /api/routes/:routeId/pois/:poiId/audio',
'POST /api/routes/:routeId/pois/:poiId/audio',
'PUT /api/routes/:routeId/pois/:poiId/audio',
'DELETE /api/routes/:routeId/pois/:poiId/audio',
'DELETE /api/routes/:id',
'POST /api/routes/:id/restore'
]) {
@@ -51,21 +50,24 @@ test('complete REST API documentation is kept outside the README', async () => {
}
});
test('POI media uploads use dedicated single-resource endpoints', async () => {
test('POI media uploads use POI-scoped single-resource endpoints', async () => {
const router = await read('src/routes/api.js');
const routesService = await read('src/services/routes-service.js');
const mediaService = await read('src/services/media-service.js');
assert.match(router, /post\('\/routes\/:id\/pictures', upload\.single\('picture'\)/);
assert.match(router, /put\('\/routes\/:id\/pictures\/:pictureId', upload\.single\('picture'\)/);
assert.match(router, /delete\('\/routes\/:id\/pictures\/:pictureId'/);
assert.match(router, /post\('\/routes\/:id\/audio', upload\.single\('audio'\)/);
assert.match(router, /put\('\/routes\/:id\/audio\/:poiId', upload\.single\('audio'\)/);
assert.match(router, /delete\('\/routes\/:id\/audio\/:poiId'/);
assert.match(router, /post\('\/routes\/:id\/pois', upload\.none\(\)/);
assert.match(router, /post\('\/routes\/:routeId\/pois\/:poiId\/pictures', upload\.single\('picture'\)/);
assert.match(router, /put\('\/routes\/:routeId\/pois\/:poiId\/pictures\/:pictureId', upload\.single\('picture'\)/);
assert.match(router, /delete\('\/routes\/:routeId\/pois\/:poiId\/pictures\/:pictureId'/);
assert.match(router, /get\('\/routes\/:routeId\/pois\/:poiId\/audio'/);
assert.match(router, /post\('\/routes\/:routeId\/pois\/:poiId\/audio', upload\.single\('audio'\)/);
assert.match(router, /put\('\/routes\/:routeId\/pois\/:poiId\/audio', upload\.single\('audio'\)/);
assert.match(router, /delete\('\/routes\/:routeId\/pois\/:poiId\/audio'/);
assert.doesNotMatch(router, /\/routes\/:id\/(?:pictures|audio)/);
assert.match(router, /post\('\/routes\/:id\/pois', upload\.none\(\),/);
assert.doesNotMatch(routesService, /files\.images|files\.audio/);
assert.match(mediaService, /caption/);
assert.match(mediaService, /removeStoredFile/);
assert.doesNotMatch(mediaService, /fields\.poiId/);
assert.match(router, /get\('\/routes\/:id\/gpx'/);
assert.match(router, /req\.query\.metadata === 'true'/);
assert.doesNotMatch(router, /['"`]\/media\//);
+40 -20
View File
@@ -102,32 +102,50 @@ test('pictures and audio are managed as individual REST resources', { timeout: 3
})
}, 201);
const { body: otherPoi } = await requestJson(`${baseUrl}/api/routes/${route.id}/pois`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: 'Andere Teststation',
lat: route.start.lat + 0.0001,
lon: route.start.lon + 0.0001,
triggerRadiusM: 40,
sequence: 1
})
}, 201);
const pictureForm = new FormData();
pictureForm.append('poiId', String(poi.id));
pictureForm.append('poiId', String(otherPoi.id));
pictureForm.append('caption', 'Erste Bildbeschreibung');
pictureForm.append('sequence', '0');
pictureForm.append('picture', new Blob(['picture-one'], { type: 'image/png' }), 'bild.png');
const { response: pictureResponse, body: picture } = await requestJson(
`${baseUrl}/api/routes/${route.id}/pictures`,
`${baseUrl}/api/routes/${route.id}/pois/${poi.id}/pictures`,
{ method: 'POST', body: pictureForm },
201
);
assert.equal(pictureResponse.headers.get('location'), `/api/routes/${route.id}/pictures/${picture.id}`);
assert.equal(pictureResponse.headers.get('location'), `/api/routes/${route.id}/pois/${poi.id}/pictures/${picture.id}`);
assert.equal(picture.poiId, poi.id);
assert.equal(picture.caption, 'Erste Bildbeschreibung');
assert.equal(picture.url, `/api/routes/${route.id}/pictures/${picture.id}`);
assert.equal(picture.url, `/api/routes/${route.id}/pois/${poi.id}/pictures/${picture.id}`);
const { response: pictureFileResponse, body: pictureFile } = await requestBuffer(`${baseUrl}${picture.url}`);
assert.match(pictureFileResponse.headers.get('content-type') || '', /image\/png/);
assert.equal(pictureFile.toString(), 'picture-one');
const { body: pictureMetadata } = await requestJson(`${baseUrl}${picture.url}?metadata=true`);
assert.equal(pictureMetadata.caption, 'Erste Bildbeschreibung');
assert.equal(pictureMetadata.poiId, poi.id);
await requestJson(
`${baseUrl}/api/routes/${route.id}/pois/${otherPoi.id}/pictures/${picture.id}?metadata=true`,
{},
404
);
const { body: pictureList } = await requestJson(`${baseUrl}/api/routes/${route.id}/pictures?poiId=${poi.id}`);
const { body: pictureList } = await requestJson(`${baseUrl}/api/routes/${route.id}/pois/${poi.id}/pictures`);
assert.equal(pictureList.pictures.length, 1);
assert.equal(pictureList.pictures[0].id, picture.id);
const { body: updatedPicture } = await requestJson(
`${baseUrl}/api/routes/${route.id}/pictures/${picture.id}`,
`${baseUrl}/api/routes/${route.id}/pois/${poi.id}/pictures/${picture.id}`,
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
@@ -138,16 +156,15 @@ test('pictures and audio are managed as individual REST resources', { timeout: 3
assert.equal(updatedPicture.sequence, 2);
const audioForm = new FormData();
audioForm.append('poiId', String(poi.id));
audioForm.append('audio', new Blob(['audio-one'], { type: 'audio/mpeg' }), 'ansage.mp3');
const { response: audioResponse, body: audio } = await requestJson(
`${baseUrl}/api/routes/${route.id}/audio`,
`${baseUrl}/api/routes/${route.id}/pois/${poi.id}/audio`,
{ method: 'POST', body: audioForm },
201
);
assert.equal(audioResponse.headers.get('location'), `/api/routes/${route.id}/audio/${poi.id}`);
assert.equal(audioResponse.headers.get('location'), `/api/routes/${route.id}/pois/${poi.id}/audio`);
assert.equal(audio.poiId, poi.id);
assert.equal(audio.url, `/api/routes/${route.id}/audio/${poi.id}`);
assert.equal(audio.url, `/api/routes/${route.id}/pois/${poi.id}/audio`);
const { response: audioFileResponse, body: audioFile } = await requestBuffer(`${baseUrl}${audio.url}`);
assert.match(audioFileResponse.headers.get('content-type') || '', /audio\/mpeg/);
assert.equal(audioFile.toString(), 'audio-one');
@@ -155,9 +172,8 @@ test('pictures and audio are managed as individual REST resources', { timeout: 3
assert.equal(audioMetadata.poiId, poi.id);
const duplicateAudio = new FormData();
duplicateAudio.append('poiId', String(poi.id));
duplicateAudio.append('audio', new Blob(['duplicate'], { type: 'audio/mpeg' }), 'doppelt.mp3');
await requestJson(`${baseUrl}/api/routes/${route.id}/audio`, {
await requestJson(`${baseUrl}/api/routes/${route.id}/pois/${poi.id}/audio`, {
method: 'POST',
body: duplicateAudio
}, 409);
@@ -165,10 +181,10 @@ test('pictures and audio are managed as individual REST resources', { timeout: 3
const replacementAudio = new FormData();
replacementAudio.append('audio', new Blob(['audio-two'], { type: 'audio/ogg' }), 'ansage-neu.ogg');
const { body: replacedAudio } = await requestJson(
`${baseUrl}/api/routes/${route.id}/audio/${poi.id}`,
`${baseUrl}/api/routes/${route.id}/pois/${poi.id}/audio`,
{ method: 'PUT', body: replacementAudio }
);
assert.equal(replacedAudio.url, `/api/routes/${route.id}/audio/${poi.id}`);
assert.equal(replacedAudio.url, `/api/routes/${route.id}/pois/${poi.id}/audio`);
const { response: replacedAudioResponse, body: replacedAudioFile } = await requestBuffer(`${baseUrl}${replacedAudio.url}`);
assert.match(replacedAudioResponse.headers.get('content-type') || '', /audio\/ogg/);
assert.equal(replacedAudioFile.toString(), 'audio-two');
@@ -176,24 +192,28 @@ test('pictures and audio are managed as individual REST resources', { timeout: 3
const { body: routeWithMedia } = await requestJson(`${baseUrl}/api/routes/${route.id}`);
assert.equal(routeWithMedia.pois[0].images[0].caption, 'Aktualisierte Bildbeschreibung');
assert.equal(routeWithMedia.gpxUrl, `/api/routes/${route.id}/gpx`);
assert.equal(routeWithMedia.pois[0].audioUrl, `/api/routes/${route.id}/audio/${poi.id}`);
assert.equal(routeWithMedia.pois[0].images[0].url, `/api/routes/${route.id}/pictures/${picture.id}`);
assert.equal(routeWithMedia.pois[0].audioUrl, `/api/routes/${route.id}/pois/${poi.id}/audio`);
assert.equal(routeWithMedia.pois[0].images[0].url, `/api/routes/${route.id}/pois/${poi.id}/pictures/${picture.id}`);
assert.doesNotMatch(JSON.stringify(routeWithMedia), /\/media\//);
const legacyMediaResponse = await fetch(`${baseUrl}/media/routes/${route.id}/route.gpx`);
assert.equal(legacyMediaResponse.status, 404);
const legacyPictureEndpoint = await fetch(`${baseUrl}/api/routes/${route.id}/pictures`);
assert.equal(legacyPictureEndpoint.status, 404);
const legacyAudioEndpoint = await fetch(`${baseUrl}/api/routes/${route.id}/audio`);
assert.equal(legacyAudioEndpoint.status, 404);
const { body: deletedAudio } = await requestJson(
`${baseUrl}/api/routes/${route.id}/audio/${poi.id}`,
`${baseUrl}/api/routes/${route.id}/pois/${poi.id}/audio`,
{ method: 'DELETE' }
);
assert.equal(deletedAudio.deleted, true);
const { body: deletedPicture } = await requestJson(
`${baseUrl}/api/routes/${route.id}/pictures/${picture.id}`,
`${baseUrl}/api/routes/${route.id}/pois/${poi.id}/pictures/${picture.id}`,
{ method: 'DELETE' }
);
assert.equal(deletedPicture.deleted, true);
await requestJson(`${baseUrl}/api/routes/${route.id}/pictures/${picture.id}`, {}, 404);
await requestJson(`${baseUrl}/api/routes/${route.id}/audio/${poi.id}`, {}, 404);
await requestJson(`${baseUrl}/api/routes/${route.id}/pois/${poi.id}/pictures/${picture.id}`, {}, 404);
await requestJson(`${baseUrl}/api/routes/${route.id}/pois/${poi.id}/audio`, {}, 404);
});