test(e2e): add smoke tests for CI/CD post-deploy validation
- Add smoke.spec.ts with 11 production-ready tests - Configure playwright for BASE_URL environment variable - Tests verify: login page, register, activation, API endpoints - Validate business rules: 10 subjects, 5 professors, 3 credits Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a8e4b8090a
commit
1318b4dd1b
|
|
@ -0,0 +1,171 @@
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smoke Tests E2E para CI/CD
|
||||||
|
*
|
||||||
|
* Estos tests verifican funcionalidades críticas contra el servidor real.
|
||||||
|
* Se ejecutan después del deploy para validar que el sistema funciona.
|
||||||
|
*
|
||||||
|
* Ejecutar: BASE_URL=https://academia.ingeniumcodex.com npx playwright test smoke.spec.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
test.describe('Smoke Tests - Páginas Públicas', () => {
|
||||||
|
test('página de login carga correctamente', async ({ page }) => {
|
||||||
|
await page.goto('/login');
|
||||||
|
|
||||||
|
// Verificar que la página cargó
|
||||||
|
await expect(page).toHaveTitle(/estudiante|academia|login/i);
|
||||||
|
|
||||||
|
// Verificar elementos básicos del formulario
|
||||||
|
await expect(page.locator('input[type="text"], input[name="username"]').first()).toBeVisible();
|
||||||
|
await expect(page.locator('input[type="password"]').first()).toBeVisible();
|
||||||
|
await expect(page.locator('button[type="submit"]').first()).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('página de registro carga correctamente', async ({ page }) => {
|
||||||
|
await page.goto('/register');
|
||||||
|
|
||||||
|
// Verificar elementos del formulario de registro
|
||||||
|
await expect(page.locator('input').first()).toBeVisible();
|
||||||
|
await expect(page.locator('button[type="submit"]').first()).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('página de activación acepta código en URL', async ({ page }) => {
|
||||||
|
await page.goto('/activate?code=TEST123');
|
||||||
|
|
||||||
|
// Debe mostrar algún contenido (error o formulario)
|
||||||
|
await expect(page.locator('body')).not.toBeEmpty();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('redirección a login si no autenticado', async ({ page }) => {
|
||||||
|
// Limpiar storage
|
||||||
|
await page.goto('/');
|
||||||
|
await page.evaluate(() => localStorage.clear());
|
||||||
|
|
||||||
|
// Intentar acceder a ruta protegida
|
||||||
|
await page.goto('/dashboard');
|
||||||
|
|
||||||
|
// Debe redirigir a login
|
||||||
|
await page.waitForURL(/\/login/, { timeout: 10000 });
|
||||||
|
expect(page.url()).toContain('/login');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Smoke Tests - API GraphQL', () => {
|
||||||
|
test('endpoint GraphQL responde', async ({ request, baseURL }) => {
|
||||||
|
const response = await request.post(`${baseURL?.replace(':4200', ':5000') || 'http://localhost:5000'}/graphql`, {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
data: {
|
||||||
|
query: '{ __typename }',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body.data).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('query de materias retorna datos', async ({ request, baseURL }) => {
|
||||||
|
const apiUrl = baseURL?.includes('localhost:4200')
|
||||||
|
? 'http://localhost:5000/graphql'
|
||||||
|
: baseURL?.replace(/\/$/, '') + '/graphql';
|
||||||
|
|
||||||
|
const response = await request.post(apiUrl, {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
data: {
|
||||||
|
query: '{ subjects { id name credits } }',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body.data?.subjects).toBeDefined();
|
||||||
|
expect(body.data.subjects.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('query de profesores retorna datos', async ({ request, baseURL }) => {
|
||||||
|
const apiUrl = baseURL?.includes('localhost:4200')
|
||||||
|
? 'http://localhost:5000/graphql'
|
||||||
|
: baseURL?.replace(/\/$/, '') + '/graphql';
|
||||||
|
|
||||||
|
const response = await request.post(apiUrl, {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
data: {
|
||||||
|
query: '{ professors { id name } }',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body.data?.professors).toBeDefined();
|
||||||
|
expect(body.data.professors.length).toBe(5); // 5 profesores según reglas de negocio
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Smoke Tests - Health Check', () => {
|
||||||
|
test('health endpoint responde healthy', async ({ request, baseURL }) => {
|
||||||
|
const healthUrl = baseURL?.includes('localhost:4200')
|
||||||
|
? 'http://localhost:5000/health'
|
||||||
|
: baseURL?.replace(/\/$/, '') + '/health';
|
||||||
|
|
||||||
|
const response = await request.get(healthUrl);
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body.status).toBe('Healthy');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Smoke Tests - Validaciones de Negocio', () => {
|
||||||
|
test('debe existir exactamente 10 materias', async ({ request, baseURL }) => {
|
||||||
|
const apiUrl = baseURL?.includes('localhost:4200')
|
||||||
|
? 'http://localhost:5000/graphql'
|
||||||
|
: baseURL?.replace(/\/$/, '') + '/graphql';
|
||||||
|
|
||||||
|
const response = await request.post(apiUrl, {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
data: {
|
||||||
|
query: '{ subjects { id } }',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body.data.subjects.length).toBe(10); // 10 materias según reglas
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cada materia debe tener 3 créditos', async ({ request, baseURL }) => {
|
||||||
|
const apiUrl = baseURL?.includes('localhost:4200')
|
||||||
|
? 'http://localhost:5000/graphql'
|
||||||
|
: baseURL?.replace(/\/$/, '') + '/graphql';
|
||||||
|
|
||||||
|
const response = await request.post(apiUrl, {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
data: {
|
||||||
|
query: '{ subjects { credits } }',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const body = await response.json();
|
||||||
|
const allThreeCredits = body.data.subjects.every((s: { credits: number }) => s.credits === 3);
|
||||||
|
expect(allThreeCredits).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cada profesor debe tener exactamente 2 materias', async ({ request, baseURL }) => {
|
||||||
|
const apiUrl = baseURL?.includes('localhost:4200')
|
||||||
|
? 'http://localhost:5000/graphql'
|
||||||
|
: baseURL?.replace(/\/$/, '') + '/graphql';
|
||||||
|
|
||||||
|
const response = await request.post(apiUrl, {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
data: {
|
||||||
|
query: '{ professors { id subjects { id } } }',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const body = await response.json();
|
||||||
|
const allTwoSubjects = body.data.professors.every(
|
||||||
|
(p: { subjects: unknown[] }) => p.subjects.length === 2
|
||||||
|
);
|
||||||
|
expect(allTwoSubjects).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,16 +1,23 @@
|
||||||
import { defineConfig, devices } from '@playwright/test';
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
// URL base configurable via variable de entorno (para CI/CD)
|
||||||
|
const baseURL = process.env['BASE_URL'] || 'http://localhost:4200';
|
||||||
|
const isCI = !!process.env['CI'];
|
||||||
|
const isProduction = baseURL.includes('https://');
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: './e2e',
|
testDir: './e2e',
|
||||||
fullyParallel: true,
|
fullyParallel: true,
|
||||||
forbidOnly: !!process.env['CI'],
|
forbidOnly: isCI,
|
||||||
retries: process.env['CI'] ? 2 : 0,
|
retries: isCI ? 2 : 0,
|
||||||
workers: process.env['CI'] ? 1 : undefined,
|
workers: isCI ? 1 : undefined,
|
||||||
reporter: 'html',
|
reporter: isCI ? [['html'], ['list']] : 'html',
|
||||||
|
timeout: isProduction ? 60000 : 30000,
|
||||||
use: {
|
use: {
|
||||||
baseURL: 'http://localhost:4200',
|
baseURL,
|
||||||
trace: 'on-first-retry',
|
trace: 'on-first-retry',
|
||||||
screenshot: 'only-on-failure',
|
screenshot: 'only-on-failure',
|
||||||
|
video: isCI ? 'retain-on-failure' : 'off',
|
||||||
},
|
},
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
|
|
@ -18,10 +25,15 @@ export default defineConfig({
|
||||||
use: { ...devices['Desktop Chrome'] },
|
use: { ...devices['Desktop Chrome'] },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
webServer: {
|
// Solo iniciar servidor local si no es producción
|
||||||
command: 'npm run start',
|
...(isProduction
|
||||||
url: 'http://localhost:4200',
|
? {}
|
||||||
reuseExistingServer: !process.env['CI'],
|
: {
|
||||||
timeout: 120000,
|
webServer: {
|
||||||
},
|
command: 'npm run start',
|
||||||
|
url: 'http://localhost:4200',
|
||||||
|
reuseExistingServer: !isCI,
|
||||||
|
timeout: 120000,
|
||||||
|
},
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue