Bloque III

Construcción del sistema

Taller Integrador de Especialización

Facultad de Informática Culiacán
Universidad Autónoma de Sinaloa

UAS - Taller Integrador de Especialización

Al terminar este bloque vas a poder

RA3.1 Implementar lo prioritario

Las historias más valiosas de tu backlog

RA3.2 Integrar componentes

Una API que reutiliza tu lógica sin copiarla

RA3.3 APIs y persistencia

REST versionada sobre PostgreSQL

RA3.4 Calidad y seguridad

Pruebas revisión permisos y secretos

RA3.5 Iteraciones Scrum

Dos o tres sprints con incremento real

UAS - Taller Integrador de Especialización

Dos decisiones del curso

GitLab

Lo que en el Bloque II llamaste pull request aquí se llama merge request

PostgreSQL

Tu proyecto deja SQLite y trabaja con el motor del estándar

Mismo mecanismo con otro nombre el programa oficial dice pull request y GitLab dice merge request

UAS - Taller Integrador de Especialización

Un defecto que nadie vio

UAS - Taller Integrador de Especialización

El código del Bloque II

Prestamo::where('equipo_id', $datos['equipo_id'])
    ->where(function ($q) use ($datos) {
        $q->whereBetween('fecha_inicio', [$datos['fecha_inicio'], $datos['fecha_fin']])
          ->orWhereBetween('fecha_fin', [$datos['fecha_inicio'], $datos['fecha_fin']]);
    })->exists();

Pregunta si el inicio o el fin de un préstamo existente cae dentro del horario nuevo

Suena razonable y funcionó en la demostración

UAS - Taller Integrador de Especialización

Pruébalo con un proyector

Existente Nuevo El sistema Debería
10:00 a 14:00 11:00 a 12:00 Lo acepta Rechazarlo
10:00 a 12:00 12:00 a 14:00 Lo rechaza Aceptarlo
UAS - Taller Integrador de Especialización

La pregunta del bloque

¿Cómo puede tu equipo tener evidencia y no solo confianza de que el sistema hace lo correcto para el usuario correcto cada vez que alguien integra un cambio?

Actividad A1 — dibujen todas las posiciones posibles entre dos reservas

UAS - Taller Integrador de Especialización

El camino del bloque

Datos

Modelar y persistir en PostgreSQL

Componentes y API

REST con errores bien manejados

Calidad

Pruebas que encuentran el defecto

Seguridad

Permisos secretos y configuración

Integración continua

Una máquina que impide que el defecto regrese

UAS - Taller Integrador de Especialización

3.2 Manejo de datos

UAS - Taller Integrador de Especialización

Modelar antes de guardar

Entidad

Algo con identidad propia que el sistema recuerda

Relación

Cómo se conectan las entidades y en qué cantidad

Documento

Una unidad de información con sus datos anidados

users 1 ─── N prestamos N ─── 1 equipos
UAS - Taller Integrador de Especialización

Relacional o documental

Relacional Documental
Estructura Fija por esquema Flexible por documento
Integridad La garantiza el motor La garantiza tu código
Cruzar entidades Natural con JOIN Costoso o con datos duplicados
Buen uso Datos con relaciones fuertes Estructura variable leída como unidad

Estándar del proyecto — PostgreSQL por defecto y MongoDB solo con ADR que lo justifique

UAS - Taller Integrador de Especialización

El mismo préstamo como documento

{
  "_id": "42",
  "usuario": { "id": 7, "nombre": "Ana" },
  "equipo": { "id": 3, "nombre": "Proyector 101" },
  "fechaInicio": "2026-11-10T10:00:00Z"
}

Si Ana cambia su nombre hay que actualizar cada documento donde aparece

Actividad A3 — decidan tabla o documento para tres datos de su proyecto

UAS - Taller Integrador de Especialización

La base de datos también se defiende

$table->foreignId('user_id')->constrained();

