home
  • Spanish (ES)
  • English (UK)
  • Portuguese (PT)
  • star Featured
  • Hermes Agent
  • draw UX Projects
  • person About me
  • mail Contact
arrow_back Back to project Home

Project testing

Testing · Task Cloud

5 markdown files (overview, unit, integration, e2e, GitHub Actions).

On this page Testing · visión 0% expand_more
Testing · visión Testing · unitarios PIN (apps/api/src/auth/pin.util.spec.ts) JWT (apps/api/src/auth/auth.service.spec.ts) Ownership (apps/api/src/users/users.service.spec.ts) Guard (apps/api/src/auth/auth.guard.spec.ts) Web — filtro de estado (apps/web/src/app/core/services/task.service.spec.ts) Testing · integración Fake de base de datos Contratos que se afirman Qué no cubre esta capa Testing · e2e Specs reales Testing · GitHub Actions Job quality Job e2e Por qué no hay errores de entorno

12-testing.md

Testing · visión

Task Cloud tiene tres capas de prueba más un job de GitHub Actions. Todas se pueden correr en local y en CI sin Neon real (excepto el smoke HTTP opcional pnpm test:api).

CapaRunnerQué cubreComando
Unitario webJasmine + Karma (ChromeHeadless)Componentes y servicios Angularpnpm test:ci
Unitario APIVitestPIN, JWT, guards, ownershippnpm test:api
Integración APIVitest + Supertest + @nestjs/testingHTTP real contra Nest, Neon mockeadopnpm test:integration
E2EPlaywright (Chromium)PWA: título, filtro, tab Optionspnpm test:e2e
Smoke Neon (opcional)apps/api/scripts/test-api.mjsAPI viva + DATABASE_URLpnpm --filter @task-cloud/api test:api

CI (.github/workflows/ci.yml):

  1. Job quality — lint, unit web, unit API, integración API, build:api.
  2. Job e2e — instala Chromium y lanza Playwright (el webServer arranca Angular con CI=true).

No hace falta un Postgres en Actions: la integración sustituye DatabaseService por un fake. El e2e no depende de que /api responda para pintar el header.

Siguientes documentos: unitarios · integración · e2e · CI.

13-testing-unit.md

Testing · unitarios

Los unitarios instancian la clase sin HTTP y sin Neon. En la API se usa Vitest (globals: true). En la web, Jasmine/Karma.

PIN (apps/api/src/auth/pin.util.spec.ts)

El lookup del PIN es HMAC-SHA256 con PIN_PEPPER. El hash persistido es bcrypt, con un fallback SHA-256 de la era anterior.

it('accepts exactly 8 digits', () => {
  expect(isValidPin('12345678')).toBe(true);
  expect(isValidPin('1234567')).toBe(false);
  expect(isValidPin('1234567a')).toBe(false);
});

it('verifies a bcrypt PIN hash', async () => {
  const hash = await hashPin('87654321');
  expect(await verifyPinHash('87654321', hash)).toBe(true);
  expect(await verifyPinHash('00000000', hash)).toBe(false);
});

JWT (apps/api/src/auth/auth.service.spec.ts)

AuthService se construye a mano con un ConfigService falso. Así se firma y se verifica un token sin Nest.

const token = await auth.createAccessToken({
  userId: 42,
  sessionId: 'sid-1',
  expiresAt: new Date(Date.now() + 60_000),
});
const claims = await auth.verifyAccessToken(token);
expect(claims.userId).toBe(42);
expect(claims.sessionId).toBe('sid-1');

extractBearerToken solo acepta el esquema Bearer:

expect(auth.extractBearerToken('Bearer abc.def')).toBe('abc.def');
expect(auth.extractBearerToken('Basic nope')).toBe('');

Ownership (apps/api/src/users/users.service.spec.ts)

Si el actorId no coincide, el servicio lanza NotFoundException (no 403: no se confirma que el recurso existe).

it('hides another user behind 404', () => {
  expect(() => users.getOwnUser(1, 99)).toThrow(NotFoundException);
});

El mismo criterio está en TasksService.updateTaskForUser / deleteTaskForUser.

Guard (apps/api/src/auth/auth.guard.spec.ts)

El Reflector decide si la ruta es @Public(). Sin token y sin @Public(), el guard responde 401.

