Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,63 @@ jobs:
GFRAME_TEST_NGINX: /usr/sbin/nginx
GFRAME_TEST_PHP_FPM: /usr/sbin/php-fpm8.3

infrastructure-84:
name: MySQL, HTTP y Redis reales con PHP 8.4
runs-on: ubuntu-24.04
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: gframe_test
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -h 127.0.0.1 -pgframe_test"
--health-interval=10s --health-timeout=5s --health-retries=10
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd="redis-cli ping"
--health-interval=5s --health-timeout=3s --health-retries=10
env:
GFRAME_TEST_MYSQL: '1'
GFRAME_TEST_MYSQL_DSN: mysql:host=127.0.0.1;port=3306
GFRAME_TEST_MYSQL_USER: root
GFRAME_TEST_MYSQL_PASSWORD: gframe_test
GFRAME_TEST_REDIS: '1'
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: mbstring, openssl, pdo_mysql, pdo_sqlite, redis
coverage: none
- uses: ramsey/composer-install@v3
with:
dependency-versions: locked
- run: php packages/phpunit/phpunit/phpunit --filter 'MySqlMigrationIntegration|DatabasePreflight|InstallerTest|InstallerHttp|InstallerWizardHttp|RedisSessionIntegration' --fail-on-skipped

nginx-cgi:
name: Routing real con Nginx y PHP-CGI
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: mbstring, openssl, pdo_sqlite
coverage: none
- uses: ramsey/composer-install@v3
with:
dependency-versions: locked
- run: sudo apt-get update && sudo apt-get install -y nginx php8.3-cgi php8.3-sqlite3 php8.3-mbstring php8.3-xml
- run: php packages/phpunit/phpunit/phpunit --filter 'ServerRoutingTest::testRealNginxRoutesRequestsAndServerErrorsToTheInstalledFramework' --fail-on-skipped
env:
GFRAME_TEST_NGINX: /usr/sbin/nginx
GFRAME_TEST_PHP_CGI: /usr/bin/php-cgi8.3