DB::statement('ALTER TABLE prestamos
  ADD CONSTRAINT chk_prestamos_fechas CHECK (fecha_fin > fecha_inicio)');
Sin restricción

Un seeder o una consola pueden guardar basura

Con restricción

PostgreSQL rechaza el dato sin importar de dónde venga

STD-DB-P02 y STD-DB-P03

UAS - Taller Integrador de Especialización

Migraciones que van y vuelven

php artisan migrate
php artisan migrate:rollback
php artisan migrate
Reversible

Todo down deshace exactamente su up

Intocable

Una migración ya integrada no se edita se crea otra

STD-DB-M01 y STD-DB-M03

UAS - Taller Integrador de Especialización

El problema N+1

foreach (Prestamo::all() as $prestamo) {
    echo $prestamo->equipo->nombre;   // una consulta por préstamo
}

Prestamo::with('equipo')->get();      // dos consultas en total
UAS - Taller Integrador de Especialización

Lo que la validación no resuelve sola

Dos personas envían la misma reserva en el mismo instante y ambas consultas ven sin conflicto antes de que alguna guarde

Se llama condición de carrera

En el Bloque IV profundizarás en el comportamiento bajo carga por ahora regístralo como riesgo

UAS - Taller Integrador de Especialización

3.1 Desarrollo de componentes

UAS - Taller Integrador de Especialización

Capas módulos y componentes

                 Préstamos             Equipos
Presentación     Controller web + API   Controller API
                 Form Request           Form Request
                 Resource               Resource
Negocio          PrestamoService        (Eloquent basta)
Datos            Repository + Model     Model
                        └──── PostgreSQL ────┘

No todo módulo necesita todas las capas

UAS - Taller Integrador de Especialización

Qué es una API

Un contrato que permite a otro programa usar tu sistema sin conocer su código

Dónde

La URL del recurso

Cómo

El método HTTP y los datos

Qué recibes

El código de estado y el JSON

Actividad A2 — el contrato de la API de tu proyecto antes de programarla

UAS - Taller Integrador de Especialización

REST en cinco ideas

  • Todo es un recurso con nombre en plural
  • Cada recurso tiene su propia URL
  • El método dice la acción no la URL
  • Cada petición trae todo lo necesario incluido el token
  • El código de estado dice el resultado
Correcto Incorrecto
POST /api/v1/prestamos POST /api/crearPrestamo
UAS - Taller Integrador de Especialización

Métodos HTTP

Método Uso Seguro Idempotente
GET Leer Sí Sí
POST Crear No No
PUT Reemplazar No Sí
PATCH Modificar parte No No
DELETE Eliminar No Sí

RFC 9110 HTTP Semantics secciones 9.2.1 y 9.2.2

UAS - Taller Integrador de Especialización

Códigos que vas a usar

2xx éxito

200 leer · 201 creado · 204 sin cuerpo

4xx el cliente

401 · 403 · 404 · 409 · 422 · 429

UAS - Taller Integrador de Especialización

422 o 409

422

Los datos están mal formados como una fecha de fin antes del inicio

409

Los datos son válidos pero chocan con el estado del sistema como un equipo ya reservado

401

No sé quién eres

403

Sé quién eres y no puedes hacer esto

UAS - Taller Integrador de Especialización

3.2.3 Datos en tránsito

JSON ── petición ──►  deserializar → validar → Service → PostgreSQL
JSON ◄─ respuesta ──  serializar (API Resource) ◄──────────┘
snake_case

fecha_inicio

ISO 8601 UTC

2026-11-10T10:00:00Z

Sin sensibles

Nunca password

JSON no tiene tipo fecha · RFC 8259

UAS - Taller Integrador de Especialización

El controlador integra no decide

public function store(GuardarPrestamoRequest $request, PrestamoService $service)
{
    $prestamo = $service->crear([
        ...$request->validated(),
        'user_id' => $request->user()->id,
    ]);

    return PrestamoResource::make($prestamo->load('equipo'))
        ->response()->setStatusCode(201);
}

Form Request y Service son los del Bloque II sin cambios · user_id sale del token nunca del cuerpo

UAS - Taller Integrador de Especialización

3.1.3 Cuando algo sale mal

Errores y situaciones no esperadas

UAS - Taller Integrador de Especialización

No todo error es igual

Situación Ejemplo Quién la detecta Respuesta
Datos inválidos Falta fecha_fin Form Request 422
Regla de negocio Equipo ya reservado Service 409
Sin identidad Token vencido auth:sanctum 401
Sin permiso Préstamo ajeno Policy 403
No existe Préstamo 999 Laravel 404
No esperada PostgreSQL no responde Nadie la planeó 500 genérico

Las cinco primeras son esperadas se diseñan · la última se controla

UAS - Taller Integrador de Especialización

Esperadas e inesperadas

Esperada

Parte del dominio · tiene nombre propio · el usuario puede corregirla · se registra como info

Inesperada

Falla técnica · el usuario no puede hacer nada · se registra como error con todo el detalle

UAS - Taller Integrador de Especialización

Paso 1 — dale nombre al problema

class ConflictoDeReservaException extends RuntimeException
{
    public function __construct(public readonly ?int $equipoId = null)
    {
        parent::__construct('El equipo ya está reservado en ese horario.');
    }
}

Una excepción de dominio dice qué pasó y lleva los datos para diagnosticarlo

UAS - Taller Integrador de Especialización

Paso 2 — lanzar donde se detecta

// PrestamoService
public function crear(array $datos): Prestamo
{
    if ($this->repositorio->existeConflicto($datos)) {
        throw new ConflictoDeReservaException((int) $datos['equipo_id']);
    }

    return $this->repositorio->crear($datos);
}

El Service no sabe si lo llamó la web o la API · no devuelve JSON ni códigos HTTP

UAS - Taller Integrador de Especialización

Paso 3 — traducir en un solo lugar

// bootstrap/app.php
$exceptions->render(function (ConflictoDeReservaException $e, Request $request) use ($error) {
    if ($request->is('api/*')) {
        Log::info('Reserva rechazada por conflicto de horario', [
            'equipo_id' => $e->equipoId,
            'user_id' => $request->user()?->id,
        ]);

        return $error('CONFLICTO_RESERVA', $e->getMessage(), [], 409);
    }
});

STD-BE-E02 · un render por excepción conocida · lo demás lo maneja Laravel

UAS - Taller Integrador de Especialización

Un formato de error para toda la API

{
  "error": {
    "code": "CONFLICTO_RESERVA",
    "message": "El equipo ya está reservado en ese horario.",
    "details": {}
  }
}
code

Lo compara un programa

message

Lo lee una persona

details

Qué campo falló

UAS - Taller Integrador de Especialización

Paso 4 — atrapar solo si puedes hacer algo

Deshacer

Revertir lo que quedó a medias

Traducir

Convertir un error técnico en uno de dominio

Reintentar

Un servicio externo que falló una vez

catch (\Exception $e) { }   // BLOCKER — STD-BE-E03 el error desaparece

Si no puedes hacer ninguna de las tres no lo atrapes deja que suba

UAS - Taller Integrador de Especialización

Deshacer — todo o nada

DB::transaction(function () use ($datos) {
    $prestamo = Prestamo::create($datos);
    $prestamo->equipo->update(['disponible' => false]);
});
Sin transacción

Si falla la segunda línea queda un préstamo con el equipo marcado disponible

Con transacción

Si algo lanza una excepción PostgreSQL deshace las dos escrituras

Ejemplo ilustrativo · ReservaFIC no actualiza disponible al prestar

UAS - Taller Integrador de Especialización

Traducir — error técnico a error de dominio

try {
    $prestamo = $this->repositorio->crear($datos);
} catch (UniqueConstraintViolationException $e) {
    throw new ConflictoDeReservaException((int) $datos['equipo_id']);
}

Si el motor rechaza un duplicado el usuario recibe el mismo 409 de siempre y no un 500

Ejemplo ilustrativo · solo aplica si existe una restricción única que lo provoque

UAS - Taller Integrador de Especialización

Lo que ve cada quien

El cliente

Código de estado · code · un mensaje genérico en el 500

El log

Clase de la excepción · archivo · línea · IDs del caso

[2026-11-10 10:03:12] local.ERROR: SQLSTATE[08006] connection to server failed
  {"userId":7,"exception":"[object] (Illuminate\\Database\\QueryException ...)"}

Registro ilustrativo

UAS - Taller Integrador de Especialización

Registrar con contexto y sin secretos

Log::info('Reserva rechazada por conflicto de horario', [
    'equipo_id' => $e->equipoId,
    'user_id' => $request->user()?->id,
]);
Sí

Identificadores que permiten reconstruir el caso

Nunca

Contraseñas tokens o el cuerpo completo de la petición

Nivel Cuándo
info Evento normal del negocio
warning Algo raro que no impidió la operación
error La operación falló
UAS - Taller Integrador de Especialización

3.3 Calidad durante el desarrollo

UAS - Taller Integrador de Especialización

Qué es una prueba automatizada

Código que ejecuta tu código con datos conocidos y compara el resultado con lo esperado

Repetible

Mismo resultado cada vez

Rápida

Segundos no minutos

Automática

Nadie tiene que recordar ejecutarla

UAS - Taller Integrador de Especialización

Unitaria o integración

Unitaria Integración
Verifica Una pieza aislada Piezas reales juntas
Base de datos No Sí la de pruebas
Velocidad Milisegundos Más lenta
En Laravel tests/Unit tests/Feature

Una unidad es la pieza más pequeña con una responsabilidad propia · un método con una regla

UAS - Taller Integrador de Especialización

Toda prueba tiene tres partes

Preparar   → los datos y las piezas que la prueba necesita
Actuar     → llamar una sola vez al código que se prueba
Verificar  → comparar el resultado con lo esperado

En inglés Arrange Act Assert · si no puedes nombrar las tres partes la prueba hace demasiado

UAS - Taller Integrador de Especialización

Ejemplo mínimo — la regla

// Ejemplo de aprendizaje · no forma parte de ReservaFIC
class ReglaHorario
{
    public static function esValido(string $inicio, string $fin): bool
    {
        return strtotime($fin) > strtotime($inicio);
    }
}

Sin base de datos sin HTTP sin Laravel · la forma más simple de unidad

UAS - Taller Integrador de Especialización

Ejemplo mínimo — su prueba

use PHPUnit\Framework\TestCase;

class ReglaHorarioTest extends TestCase
{
    public function test_acepta_un_fin_posterior_al_inicio(): void
    {
        $inicio = '2026-11-10 10:00';                 // Preparar
        $fin = '2026-11-10 12:00';

        $resultado = ReglaHorario::esValido($inicio, $fin);   // Actuar

        $this->assertTrue($resultado);                 // Verificar
    }
}

tests/Unit · extiende PHPUnit\Framework\TestCase · el método empieza con test_

UAS - Taller Integrador de Especialización

Un positivo no basta

public function test_rechaza_un_fin_anterior_al_inicio(): void
{
    $this->assertFalse(ReglaHorario::esValido('2026-11-10 12:00', '2026-11-10 10:00'));
}

public function test_rechaza_un_fin_igual_al_inicio(): void
{
    $this->assertFalse(ReglaHorario::esValido('2026-11-10 10:00', '2026-11-10 10:00'));
}
Positivo

Hace lo que debe

Negativo

Rechaza lo que debe

Borde

El valor justo en el límite

UAS - Taller Integrador de Especialización

Ejecutar y leer el resultado

php artisan test --filter=ReglaHorarioTest
PASS  Tests\Unit\ReglaHorarioTest
  acepta un fin posterior al inicio
  rechaza un fin anterior al inicio
  rechaza un fin igual al inicio

Tests:  3 passed

Salida simplificada · ante una falla PHPUnit muestra lo esperado lo obtenido y la línea

UAS - Taller Integrador de Especialización

Las aserciones que más usarás

Aserción Verifica
assertTrue y assertFalse Un resultado booleano
assertSame($esperado, $real) Valor y tipo idénticos
assertCount(1, $lista) Cuántos elementos hay
expectException(Clase::class) Que se lance una excepción
assertDatabaseHas('tabla', [...]) Que una fila existe · solo integración

Una prueba sin aserción siempre pasa y no demuestra nada

UAS - Taller Integrador de Especialización

Paso 2 — una unidad con dependencias

PrestamoService necesita un repositorio que sí usa la base de datos · ¿cómo se prueba aislado?

Prueba ──► PrestamoService ──► PrestamoRepositoryInterface
                                   ├── EloquentPrestamoRepository   (producción)
                                   └── repositorio falso            (prueba)

Por esto el equipo conserva el Repository · D-008 y ADR-002

UAS - Taller Integrador de Especialización

El repositorio falso

private function repositorioFalso(bool $hayConflicto): PrestamoRepositoryInterface
{
    return new class($hayConflicto) implements PrestamoRepositoryInterface {
        public array $creados = [];

        public function __construct(private bool $hayConflicto) {}

        public function existeConflicto(array $datos): bool
        {
            return $this->hayConflicto;          // responde lo que la prueba decide
        }

        public function crear(array $datos): Prestamo
        {
            $this->creados[] = $datos;           // recuerda qué le pidieron
            return new Prestamo();
        }
    };
}
UAS - Taller Integrador de Especialización

Probar el camino feliz

public function test_crea_el_prestamo_cuando_no_hay_conflicto(): void
{
    $repositorio = $this->repositorioFalso(hayConflicto: false);

    (new PrestamoService($repositorio))->crear(['equipo_id' => 1, 'user_id' => 7]);

    $this->assertCount(1, $repositorio->creados);
}

No hay base de datos · la prueba corre en milisegundos

UAS - Taller Integrador de Especialización

Probar que debe fallar

public function test_lanza_excepcion_y_no_crea_cuando_hay_conflicto(): void
{
    $repositorio = $this->repositorioFalso(hayConflicto: true);

    try {
        (new PrestamoService($repositorio))->crear(['equipo_id' => 1, 'user_id' => 7]);
        $this->fail('Se esperaba ConflictoDeReservaException.');
    } catch (ConflictoDeReservaException $e) {
        $this->assertSame(1, $e->equipoId);
        $this->assertCount(0, $repositorio->creados);
    }
}

Verifica dos cosas — que se lanzó la excepción correcta y que no se guardó nada

UAS - Taller Integrador de Especialización

Dobles de prueba

Objetos que ocupan el lugar de una dependencia real durante la prueba · lo viste en Pruebas de Software

Doble Qué hace
Dummy Se pasa porque la firma lo exige · nadie lo usa
Stub Devuelve respuestas preparadas
Spy Registra cómo lo llamaron
Mock Trae programadas las llamadas que deben ocurrir
Fake Funciona pero simplificado · por ejemplo en memoria
UAS - Taller Integrador de Especialización

¿Qué doble es el repositorio falso?

Stub

existeConflicto() responde lo que la prueba decide

Spy

$creados recuerda qué le pidieron

Los nombres describen papeles no clases · un mismo objeto puede cumplir dos

UAS - Taller Integrador de Especialización

Stub o mock — qué se verifica

Stub · verificación de estado

¿Qué resultado produjo? · excepción · valor devuelto · lo registrado

Mock · verificación de interacción

¿Se llamó o no se llamó? · cuántas veces · con qué argumentos

La diferencia importante no es el nombre sino lo que la prueba comprueba al final

UAS - Taller Integrador de Especialización

El mismo caso con un mock de PHPUnit

public function test_no_guarda_nada_cuando_hay_conflicto(): void
{
    $repositorio = $this->createMock(PrestamoRepositoryInterface::class);
    $repositorio->method('existeConflicto')->willReturn(true);   // papel de stub
    $repositorio->expects($this->never())->method('crear');      // papel de mock

    $this->expectException(ConflictoDeReservaException::class);

    (new PrestamoService($repositorio))->crear(['equipo_id' => 1, 'user_id' => 7]);
}

createStub() solo responde · createMock() además declara expectativas con expects()

UAS - Taller Integrador de Especialización

La expectativa es la aserción

$repositorio->expects($this->once())
    ->method('crear')
    ->with($datos)
    ->willReturn(new Prestamo());

(new PrestamoService($repositorio))->crear($datos);

No hay assert · PHPUnit falla la prueba si crear() no se llamó una vez con esos datos

UAS - Taller Integrador de Especialización

Un fake puede esconder el defecto

Repositorio en memoria

Reescribe en PHP la condición de traslape · prueba la copia

PostgreSQL de pruebas

Ejecuta la consulta real · prueba el original

Si la consulta SQL sigue mal el fake pasa igual · por eso la condición se prueba contra la base real

UAS - Taller Integrador de Especialización

Cuándo usar un doble y cuándo no

Situación Decisión
Correo o API de terceros Doble · Mail::fake() Http::fake()
Aislar una regla de la base de datos Stub del repositorio
Importa que algo no ocurra Mock o spy
Integración con la base del proyecto Base real · nunca un doble
La clase que se prueba Nunca se sustituye

08-ESTANDAR-PRUEBAS.md §6 · muchos mocks que verifican el orden interno atan la prueba a la implementación

UAS - Taller Integrador de Especialización

Paso 3 — muchos casos una sola prueba

public static function casos(): array
{
    return [
        'mismo horario'                             => ['10:00', '12:00', true],
        'contenido dentro del existente'            => ['10:30', '11:30', true],
        'termina justo cuando empieza el existente' => ['08:00', '10:00', false],
        'empieza justo cuando termina el existente' => ['12:00', '14:00', false],
    ];
}

#[DataProvider('casos')]
public function test_detecta_traslape_de_horarios(string $inicio, string $fin, bool $esperado): void

Cada fila es un caso con nombre · la tabla de la Actividad A1 se vuelve código

UAS - Taller Integrador de Especialización

Nombres que explican

Así no

test1 · testPrestamo · testFunciona

Así sí

test_responde_422_cuando_falta_fecha_fin

Cuando una prueba falla su nombre debe decirte qué regla se rompió sin abrir el archivo

UAS - Taller Integrador de Especialización

Errores frecuentes al probar

Error Por qué es un problema
Prueba sin aserción Siempre pasa
Una prueba depende de otra Falla según el orden · STD-TEST-03
Probar el framework Verificar que Eloquent guarda no prueba tu regla
Solo el caso feliz Los defectos viven en los bordes
Nunca verla fallar Puede estar pasando por la razón equivocada
UAS - Taller Integrador de Especialización

Buenas prácticas — F.I.R.S.T.

Significa En el proyecto
Fast Milisegundos Unitarias sin base de datos
Independent No depende de otra STD-TEST-03
Repeatable Mismo resultado siempre Sin red ni fecha actual sin control
Self-validating Pasa o falla sola Aserciones · nunca dd() para mirar
Timely Junto con el código Sin pruebas la historia no está terminada

Clean Code · Martin 2008 · retoma las buenas prácticas de Pruebas de Software

UAS - Taller Integrador de Especialización

Seis prácticas concretas

Un comportamiento por prueba

Varias aserciones sí · dos reglas no

Sin lógica en la prueba

Nada de if o for · usa data provider

Esperados literales

assertSame(120 ...) · no recalcules con la misma fórmula

Controla el tiempo

Fechas fijas · travelTo() si la regla usa now()

Por la puerta principal

Métodos públicos · no privados

Preparación a la vista

El dato que decide se lee en la prueba

UAS - Taller Integrador de Especialización

La lección del defecto

Prueba unitaria del Service

Usa un repositorio falso que no ejecuta SQL y pasa con el defecto

Prueba de integración

Ejecuta la consulta real contra PostgreSQL y falla con el defecto

UAS - Taller Integrador de Especialización

Ocho casos tres fallan

Nuevo frente a 10:00 a 12:00 Esperado Bloque II
10:30 a 11:30 contenido Conflicto Sin conflicto
08:00 a 10:00 contiguo Sin conflicto Conflicto
12:00 a 14:00 contiguo Sin conflicto Conflicto

Los defectos viven en los bordes

UAS - Taller Integrador de Especialización

La corrección

Prestamo::where('equipo_id', $datos['equipo_id'])
    ->where('fecha_inicio', '<', $datos['fecha_fin'])
    ->where('fecha_fin', '>', $datos['fecha_inicio'])
    ->exists();

Dos intervalos se traslapan si cada uno empieza antes de que termine el otro

El Service no cambió — el Repository aisló el defecto en un solo lugar

UAS - Taller Integrador de Especialización

Ver la prueba fallar primero

1 Issue

Pasos para reproducir

2 Prueba en rojo

Commit antes de corregir

3 Corrección

La misma prueba en verde

Una prueba que nunca viste fallar puede estar pasando por la razón equivocada

Actividad A4 — matriz de casos positivos negativos y de borde

UAS - Taller Integrador de Especialización

Datos de prueba

De dónde salen y dónde viven

UAS - Taller Integrador de Especialización

Tres fuentes de datos distintas

Desarrollo Pruebas Producción
Base reservafic reservafic_testing La del servidor
Quién la llena Seeder Cada prueba con factories Los usuarios
Se borra Cuando quieras Antes de cada prueba Nunca
Datos reales No No Sí
UAS - Taller Integrador de Especialización

La base de pruebas se configura aparte

<!-- phpunit.xml -->
<env name="APP_ENV" value="testing"/>
<env name="DB_CONNECTION" value="pgsql"/>
<env name="DB_DATABASE" value="reservafic_testing"/>
Mismo motor

PostgreSQL como en producción · STD-TEST §6

Otra base

RefreshDatabase la vacía · si apunta a desarrollo pierdes tus datos

UAS - Taller Integrador de Especialización

Factories — datos coherentes en una línea

// database/factories/EquipoFactory.php
public function definition(): array
{
    return [
        'nombre' => fake()->randomElement(['Laptop', 'Proyector', 'Cámara']).' '.fake()->numerify('###'),
        'descripcion' => fake()->sentence(),
        'disponible' => true,
    ];
}
$equipo = Equipo::factory()->create();            // uno con valores al azar válidos
$equipos = Equipo::factory()->count(5)->create(); // cinco
UAS - Taller Integrador de Especialización

Fijar solo lo que importa

$equipo = Equipo::factory()->create();

Prestamo::factory()->for($equipo)->create([
    'fecha_inicio' => '2026-11-10 10:00:00',   // lo que la prueba necesita controlar
    'fecha_fin' => '2026-11-10 12:00:00',
]);

La factory llena el resto · quien lee la prueba ve de inmediato qué datos son relevantes

STD-TEST §5 · factories y no datos repetidos escritos a mano en cada prueba

UAS - Taller Integrador de Especialización

Cada prueba empieza en limpio

class PrestamoApiTest extends TestCase
{
    use RefreshDatabase;   // migra una vez y revierte lo de cada prueba
Sin RefreshDatabase

Una prueba ve los préstamos que dejó otra y pasa o falla según el orden

Con RefreshDatabase

Cada prueba crea lo que necesita y nada más

STD-TEST-03 y STD-TEST-04

UAS - Taller Integrador de Especialización

Seeder o factory

Seeder

Datos de demostración para trabajar en desarrollo · admin@reservafic.test

Factory en la prueba

Los datos exactos que esa prueba necesita

No uses el seeder de demo como datos de prueba · si alguien cambia la demo se rompen pruebas que no tienen relación

UAS - Taller Integrador de Especialización

Datos de prueba que no lastiman

  • Nunca copies datos de personas reales a la base de pruebas
  • Usa fake() para nombres y correos y dominios .test
  • La contraseña de la UserFactory es conocida · solo para desarrollo y pruebas
  • Los datos de prueba tampoco van al repositorio si salen de una base real
UAS - Taller Integrador de Especialización

Revisar con severidades

Severidad Bloquea
BLOCKER — viola una regla P0 Sí
MAJOR — viola una regla P1 Sí hasta corregir
MINOR — mejora razonable No
NIT — preferencia No

BLOCKER — STD-API-05 el endpoint no está dentro de auth:sanctum y cualquiera puede leer préstamos

Actividad A5 — un merge request lleno de defectos

UAS - Taller Integrador de Especialización

3.4 Desarrollo seguro

UAS - Taller Integrador de Especialización

Principios antes que herramientas

Validar entradas

Nada de fuera se usa sin validar

Gestionar errores

Detalle en el servidor y lo mínimo al cliente

Mínimo privilegio

Solo los permisos necesarios

Y una idea que los une — defensa en profundidad varias barreras independientes para que una falla no sea suficiente

UAS - Taller Integrador de Especialización

Validar entradas — en la práctica

// Mal: confía en todo lo que manda el cliente
Prestamo::create($request->all());

// Bien: solo lo validado y la identidad desde el token
$service->crear([
    ...$request->validated(),
    'user_id' => $request->user()->id,
]);

Con la primera línea basta enviar "user_id": 1 para reservar a nombre de otra persona

STD-SEC-12 y STD-SEC-11

UAS - Taller Integrador de Especialización

Gestionar errores — fallar de forma segura

Revela

El correo no existe · La contraseña es incorrecta

No revela

CREDENCIALES_INVALIDAS para ambos casos

Falla abierta

Si no pude verificar el permiso dejo pasar

Falla cerrada

Si no pude verificar el permiso respondo 403

UAS - Taller Integrador de Especialización

Mínimo privilegio — en la práctica

Quién Demasiado Suficiente
La app en PostgreSQL Usuario postgres superusuario Usuario reservafic dueño solo de sus bases
Un estudiante Puede registrar equipos Solo crea y ve sus préstamos
El pipeline Variables visibles en cualquier rama Variables protegidas y enmascaradas
UAS - Taller Integrador de Especialización

Defensa en profundidad en ReservaFIC

Petición
  │  auth:sanctum         ¿quién eres?                 → 401
  │  Form Request         ¿los datos tienen sentido?   → 422
  │  Policy               ¿puedes hacer esto?          → 403
  │  Service              ¿respeta la regla?           → 409
  ▼  PostgreSQL           FK · NOT NULL · CHECK        → rechaza

Si alguien se salta la API con tinker o un seeder la base de datos sigue defendiéndose

UAS - Taller Integrador de Especialización

Quién eres y qué puedes hacer

Autenticación

Token de Sanctum que expira · falla con 401

Autorización

Policy por rol y por dueño · falla con 403

¿Ana puede cancelar el préstamo 42?
  ¿es administradora?        → sí puede
  ¿el préstamo es de Ana?    → sí puede
  en otro caso               → 403
UAS - Taller Integrador de Especialización

Una Policy

public function view(User $usuario, Prestamo $prestamo): bool
{
    return $usuario->esAdministrador()
        || $usuario->id === (int) $prestamo->user_id;
}
Gate::authorize('view', $prestamo);

Si llegó autenticado puede hacer todo es el error más frecuente

UAS - Taller Integrador de Especialización

3.4.3 Gestión de secretos

UAS - Taller Integrador de Especialización

Qué es un secreto

Cualquier valor que da acceso a algo · si otra persona lo tiene puede actuar como tú o como tu sistema

Credenciales

DB_PASSWORD · usuario y contraseña de servicios

Llaves

APP_KEY · llaves de API de terceros

Tokens

Tokens de Sanctum · tokens de acceso de GitLab

No son secretos APP_NAME · DB_HOST de tu máquina · el puerto · van en .env.example con su valor

UAS - Taller Integrador de Especialización

Dónde vive cada secreto

Ambiente Dónde Quién lo ve
Tu máquina .env fuera del repositorio Solo tú
Pruebas locales .env o .env.testing ignorados Solo tú
Pipeline de CI Variables de CI/CD de GitLab El pipeline · enmascaradas en el log
Producción La plataforma de despliegue En el Bloque IV profundizarás en esto
UAS - Taller Integrador de Especialización

El camino de un secreto en Laravel

.env                  DB_PASSWORD=clave-local-de-ana
  ▼
config/database.php   'password' => env('DB_PASSWORD'),
  ▼
tu código             config('database.connections.pgsql.password')
// Mal: el secreto queda en el repositorio para siempre
$cliente = new ServicioCorreo('sk_live_8f3a...');

// Bien: se lee de la configuración
$cliente = new ServicioCorreo(config('services.correo.key'));

env() solo dentro de config/ · el resto del código usa config()

UAS - Taller Integrador de Especialización

.env y .env.example

.env · no se versiona

DB_PASSWORD=clave-local-de-ana
APP_KEY=base64:Qm9...

.env.example · sí se versiona

DB_PASSWORD=
APP_KEY=

cp .env.example .env
php artisan key:generate

Una persona nueva levanta el proyecto con dos comandos sin recibir tus credenciales · STD-SEC-02

UAS - Taller Integrador de Especialización

Retomando .gitignore

En el Bloque II viste qué no entra al repositorio · ahora interesa cómo decide Git y dónde puede fallar

# Entorno y secretos
.env
.env.*
!.env.example

# Generados
/vendor
/node_modules
.phpunit.result.cache
/storage/*.key

recursos/estandares/config-examples/.gitignore adaptado a un proyecto en la raíz

UAS - Taller Integrador de Especialización

Cómo lee Git un .gitignore

Patrón Significa
.env Cualquier archivo llamado .env
.env.* .env.testing · .env.production · .env.backup
!.env.example Excepción · este sí se versiona
/vendor La carpeta vendor de la raíz solamente
*.log Cualquier archivo que termine en .log
# texto Comentario

Se leen de arriba hacia abajo · la última regla que coincide gana · por eso el ! va después

UAS - Taller Integrador de Especialización

Comprobar en lugar de suponer

$ git check-ignore -v .env
.gitignore:2:.env	.env

$ git check-ignore -v .env.testing
.gitignore:3:.env.*	.env.testing

$ git status --ignored --short
!! .env
!! vendor/

check-ignore dice qué línea del .gitignore decide el destino de cada archivo

UAS - Taller Integrador de Especialización

La trampa — .gitignore no deja de rastrear

.gitignore solo afecta archivos que Git todavía no rastrea · si .env ya se había agregado seguirá subiendo cambios

$ git ls-files .env
.env                          # Git lo rastrea aunque esté en .gitignore

$ git rm --cached .env        # deja de rastrearlo y conserva tu archivo local
$ git commit -m "chore: dejar de versionar .env"

Esto lo saca de los commits futuros · los anteriores lo siguen teniendo

UAS - Taller Integrador de Especialización

Si un secreto llegó al historial

git log --all --oneline -- .env
1 Rotar

Cambiar la contraseña o generar una llave nueva

2 Avisar

Al equipo y al docente

3 Prevenir

Corregir .gitignore y verificar con check-ignore

UAS - Taller Integrador de Especialización

Detectarlo antes de que alguien lo use

Antes del commit

git status y git diff --staged · ¿hay un .env o una llave?

En cada push

El pipeline revisa todo el historial con una herramienta de detección de secretos

Finding:  DB_PASSWORD=REDACTED
File:     config/database.php
Commit:   3f2a1b0

STD-SEC-03 · salida ilustrativa con el secreto ocultado

UAS - Taller Integrador de Especialización

Configuración insegura

APP_DEBUG=true

El 500 muestra archivos líneas y datos de conexión

APP_DEBUG=false

El cliente recibe un mensaje genérico y el detalle queda en el log

APP_DEBUG=true solo en tu máquina · en cualquier otro ambiente APP_ENV=production y APP_DEBUG=false

UAS - Taller Integrador de Especialización

OWASP Top 10:2025

A01 Broken Access Control A02 Security Misconfiguration A03 Software Supply Chain Failures A04 Cryptographic Failures
A05 Injection A06 Insecure Design A07 Authentication Failures A08 Software or Data Integrity Failures
A09 Security Logging and Alerting Failures A10 Mishandling of Exceptional Conditions

top10.owasp.org/2025 consultado 2026-10-20 · el estándar del proyecto aún cita la edición 2021

UAS - Taller Integrador de Especialización

Las cuatro del programa

Inyección

A05 · datos del usuario dentro del SQL

Exposición de información

A01 A02 A10 · datos ajenos o detalles internos

Configuraciones inseguras

A02 · debug abierto o CORS sin control

Dependencias vulnerables

A03 · composer audit

A10 es exactamente el tema de manejo de errores · un error mal controlado también es una vulnerabilidad

UAS - Taller Integrador de Especialización

Inyección en una línea

// Vulnerable
DB::select("SELECT * FROM equipos WHERE nombre = '{$request->nombre}'");

// Seguro
Equipo::where('nombre', $request->validated('nombre'))->get();

Con x' OR '1'='1 la primera consulta devuelve todo

Actividad A6 — mapa de amenazas de tu proyecto

UAS - Taller Integrador de Especialización

3.5 Integración continua

UAS - Taller Integrador de Especialización

Integrar poco y seguido

MR grande

40 archivos que nadie revisa a fondo y conflictos con todos

MR pequeño

5 archivos que se revisan en una sentada

Integración continua — integrar con frecuencia y verificar automáticamente cada integración

Humble y Farley Continuous Delivery 2010

UAS - Taller Integrador de Especialización

Lo que hoy haces a mano

composer install
cp .env.example .env && php artisan key:generate
vendor/bin/pint --test
php artisan migrate && php artisan migrate:rollback && php artisan migrate
php artisan test
A mano

Alguien lo olvida · funciona en mi máquina

Automático

Se ejecuta en cada push en una máquina limpia

UAS - Taller Integrador de Especialización

El vocabulario

Término Qué es
Pipeline Todo lo que se ejecuta por un push
Etapa (stage) Un grupo de trabajos · empieza si la anterior pasó
Trabajo (job) Una tarea con sus comandos
Runner La máquina que ejecuta los trabajos
.gitlab-ci.yml El archivo en la raíz que lo describe
UAS - Taller Integrador de Especialización

Paso 1 — el pipeline más pequeño

# .gitlab-ci.yml
hola:
  image: php:8.3-cli
  script:
    - php -v
    - echo "El pipeline funciona"

Un trabajo · una imagen · dos comandos · si alguno termina con error el trabajo queda en rojo

En GitLab Build · Pipelines muestra cada ejecución y su registro

UAS - Taller Integrador de Especialización

Paso 2 — etapas y trabajos

stages:
  - calidad
  - pruebas

estilo:
  stage: calidad
  script:
    - vendor/bin/pint --test

pruebas:
  stage: pruebas
  script:
    - php artisan test

Si estilo falla pruebas ni siquiera empieza · el error más barato se detecta primero

UAS - Taller Integrador de Especialización

Paso 3 — no repetir la preparación

.php:
  image: php:8.3-cli
  before_script:
    - composer install --prefer-dist --no-progress --no-interaction
    - cp .env.example .env
    - php artisan key:generate

estilo:
  extends: .php
  stage: calidad
  script:
    - vendor/bin/pint --test

Un trabajo que empieza con punto es una plantilla · extends la hereda · el equivalente de compilar en PHP

UAS - Taller Integrador de Especialización

Paso 4 — una base de datos para las pruebas

variables:
  POSTGRES_DB: reservafic_testing
  POSTGRES_USER: reservafic
  # POSTGRES_PASSWORD vive en Settings · CI/CD · Variables
  DB_CONNECTION: pgsql
  DB_HOST: postgres
  DB_PASSWORD: $POSTGRES_PASSWORD

pruebas:
  extends: .php
  stage: pruebas
  services:
    - postgres:16
  script:
    - php artisan migrate --force
    - php artisan test

El servicio es un PostgreSQL desechable · se crea para el trabajo y desaparece al terminar

UAS - Taller Integrador de Especialización

Los secretos también en CI

En el YAML

DB_PASSWORD: clave123 · queda en el repositorio y en cada copia

En variables de CI/CD

Masked se oculta en el log · Protected solo en ramas protegidas

UAS - Taller Integrador de Especialización

El pipeline del proyecto

calidad   ── estilo     → vendor/bin/pint --test
          └─ secretos   → detección de secretos sobre todo el historial
pruebas   ── pruebas    → migrate · rollback · migrate · php artisan test
                          contra un PostgreSQL efímero

El runner usa imágenes de contenedor — construirlas es tema del Bloque IV

UAS - Taller Integrador de Especialización

Leer un trabajo en rojo

$ php artisan test
   FAIL  Tests\Feature\Repositories\EloquentPrestamoRepositoryTest
  x detecta traslape de horarios with data set "contenido dentro del existente"
  Failed asserting that false is identical to true.

Tests:  1 failed, 7 passed
ERROR: Job failed: exit code 1
Qué trabajo

pruebas

Qué caso

El nombre del data set

Qué esperaba

true y obtuvo false

Salida simplificada · lee de abajo hacia arriba hasta encontrar la primera falla

UAS - Taller Integrador de Especialización

El pipeline decide

Rama main protegida

Nadie hace push directo

Pipelines must succeed

Un MR en rojo no se integra

En GitLab Free las aprobaciones no bloquean · nadie integra su propio MR sin aprobación por acuerdo del equipo

UAS - Taller Integrador de Especialización

El experimento

1

Cambien < por <= en la consulta de traslape

2

Abran un MR

3

El pipeline queda en rojo y el botón Merge se deshabilita

UAS - Taller Integrador de Especialización

Lo que la máquina no revisa

El pipeline sí

Estilo · pruebas · migraciones · secretos

La revisión humana sí

Si la lógica está en la capa correcta · si el nombre refleja el dominio · si falta un caso

Ninguna sustituye a la otra · STD-CI §8

UAS - Taller Integrador de Especialización

Calidad del repositorio

Elemento Qué muestra
Historial Conventional Commits legibles
Issues Cada historia o defecto con su issue
Merge requests Closes #N · revisión con severidad · pipeline verde
Documentación README · docs/api · docs/adr · CHANGELOG
UAS - Taller Integrador de Especialización

Scrum en este bloque

Sprint Temas Historias
A Datos y API La historia principal expuesta por API
B Pruebas y seguridad Reglas de negocio y permisos
C Integración continua Prioritarias restantes y v0.2.0

Los temas técnicos son el medio · las historias prioritarias de tu backlog son el fin

Actividad A7 — Planning Review y Retrospective de cada sprint

UAS - Taller Integrador de Especialización

Qué entregas al cierre

Funcionalidad

Historias prioritarias con DoD sobre PostgreSQL y API v1

Calidad

Suite en verde y pipeline que bloquea

Seguridad

Token Policies límites y secretos fuera

Repositorio

ADR-001 ADR-002 docs CHANGELOG y v0.2.0

UAS - Taller Integrador de Especialización

Volviendo al problema

Documentado

Como issue

Demostrado

Por una prueba que falló

Corregido

En un solo componente

Vigilado

Por el pipeline en cada cambio

UAS - Taller Integrador de Especialización

Siguiente bloque

Contenedores despliegue y escalabilidad

El mismo sistema empaquetado y ejecutado de forma reproducible

UAS - Taller Integrador de Especialización

estado: BORRADOR v2 fuentes: - bloque03/CONTEXTO_UNIDAD.md - bloque03/material/bloque03_manual_estudiante.md - bloque03/material/bloque03_actividades.md - bloque03/laboratorios/bloque03_lab01 a lab05 (solo para mantener consistentes nombres y código) - bloque03/referencias/bloque03_materiales_referencias.md - bloque02/material/bloque02_presentacion.md (continuidad de estilo de ReservaFIC y de .gitignore) - recursos/estandares/08-ESTANDAR-PRUEBAS.md §4–5 y 09-ESTANDAR-SEGURIDAD.md §2 propósito: Presentación de apoyo docente y estudiante del Bloque III criterios: - lenguaje humano y directo - sin punto final ni comas en el cuerpo de las diapositivas (mismo criterio que Bloques I y II) - hilo conductor: el defecto de traslape del Bloque II (CONTEXTO_UNIDAD §10 y H-001) - GitLab y merge request (D-003); PostgreSQL (D-007) - reglas del handbook citadas por ID - no introducir Docker ni despliegue (Bloque IV) - no asignar ponderaciones no validadas - la presentación aborda los temas; no anuncia ni numera laboratorios (los laboratorios viven en bloque03/laboratorios/) - cada tema avanza de lo conceptual a lo práctico: idea → ejemplo mínimo → ejemplo en ReservaFIC → errores frecuentes cambios v2 (2026-10-25): - se retiraron las diapositivas de portada de Laboratorio 1 a 5 y las menciones a laboratorios en el cuerpo - se agregaron secuencias concepto → práctica para pruebas unitarias, datos de prueba, manejo de errores y situaciones no esperadas, principios de seguridad, gestión de secretos (retomando .gitignore del Bloque II) e integración continua