it('lets public handlers through', async () => {
  const reflector = { getAllAndOverride: () => true } as unknown as Reflector;
  const guard = new AuthGuard(auth, reflector);
  await expect(guard.canActivate(ctx())).resolves.toBe(true);
});

Web — filtro de estado (apps/web/src/app/core/services/task.service.spec.ts)

Ionic Storage se sustituye por un Map. changeFilter() recorre All → Done → Pending.

expect(service.filter()).toBe(StatusEnum.All);
service.changeFilter();
expect(service.filter()).toBe(StatusEnum.Done);

Al guardar, una tarea sin priority queda en medium y tags en []. Los tags se recortan y pasan a minúsculas (" Work " → "work"). El filtro hidrata Ionic Storage:

store.set('filter', StatusEnum.Done);
await service.init();
expect(service.filter()).toBe(StatusEnum.Done);

TabListPage (Jasmine) cubre búsqueda por título/descripcion/tag, el overlay de crear y que el chip de prioridad no se pinte como badge.

14-testing-integration.md

Testing · integración

Archivo: apps/api/test/app.integration.spec.ts.
Comando: pnpm test:integration.

Levanta Nest de verdad (Test.createTestingModule + configureApp) y habla por HTTP con Supertest. Neon no entra: se hace overrideProvider(DatabaseService).

Antes de importar AppModule se fuerzan las env que validateEnv exige al boot (apps/api/test/setup-env.ts). No se usa ||=: un JWT corto heredado del .env local rompería el arranque.

process.env.DATABASE_URL = 'postgresql://task:cloud@localhost/taskcloud';
process.env.JWT_SECRET = 'integration-jwt-secret-32-chars-ok';
process.env.PIN_PEPPER = 'integration-pin-pepper-32-chars-ok';
process.env.ALLOWED_ORIGINS = 'http://127.0.0.1:4200';

Vitest transpila los decoradores de Nest con SWC (unplugin-swc + decoratorMetadata: true en vitest.integration.config.ts). Sin metadata, Reflector y DatabaseService llegan undefined al TestingModule.

Fake de base de datos

const fakeDb = {
  getSql: () => Object.assign(async () => [], {}) as never,
  ping: async () => undefined,
  ensureAuthSchema: async () => undefined,
};

configureApp aplica el mismo ValidationPipe, filtro, helmet y request-id que producción. Si un test no llama a configureApp, el prefix /api y el pipe no existen — ese es el fallo clásico de e2e Nest.

Contratos que se afirman

it('GET /api/health returns the product payload', async () => {
  const res = await request(app.getHttpServer()).get('/api/health').expect(200);
  expect(res.body).toEqual({ ok: true, service: 'task-cloud-nest-api' });
  expect(res.headers['x-request-id']).toBeTruthy();
});

it('rejects an unauthenticated task list', async () => {
  await request(app.getHttpServer()).get('/api/tasks').expect(401);
});

it('rejects a short PIN at the DTO boundary', async () => {
  await request(app.getHttpServer())
    .post('/api/auth/login')
    .send({ pin: '12' })
    .expect(400);
});

it('rejects extra fields on login (forbidNonWhitelisted)', async () => {
  await request(app.getHttpServer())
    .post('/api/auth/login')
    .send({ pin: '12345678', admin: true })
    .expect(400);
});

Un PIN válido pero sin filas en el fake ([]) produce 401 — el login llega al servicio, no se queda en el DTO.

También se afirma GET /api (mismo payload que health), GET /api/auth/me y POST /api/tasks sin Bearer (401), y el header Helmet x-content-type-options: nosniff.

Qué no cubre esta capa

El smoke apps/api/scripts/test-api.mjs sí habla con Neon real (register → tasks → logout). No corre en GitHub Actions para no depender de secretos. Úsalo en local con pnpm api + pnpm --filter @task-cloud/api test:api.

15-testing-e2e.md

Testing · e2e

Playwright en la raíz del monorepo. Config: playwright.config.ts. Specs: e2e/home.spec.ts.

pnpm test:e2e

En CI, Playwright arranca Angular solo:

webServer: {
  command: 'pnpm --filter @task-cloud/web start',
  url: 'http://127.0.0.1:4200',
  env: { CI: 'true', API_BASE_URL: '/api' },
}

CI=true hace que apps/web/scripts/set-env.js genere environment.local.ts sin copiar .env (no hay secretos en Actions).

Specs reales

