Lo que queda después de la revisión de código; se aplica sobre busqueda-agente.diff. La consulta usa un parámetro, el texto vacío se rechaza y las pruebas revisan el resultado. diff --git a/src/http/handlers.ts b/src/http/handlers.ts index 521f3ab..6712b25 100644 --- a/src/http/handlers.ts +++ b/src/http/handlers.ts @@ -155,7 +155,8 @@ export function memberBookings(deps: AppDependencies): Handler { // GET /api/bookings/search?team=texto export function searchBookings(deps: AppDependencies): Handler { return ({ res, url }) => { - const team = url.searchParams.get('team') ?? ''; + const team = url.searchParams.get('team')?.trim(); + if (!team) throw new HttpError(400, 'VALIDATION_ERROR', 'Indiquen el texto a buscar (?team=Pumas).'); sendJson(res, 200, deps.service.bookingsByTeam(team).map(toBookingJson)); }; } diff --git a/src/store/booking-repository.ts b/src/store/booking-repository.ts index 7be3d13..b35f057 100644 --- a/src/store/booking-repository.ts +++ b/src/store/booking-repository.ts @@ -47,8 +47,8 @@ export class BookingRepository { searchByTeam(team: string): BookingRow[] { return this.#db - .prepare(`SELECT * FROM bookings WHERE team_name LIKE '%${team}%' ORDER BY date, start_hour`) - .all() as BookingRow[]; + .prepare("SELECT * FROM bookings WHERE team_name LIKE '%' || ? || '%' ORDER BY date, start_hour") + .all(team) as BookingRow[]; } insert(row: NewBookingRow): number { diff --git a/test/search.test.ts b/test/search.test.ts index f908688..4738df6 100644 --- a/test/search.test.ts +++ b/test/search.test.ts @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import { after, before, test } from 'node:test'; -import { getJson, startTestServer, type TestServer } from './helpers.ts'; +import { getJson, postJson, startTestServer, type TestServer } from './helpers.ts'; let server: TestServer; @@ -12,7 +12,35 @@ after(async () => { await server.close(); }); -test('busca reservas por equipo', async () => { - const { status } = await getJson(`${server.url}/api/bookings/search?team=Pumas`); +const booking = { courtId: 1, memberId: 1, date: '2026-03-10', startHour: 8, hours: 1 }; + +async function search(team: string) { + return getJson(`${server.url}/api/bookings/search?team=${encodeURIComponent(team)}`); +} + +test('encuentra solo las reservas de los equipos que coinciden', async () => { + await postJson(`${server.url}/api/bookings`, { ...booking, teamName: 'Los Pumas de Comas' }); + await postJson(`${server.url}/api/bookings`, { ...booking, startHour: 9, teamName: "O'Higgins de Ate" }); + + const { status, body } = await search('Pumas'); + assert.equal(status, 200); + assert.deepEqual(body.map((b: { teamName: string }) => b.teamName), ['Los Pumas de Comas']); +}); + +test('un apóstrofe en el texto es un carácter más', async () => { + const { status, body } = await search("O'Higgins"); assert.equal(status, 200); + assert.deepEqual(body.map((b: { teamName: string }) => b.teamName), ["O'Higgins de Ate"]); +}); + +test('el texto no puede cambiar la consulta', async () => { + const { status, body } = await search("' OR '1'='1"); + assert.equal(status, 200); + assert.deepEqual(body, []); +}); + +test('pide el texto a buscar', async () => { + const { status, body } = await search(''); + assert.equal(status, 400); + assert.equal(body.error.code, 'VALIDATION_ERROR'); });