diff --git a/public/app.js b/public/app.js index 7963b5f..b1cadd7 100644 --- a/public/app.js +++ b/public/app.js @@ -54,7 +54,7 @@ async function loadSlots() { } async function loadMyBookings() { - const bookings = await api(`/api/members/${memberSelect.value}/bookings`); + const { bookings } = await api(`/api/v2/members/${memberSelect.value}/bookings`); myBookings.innerHTML = bookings .map((b) => { const text = `${b.date} · ${b.startHour}:00 a ${b.startHour + b.hours}:00 · ${b.teamName} · ${soles(b.priceCents)}`; diff --git a/src/http/handlers.ts b/src/http/handlers.ts index ec3043e..e338202 100644 --- a/src/http/handlers.ts +++ b/src/http/handlers.ts @@ -153,9 +153,24 @@ function bookingsOfMember(deps: AppDependencies, id: string): Booking[] { return deps.service.bookingsForMember(member.id).map(toBookingJson); } +// Apagones anunciados: durante estas ventanas la v1 ya responde como responderá +// después del fin. Así aparece quien no sabía que dependía de ella. +export const MEMBER_BOOKINGS_V1_BROWNOUTS = [ + { from: '2027-09-01T15:00:00Z', to: '2027-09-01T16:00:00Z' }, + { from: '2027-10-05T15:00:00Z', to: '2027-10-05T21:00:00Z' }, + { from: '2027-11-02T13:00:00Z', to: '2027-11-03T13:00:00Z' }, +]; + +export function isBrownout(instant: Date): boolean { + return MEMBER_BOOKINGS_V1_BROWNOUTS.some(({ from, to }) => instant >= new Date(from) && instant < new Date(to)); +} + // GET /api/members/:id/bookings: arreglo suelto, deprecado. export function memberBookings(deps: AppDependencies): Handler { return ({ res, params }) => { + if (isBrownout(deps.clock.now())) { + throw new HttpError(410, 'ENDPOINT_REMOVED', `Esta ruta deja de existir: usen /api/v2/members/${params.id}/bookings, que devuelve { bookings }.`); + } res.setHeader('Deprecation', `@${MEMBER_BOOKINGS_V1_DEPRECATED_AT}`); res.setHeader('Sunset', MEMBER_BOOKINGS_V1_SUNSET); res.setHeader('Link', `; rel="successor-version"`); diff --git a/test/v2-migrado.test.ts b/test/v2-migrado.test.ts new file mode 100644 index 0000000..13e6bbd --- /dev/null +++ b/test/v2-migrado.test.ts @@ -0,0 +1,26 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { test } from 'node:test'; +import { isBrownout } from '../src/http/handlers.ts'; +import { startTestServer } from './helpers.ts'; + +test('la página ya no usa la ruta deprecada', () => { + const page = readFileSync('public/app.js', 'utf8'); + assert.doesNotMatch(page, /`\/api\/members\/[^`]*\/bookings`/); + assert.match(page, /\/api\/v2\/members\//); +}); + +test('durante un apagón la v1 responde 410 con lo que hay que hacer; la v2 sigue', async () => { + const server = await startTestServer({ now: '2027-09-01T15:30:00Z' }); + const old = await fetch(`${server.url}/api/members/2/bookings`); + assert.equal(old.status, 410); + assert.match(((await old.json()) as { error: { message: string } }).error.message, /\/api\/v2\/members\/2\/bookings/); + assert.equal((await fetch(`${server.url}/api/v2/members/2/bookings`)).status, 200); + await server.close(); +}); + +test('un apagón empieza y termina a la hora anunciada', () => { + assert.equal(isBrownout(new Date('2027-09-01T14:59:59Z')), false); + assert.equal(isBrownout(new Date('2027-09-01T15:00:00Z')), true); + assert.equal(isBrownout(new Date('2027-09-01T16:00:00Z')), false); +});