async function openPwa(page: Page) {
  await page.goto('/');
  await expect(page.locator('ion-tab-bar')).toBeVisible({ timeout: 20_000 });
}

test('renders the product title in the header', async ({ page }) => {
  await openPwa(page);
  await expect(page.locator('h1.header-title')).toContainText(/Task Cloud/i);
});

test('shows the All filter control', async ({ page }) => {
  await openPwa(page);
  await expect(page.locator('.filter-button')).toBeVisible();
});

test('can open the Options tab', async ({ page }) => {
  await openPwa(page);
  await page.locator('ion-tab-button[tab="options"]').click();
  await expect(page).toHaveURL(/options/i);
  await expect(page.locator('app-tab-options h1')).toContainText(/settings|configuracion/i);
});

test('creates a local task from the overlay form', async ({ page }) => {
  await openPwa(page);
  const form = page.locator('form.task-form');
  await form.locator('input[matInput]').first().fill('E2E milk');
  await form.evaluate((el) => (el as HTMLFormElement).requestSubmit());
  await expect(page.locator('.task-title').filter({ hasText: 'E2E milk' })).toBeVisible();
});

Ionic no expone role="tab" de forma fiable en ion-tab-button, por eso el locator usa el atributo tab="options". El título se afirma con h1.header-title (el h1 dentro de ion-toolbar no siempre entra en el árbol de heading).

La creación e2e es offline: rellena el overlay de tab-list y afirma .task-title. No hace falta Neon.

Esta suite también pilla regresiones de arranque:

  • provideTranslateHttpLoader() va en providers de AppModule, no en TranslateModule.forRoot({ loader }). Si no, NG0201: No provider found for TranslateLoader.
  • En pnpm, preserveSymlinks: true en angular.json + PathLocationStrategy evitan NG0203 al inyectar LocationStrategy (la PWA quedaba en <ion-app></ion-app> vacío).

La PWA pinta lista y opciones offline. No se afirma sync/PIN en e2e de CI (eso es el smoke Neon).

16-testing-ci.md

Testing · GitHub Actions

Workflow: .github/workflows/ci.yml. Se dispara en push y pull_request a main.

Job quality

- name: Lint web
  run: pnpm lint:ci
- name: Run web unit tests
  run: pnpm test:ci
- name: Run API unit tests
  run: pnpm test:api
- name: Run API integration tests
  run: pnpm test:integration
- name: Build Nest API
  run: pnpm build:api

pnpm/action-setup@v4 lee packageManager: pnpm@11.21.0. Node 22 + cache de pnpm. pnpm install --frozen-lockfile falla si el lock no está commiteado.

Job e2e

- name: Install Playwright Chromium
  run: pnpm exec playwright install --with-deps chromium
- name: Run end-to-end tests
  run: pnpm test:e2e
  env:
    CI: true
    API_BASE_URL: /api

Chromium se instala en el runner; no se sube al repo. retries: 2 solo en CI (playwright.config.ts).

Por qué no hay errores de entorno

RiesgoMitigación
validateEnv exige secretostest/setup-env.ts asigna JWT/PIN de 32+ caracteres antes de importar AppModule
Decoradores Nest sin metadataunplugin-swc con decoratorMetadata: true en vitest.integration.config.ts
Build nativo de SWCpnpm-workspace.yaml → allowBuilds["@swc/core"] = true
Neon no está en ActionsDatabaseService mockeado
set-env.js pide .envCI=true genera environments y sale 0
Angular tarda en servirwebServer.timeout: 180000 y toBeVisible({ timeout: 20000 })
Lockfile desfasado--frozen-lockfile + commit de pnpm-lock.yaml

Si un job rojo es flaky de Playwright, el primer sitio a mirar es el locator de Ionic (preferir ion-tab-button[tab=…] a getByRole('tab')).

On this page

Testing · visión Testing · unitarios PIN (apps/api/src/auth/pin.util.spec.ts) JWT (apps/api/src/auth/auth.service.spec.ts) Ownership (apps/api/src/users/users.service.spec.ts) Guard (apps/api/src/auth/auth.guard.spec.ts) Web — filtro de estado (apps/web/src/app/core/services/task.service.spec.ts) Testing · integración Fake de base de datos Contratos que se afirman Qué no cubre esta capa Testing · e2e Specs reales Testing · GitHub Actions Job quality Job e2e Por qué no hay errores de entorno

0% read

folder_zip Download all (ZIP)
Hermes Agent