javascript:
name: JavaScript (sin navegador)
runs-on: ubuntu-latest
Expand All @@ -98,6 +155,36 @@ jobs:
package-manager-cache: false
- run: node --test --test-reporter=tap tests/js/*.test.cjs

browser:
name: Recorridos reales con Chromium
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: mbstring, openssl, pdo_sqlite, gd
coverage: none
- uses: ramsey/composer-install@v3
with:
dependency-versions: locked
- uses: actions/setup-node@v7
with:
node-version: '24'
package-manager-cache: false
- name: Instalar navegador de pruebas en el directorio temporal
run: |
npm install --prefix "$RUNNER_TEMP/gframe-browser" --no-package-lock --no-audit --no-fund playwright@1.62.1
"$RUNNER_TEMP/gframe-browser/node_modules/.bin/playwright" install --with-deps chromium
echo "NODE_PATH=$RUNNER_TEMP/gframe-browser/node_modules" >> "$GITHUB_ENV"
- name: Ejecutar todos los recorridos de navegador
run: |
export GFRAME_TEST_PHP="$(command -v php)"
export GFRAME_TEST_BROWSER="$(node -p 'require("playwright").chromium.executablePath()')"
set -o pipefail
node --test --test-concurrency=1 --test-reporter=tap tests/js/*.test.cjs | tee "$RUNNER_TEMP/gframe-browser.tap"
grep -Fx '# skipped 0' "$RUNNER_TEMP/gframe-browser.tap"

skills-metadata:
name: Metadata YAML de skills
runs-on: ubuntu-latest
Expand Down
6 changes: 3 additions & 3 deletions maintenance/cierre-auditorias-20261025.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ Los nueve deltas se marcan `auditada` porque la última pasada de su etapa los i
| [BridgeFrame](bridgeframe-pending.md) | El usuario confirmó la adaptación v2 el 6 de octubre | Retirado del backlog de GFrame; no acredita integración externa en vivo |
| [Skills y contratos](inventario-skills-contratos.md), [distribución](distribucion-skills.md) | Lotes 31–33, instalación/carga en Codex y resolución K-01–K-04/M-01 documentadas | No quedan esos cambios pospuestos de código; Claude fue excluido expresamente de esa validación |

## Verificaciones que no se marcan como realizadas
## Estado de verificaciones complementarias

Estos puntos son límites concretos de evidencia, no nuevas funcionalidades ni razones para reabrir una etapa cuyo alcance aprobado ya se auditó:

- **V-01 — cobertura visual restante:** prueba visual de cada biblioteca opcional y de todas las vistas/recursos de todos los módulos en una instalación limpia. Selector/editor y recorridos concretos sí tienen auditorías y pruebas. No equivalen a esa cobertura universal. Fuente: [auditoría de módulos](auditoria-modulos.md).
- **V-02 — infraestructura restante:** routing general con Nginx/PHP-CGI, integración MySQL/HTTP específicamente en PHP 8.4 y sesiones Redis con servicio real. Nginx/PHP-FPM para cron y la matriz PHP sin esas integraciones sí están acreditados. Fuente: [preparación histórica](preparacion-0.9.0.md) y [CI/lint](cobertura-checks-20261006.md).
- **V-01 — parcial:** los 99 casos de la suite con Chromium pasan sin fallos ni omisiones en CI 40171975482, incluidos instalador, selector, gestión y autoguardado. [Evidencia de la continuación](verificacion-infraestructura-20261025.md). Permanece sin acreditar la prueba visual de cada biblioteca opcional y de todas las vistas/recursos de todos los módulos en una instalación limpia; esos recorridos no equivalen a una cobertura universal. Fuente: [auditoría de módulos](auditoria-modulos.md).
- **V-02 — cerrada en continuación:** routing general con Nginx/PHP-CGI (1 prueba/85 aserciones), MySQL/HTTP con PHP 8.4 y Redis real (36/764), sin omisiones, en CI 40171556708. [Evidencia y límites](verificacion-infraestructura-20261025.md). La primera prueba Nginx tenía una expectativa CSRF obsoleta; el test corregido comprueba rechazo sin token y solicitud válida con sesión/token, sin cambiar el core. El resultado no certifica producción.
- **V-03 — correo externo:** diagnóstico de recepción del registro inicial y compatibilidad en clientes remotos. Pruebas de render, cola y transporte falso no prueban aceptación SMTP ni recepción. No se envió correo ni se accedió a proveedores en este cierre. Fuente: [plantillas](auditorias/plantillas-correo/AUDIT.md).
- **V-04 — personalización del pie del correo:** el año y la homogeneidad están implementados; el contenido personalizado de créditos del pie web no se reutiliza automáticamente en las plantillas de correo. Se conserva como decisión/integración pendiente; no se cambia el motor por marcar una lista. Fuente: [plantillas](auditorias/plantillas-correo/AUDIT.md).
- **V-05 — hosting y consumidores:** cabeceras y bloqueos efectivos del servidor de producción, despliegue de cada aplicación y pruebas con proveedores reales. Las comprobaciones de fixtures no certifican esos entornos. Fuente: [seguridad inicial](auditorias/alineacion-4.3.0/SEGURIDAD-INICIAL.md).
Expand Down
43 changes: 43 additions & 0 deletions maintenance/verificacion-infraestructura-20261025.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Verificación de infraestructura pendiente

Base: main `d477d760a3b2a3661b0c120dd258fed42a30a230`. Continuación de V-02 del [seguimiento histórico](cierre-auditorias-20261025.md). Solo pruebas y CI; no se modifica el core ni se instalan servicios en proyectos del usuario.

Convenciones seleccionadas: orchestrator, framework-maintenance y auth-access, con los contratos de sesiones y revocación. El resultado requiere ejecuciones reales; un test omitido impide aprobar el job dedicado.

## Comprobaciones preparadas

- PHP 8.4 contra MySQL 8: migraciones, preflight, instalación y recorridos HTTP de fixtures temporales.
- phpredis contra Redis 7: lectura/escritura, revocación desde otra instancia, escritura tardía que no restaura sesiones, bloqueo/desbloqueo, versiones de autorización global/usuario/tenant y expiración real. La cuenta se consulta en SQLite temporal; los Lua y TTL se ejecutan en Redis real. Limpieza limitada al prefijo aleatorio de cada prueba, sin vaciar bases Redis.
- Routing Nginx/PHP-CGI: ejecutar el test existente con recursos públicos, rutas virtuales, archivos internos protegidos, errores y canales.

Los resultados de CI se registrarán después de la ejecución. No se presenta esta preparación como V-02 resuelta. SMTP, personalización del pie, hosting real y cobertura visual universal conservan sus límites.

## Primera ejecución

[CI 40171290826](https://github.com/gorvet/gframe-framework/actions/runs/40171290826), commit `6574d5c`: MySQL/HTTP y Redis reales, PHP 8.4.27, 36 pruebas/764 aserciones, sin omisiones ni fallos; job 121102466808. Redis 7.4.11 y MySQL 8.0.46 reales en contenedores temporales.

Nginx/PHP-CGI llegó a 79 aserciones y falló porque el test antiguo esperaba `recovery_requested` enviando un POST sin CSRF; el framework devolvió correctamente `fail_csrf`. Job 121102466609. El resto de la matriz pasó. Se conserva este fallo como evidencia de una expectativa obsoleta del fixture, no se cambia la protección del framework.

La corrección de la prueba conserva el caso negativo y añade GET del formulario, cookie de sesión y token/timestamp reales antes del POST válido. Requiere una nueva ejecución de Nginx/PHP-CGI para cerrar ese escenario.

## Segunda ejecución de infraestructura

[CI 40171556708](https://github.com/gorvet/gframe-framework/actions/runs/40171556708), commit `6897107`: doce jobs aprobados. Nginx/PHP-CGI: 1 prueba/85 aserciones, sin omisiones; job 121103266424. MySQL/HTTP y Redis con PHP 8.4.27: 36 pruebas/764 aserciones, sin omisiones; job 121103266379. Los tres escenarios concretos de V-02 quedan acreditados, sin extender el resultado a producción.

## Recorridos de navegador

Suite local completa con Chrome, Playwright 1.62.1 y PHP 8.3.35, base d477d76: 99 casos, 95 aprobados, cuatro fallidos, sin omisiones. Instalador, gestión y selector fallan al navegar a loopback con `ERR_NETWORK_ACCESS_DENIED`; no se presentan como fallos demostrados del producto. El autoguardado falla al comparar timestamps tomados cuando Node recibe los eventos de red, en lugar del instante de envío en el navegador. La repetición aislada vuelve a fallar.

Sonda independiente en memoria, sin editar el runtime: envíos separados por 5005,6 ms en `performance.now()` y 5005 ms en Date.now() del navegador; recepción de esos mismos eventos en Node separada por 4933 ms. La sonda completa termina aprobada, 1 caso. La llegada del evento al arnés no es el reloj del planificador; la demora de cada evento varía.

El test conserva el umbral de 4800 ms y observa el envío real de XMLHttpRequest mediante el reloj monótono del navegador. No modifica temporizadores ni tráfico del producto. La nueva ejecución con demora deliberada del arnés debe confirmar que no se confunde esa demora con el intervalo del editor.

Sonda con 500 ms de demora deliberada antes de registrar el primer evento en Node: el navegador separa sus envíos 4999,2 ms, mientras el arnés registra 4410 ms. El criterio corregido mantiene el umbral y mide la propiedad exigida; el anterior rechazaría un intervalo correcto por la demora introducida en la observación. Recorrido completo con la demora inyectada: 1 caso aprobado, sin omisiones, 56,7 segundos. Log local `~/.gframe-agents/autosave-demora-controlada.log`.

Se añade un job Chromium que ejecuta la suite completa en Linux, donde no se hereda el bloqueo local. Dependencias solo en el directorio temporal del runner; sin package.json ni cambios del core. La cobertura de todos los paquetes visuales opcionales sigue siendo más amplia que estos recorridos y no se da por completada automáticamente.

## Cierre de los recorridos existentes

[CI 40171975482](https://github.com/gorvet/gframe-framework/actions/runs/40171975482), commit `8c1e303`, job Chromium 121104534537: **99 casos aprobados, cero fallos, cero omisiones**, 219 segundos. Incluye instalador, gestión y selector que el entorno local impedía abrir, además del autoguardado con la medición corregida. La condición posterior del job exige explícitamente `# skipped 0`, conservando el fallo del proceso mediante pipefail.

V-02 está cerrada para sus tres escenarios identificados. V-01 tiene ahora todos los recorridos existentes de navegador ejecutados, pero la cobertura visual universal de cada biblioteca/vista sigue sin acreditarse. V-03, V-04 y V-05 conservan su alcance: no hubo acceso a SMTP externo, cambios de pie personalizados ni despliegue o certificación del hosting.
112 changes: 112 additions & 0 deletions tests/RedisSessionIntegrationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php

namespace GFrame\Tests;

use GFrame\Session\RedisSessionHandler;
use GFrame\Session\SessionRuntime;
use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
use PHPUnit\Framework\TestCase;

#[RunTestsInSeparateProcesses]
final class RedisSessionIntegrationTest extends TestCase
{
private \Redis $redis;
private string $prefix;

protected function setUp(): void
{
if (getenv('GFRAME_TEST_REDIS') !== '1') self::markTestSkipped('Requiere Redis de pruebas.');
self::assertTrue(extension_loaded('redis'), 'La integración solicitada requiere phpredis.');
$this->prefix = 'gframe-test:' . bin2hex(random_bytes(12)) . ':';
$this->redis = new \Redis();
self::assertTrue($this->redis->connect(getenv('GFRAME_TEST_REDIS_HOST') ?: '127.0.0.1', (int)(getenv('GFRAME_TEST_REDIS_PORT') ?: 6379), 2));
define('DB_DEFAULT_CONNECTION', 'redis-test');
define('DB_CONNECTIONS', ['redis-test' => ['driver' => 'sqlite', 'path' => ':memory:']]);
$database = \DatabaseManager::connection();
self::assertInstanceOf(\PDO::class, $database);
$database->exec("CREATE TABLE users(user_id INTEGER PRIMARY KEY,status TEXT); INSERT INTO users VALUES(7,'verify'),(8,'verify'),(9,'suspended')");
SessionRuntime::clearAuthorizationStale();
}

protected function tearDown(): void
{
if (isset($this->redis) && $this->redis->isConnected()) {
$keys = $this->redis->keys($this->prefix . '*');
if ($keys !== []) $this->redis->del($keys);
$this->redis->close();
}
\DatabaseManager::disconnect();
SessionRuntime::clearAuthorizationStale();
}

public function testRevocationAcrossHandlersDoesNotResurrectSessions(): void
{
$writer = new RedisSessionHandler($this->redis, $this->prefix, 60);
foreach (['desktop', 'mobile'] as $id) {
$writer->register(7, $id);
self::assertTrue($writer->write($id, $id . '-payload'));
}
$writer->register(8, 'other');
$writer->write('other', 'other-payload');
$stale = new RedisSessionHandler($this->redis, $this->prefix, 60);
self::assertSame('desktop-payload', $stale->read('desktop'));
$revoker = new RedisSessionHandler($this->redis, $this->prefix, 60);
self::assertSame(2, $revoker->revokeUser(7));
self::assertTrue($stale->write('desktop', 'late-payload'));
self::assertTrue($stale->updateTimestamp('desktop', 'late-payload'));
self::assertFalse($writer->validateId('desktop'));
self::assertFalse($writer->validateId('mobile'));
self::assertSame('', $writer->read('desktop'));
self::assertSame('other-payload', $writer->read('other'));
$writer->register(7, 'new-login');
$writer->write('new-login', 'new-payload');
self::assertSame('new-payload', $writer->read('new-login'));
}

public function testBlockingAndAuthorizationVersionsUseRealRedisState(): void
{
$handler = new RedisSessionHandler($this->redis, $this->prefix, 60);
$handler->register(7, 'authorized', ['role_id' => 3, 'role_version' => 1, 'authorization_version' => 1]);
$handler->write('authorized', 'payload');
$handler->publishRoleVersion(3, 2);
self::assertSame('payload', $handler->read('authorized'));
self::assertTrue(SessionRuntime::authorizationStale());
SessionRuntime::clearAuthorizationStale();
$handler->updateAuthorization(7, 'authorized', 3, 2, 1);
$handler->publishUserAuthorizationVersion(7, 2);
$handler->read('authorized');
self::assertTrue(SessionRuntime::authorizationStale());
SessionRuntime::clearAuthorizationStale();
$handler->updateAuthorization(7, 'authorized', 3, 2, 2);
$handler->updateTenantAuthorization(7, 'authorized', 4, 1);
$handler->publishRoleVersion(4, 2);
$handler->read('authorized');
self::assertTrue(SessionRuntime::authorizationStale());
self::assertSame(1, $handler->revokeUser(7, true));
try {
$handler->register(7, 'blocked');
self::fail('Una cuenta bloqueada no debe registrar una sesión.');
} catch (\RuntimeException $exception) {
self::assertSame('La cuenta no admite nuevas sesiones.', $exception->getMessage());
}
$handler->allowUser(7);
$handler->register(7, 'allowed');
self::assertTrue($handler->validateId('allowed'));
$this->expectException(\RuntimeException::class);
$handler->register(9, 'suspended');
}

public function testStoredSessionExpiresInRedis(): void
{
$handler = new RedisSessionHandler($this->redis, $this->prefix, 2);
$handler->register(7, 'short');
$handler->write('short', 'expires');
self::assertSame('expires', $handler->read('short'));
$deadline = microtime(true) + 5;
while ($handler->validateId('short') && microtime(true) < $deadline) usleep(50000);
self::assertFalse($handler->validateId('short'));
self::assertSame('', $handler->read('short'));
self::assertTrue($handler->write('short', 'late'));
self::assertFalse($handler->validateId('short'));
}
}
15 changes: 15 additions & 0 deletions tests/ServerRoutingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,21 @@ public function testRealNginxRoutesRequestsAndServerErrorsToTheInstalledFramewor
self::assertStringNotContainsString('<html', $body);
[$status, $body] = $this->request($base . '/ajax/lostpassword', 'POST', 'recovery_email=unknown%40example.test&middle_name=');
self::assertSame(200, $status);
self::assertSame('fail_csrf', json_decode($body, true)['code'] ?? null);
[$status, $form, $headers] = $this->request($base . '/login/lostpassword');
self::assertSame(200, $status);
self::assertSame(1, preg_match('/name="csrfToken" value="([^"]+)"/', $form, $token));
self::assertSame(1, preg_match('/name="csrfTimestamp" value="([^"]+)"/', $form, $timestamp));
$cookies = [];
foreach ($headers as $header) {
if (preg_match('/^Set-Cookie:\s*([^;]+)/i', $header, $cookie)) $cookies[] = $cookie[1];
}
self::assertNotEmpty($cookies);
[$status, $body] = $this->request($base . '/ajax/lostpassword', 'POST', http_build_query([
'recovery_email' => 'unknown@example.test', 'middle_name' => '',
'csrfToken' => $token[1], 'csrfTimestamp' => $timestamp[1],
]), ['Cookie: ' . implode('; ', $cookies)]);
self::assertSame(200, $status);
self::assertSame('recovery_requested', json_decode($body, true)['code'] ?? null);
} finally {
foreach (array_reverse($processes) as $process) { proc_terminate($process); proc_close($process); }
Expand Down
Loading
Loading