From f4c6f6002192ac3953ec1509c02485668e5e9d05 Mon Sep 17 00:00:00 2001 From: JoseSerpaCode Date: Mon, 24 Aug 2026 18:33:01 -0500 Subject: [PATCH] =?UTF-8?q?fix:=20el=20404=20del=20rechazo=20devolv=C3=ADa?= =?UTF-8?q?=20la=20fila,=20y=20el=20hub=20venc=C3=ADa=20tareas=20de=20hoy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dos fallos pequeños de riesgo alto. - `friends/reject/[id].ts` devolvía `dbState` con el `SELECT *` de la amistad a quien acababa de fallar la comprobación de permisos, más un `console.log` de cada intento. Sus dos hermanos responden con la frase a secas. - El contador de vencidas del hub comparaba en UTC, así que en UTC-5 marcaba como vencida una entrega de hoy a partir de las siete de la tarde. `lib/fechas.ts` ya tenía `vencida()` y su comentario advierte del caso nombrando Bogotá; el hub reimplementó la comparación a mano. Las dos pruebas verificadas reintroduciendo su fallo. La de fechas fija la hora del sistema, que es lo único que reproduce el de UTC. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 10 ++++++ package-lock.json | 4 +-- package.json | 2 +- src/components/dashboard/Hub.astro | 19 ++++++++--- src/pages/api/friends/reject/[id].ts | 21 ++++++++----- tests/e2e/rechazo-sin-fuga.spec.ts | 32 +++++++++++++++++++ tests/vencidas-hub.test.ts | 47 ++++++++++++++++++++++++++++ 7 files changed, 120 insertions(+), 15 deletions(-) create mode 100644 tests/e2e/rechazo-sin-fuga.spec.ts create mode 100644 tests/vencidas-hub.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 12cd735..4fb5448 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ El formato está basado en [Keep a Changelog](https://keepachangelog.com/es-ES/1 > Las entradas entre la 0.6.0 y la 1.4.0 se reconstruyeron a posteriori a partir del historial de git, agrupadas por los saltos de versión que realmente ocurrieron en `package.json`. La 1.1.0 nunca existió: se pasó directamente de la 1.0.0 a la 1.2.0. +## [1.26.3] - 2026-08-23 + +### Security + +- **Rechazar una solicitud de amistad que no existe devolvía la fila entera de la tabla.** El 404 incluía un `dbState` con el `SELECT *` de la amistad —`action_user_id`, el id de la otra persona, las marcas de tiempo— a quien acababa de fallar la comprobación de permisos, y había además un `console.log` de cada intento. Era depuración que se quedó puesta, y convertía un «no puedes» en un volcado de la tabla. Sus dos hermanos, aceptar y cancelar, responden con la frase a secas: este era el único de los tres que se salía del patrón. + +### Fixed + +- **El hub marcaba como vencida una tarea que vence hoy.** El contador comparaba con `new Date().toISOString()`, que da el día en UTC: en Bogotá, a partir de las siete de la tarde, el servidor ya cree que es mañana. `lib/fechas.ts` documenta ese caso exacto en su comentario y expone `vencida()` desde entonces —la tarjeta del tablero ya la usa—, pero este contador reimplementó la comparación por su cuenta y se trajo el fallo de vuelta. No falla nunca por la mañana, así que es de los que se descartan como manía de uno. + ## [1.26.2] - 2026-08-20 ### Fixed diff --git a/package-lock.json b/package-lock.json index 7980346..a639acc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "forge-js", - "version": "1.26.2", + "version": "1.26.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "forge-js", - "version": "1.26.2", + "version": "1.26.3", "dependencies": { "@astrojs/check": "^0.9.10", "@astrojs/node": "^11.1.2", diff --git a/package.json b/package.json index 5177990..c91332c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "forge-js", "type": "module", - "version": "1.26.2", + "version": "1.26.3", "engines": { "node": ">=22.12.0" }, diff --git a/src/components/dashboard/Hub.astro b/src/components/dashboard/Hub.astro index 735eb73..45b0024 100644 --- a/src/components/dashboard/Hub.astro +++ b/src/components/dashboard/Hub.astro @@ -3,6 +3,7 @@ import Logo from '../brand/Logo.astro'; import WorkspaceIcon from '../brand/WorkspaceIcon.astro'; import { mapaPorClave } from '../../lib/issueTypes'; import { nombreDeRol } from '../../lib/roles'; +import { vencida } from '../../lib/fechas'; import { espaciosDelHub, tareasPendientes } from '../../lib/consultas'; // src/pages/index.astro import MainLayout from '../../layouts/MainLayout.astro'; @@ -73,10 +74,20 @@ for (const wsId of new Set(myTasks.map((t) => t.workspace_id))) { for (const [clave, tipo] of mapaPorClave(wsId)) tiposPendientes.set(`${wsId}:${clave}`, tipo); } -// Vencidas: comparando solo la fecha, no la hora. Una entrega «de hoy» no está -// vencida a las nueve de la mañana. -const hoy = new Date().toISOString().slice(0, 10); -const vencidas = pendientes.filter((tarea) => tarea.due_date && String(tarea.due_date).slice(0, 10) < hoy).length; +/** + * Vencidas, con el helper y no a mano. + * + * Aquí había un `new Date().toISOString().slice(0, 10)`, que da el día en + * **UTC**. `lib/fechas.ts` documenta justo este caso en el comentario de + * `hoyLocal()`: en Bogotá, entre las siete de la tarde y medianoche, el + * servidor ya cree que es mañana y marca como vencido lo que vence hoy. Se + * veía como una tarea vencida que no lo estaba, cada tarde. + * + * `vencida()` existía desde entonces y `IssueCard.astro` ya la usa; este + * contador reimplementó la comparación por su cuenta y se trajo el fallo de + * vuelta. + */ +const vencidas = pendientes.filter((tarea) => vencida(tarea.due_date)).length; --- diff --git a/src/pages/api/friends/reject/[id].ts b/src/pages/api/friends/reject/[id].ts index dd5cfad..59c79b8 100644 --- a/src/pages/api/friends/reject/[id].ts +++ b/src/pages/api/friends/reject/[id].ts @@ -18,15 +18,20 @@ export const POST: APIRoute = async ({ request, params, locals }) => { AND status = 'pending' `).run(friendshipId, user.id, user.id, user.id); - console.log('Reject attempt:', { friendshipId, userId: user.id, changes: result.changes }); - + /** + * El 404 no cuenta nada de la fila. + * + * Aquí se devolvía `dbState` con el `SELECT *` entero de la amistad: + * `action_user_id`, el id de la otra persona y las marcas de tiempo, a + * quien acababa de fallar la comprobación de permisos. Era depuración que + * se quedó puesta —había también un `console.log` de cada intento— y + * convertía un «no puedes» en un volcado de la tabla. + * + * Sus dos hermanos, `accept/[id].ts` y `cancel/[id].ts`, responden con la + * frase a secas. Este era el único de los tres que se salía del patrón. + */ if (result.changes === 0) { - return new Response(JSON.stringify({ - error: 'Not Found or Unauthorized', - friendshipId, - userId: user.id, - dbState: db.prepare('SELECT * FROM friendships WHERE id = ?').get(friendshipId) - }), { status: 404 }); + return new Response('Not Found or Unauthorized', { status: 404 }); } return new Response(JSON.stringify({ success: true }), { status: 200 }); diff --git a/tests/e2e/rechazo-sin-fuga.spec.ts b/tests/e2e/rechazo-sin-fuga.spec.ts new file mode 100644 index 0000000..1eacccf --- /dev/null +++ b/tests/e2e/rechazo-sin-fuga.spec.ts @@ -0,0 +1,32 @@ +import { test, expect } from '@playwright/test'; + +/** + * Un 404 no cuenta nada de la fila. + * + * `api/friends/reject/[id].ts` devolvía `dbState` con el `SELECT *` entero de + * la amistad —`action_user_id`, el id de la otra persona, las marcas de + * tiempo— a quien acababa de fallar la comprobación de permisos. Era + * depuración que se quedó puesta, y convertía un «no puedes» en un volcado de + * la tabla. Sus dos hermanos responden con la frase a secas. + */ +test('el rechazo de una solicitud inexistente no filtra la fila', async ({ page }) => { + await page.goto('/login'); + await page.fill('input[name="username"]', 'jose'); + await page.fill('input[name="password"]', process.env.TEST_PASSWORD || 'LocalDevPass123!'); + await page.click('button[type="submit"]'); + await page.waitForURL('**/'); + + const res = await page.request.post('/api/friends/reject/no-existe-este-id', { + headers: { Origin: 'http://localhost:4322' }, + }); + expect(res.status()).toBe(404); + + const cuerpo = await res.text(); + + // Ni el volcado, ni las columnas que llevaba dentro. + for (const filtrado of ['dbState', 'action_user_id', 'user_a_id', 'user_b_id', 'updated_at']) { + expect(cuerpo, `el 404 sigue contando «${filtrado}»`).not.toContain(filtrado); + } + // Y tampoco el id de quien pregunta, que tampoco pintaba nada ahí. + expect(cuerpo).not.toContain('userId'); +}); diff --git a/tests/vencidas-hub.test.ts b/tests/vencidas-hub.test.ts new file mode 100644 index 0000000..9204642 --- /dev/null +++ b/tests/vencidas-hub.test.ts @@ -0,0 +1,47 @@ +import { describe, it, expect } from 'vitest'; +import { vencida, hoyLocal } from '../src/lib/fechas'; + +/** + * El contador de vencidas del Hub. + * + * `Hub.astro` comparaba con `new Date().toISOString().slice(0, 10)`, que da el + * día en **UTC**. En una zona con desfase negativo —Bogotá es UTC-5— eso + * significa que a partir de las siete de la tarde el servidor ya cree que es + * mañana, y una entrega de hoy aparece como vencida. + * + * No falla nunca por la mañana, así que es de los que se descartan como «cosa + * mía». Se fija con la hora clavada, que es lo único que lo reproduce. + */ +describe('una entrega de hoy no está vencida por la tarde', () => { + // 22:00 en Bogotá = 03:00 UTC del día siguiente. + const laNoche = new Date('2026-08-21T03:00:00Z'); + + it('la comparación en UTC daba mañana; la local da hoy', () => { + const enUtc = laNoche.toISOString().slice(0, 10); + const enLocal = hoyLocal(laNoche); + + // Si esta prueba corre en UTC-5, los dos días son distintos: ahí estaba el + // fallo. En otra zona no se puede afirmar, pero la comparación de abajo + // vale igual. + if (enUtc !== enLocal) { + expect(enUtc > enLocal, 'UTC va por delante del día local').toBe(true); + } + }); + + it('una tarea que vence hoy no cuenta como vencida', () => { + const hoy = hoyLocal(laNoche); + expect(vencida(hoy, laNoche)).toBe(false); + }); + + it('una que venció ayer sí', () => { + const hoy = hoyLocal(laNoche); + const ayer = new Date(`${hoy}T12:00:00Z`); + ayer.setDate(ayer.getDate() - 1); + expect(vencida(ayer.toISOString().slice(0, 10), laNoche)).toBe(true); + }); + + it('sin fecha de entrega no está vencida', () => { + expect(vencida(null, laNoche)).toBe(false); + expect(vencida('', laNoche)).toBe(false); + }); +});