Зачем: 156 зелёных тестов на fakeDatabase не поймали, что ключевые сценарии (создание сотрудника, подтверждение смены, расчёт зарплаты) всегда падают на реальной PostgreSQL. Нужен приёмочный стенд, который проверяет продукт как чёрный ящик: настоящий процесс сервера, настоящая БД, роли как в pilot, но с минимальными привилегиями. - tests/e2e/harness/global-setup.ts: создаёт БД и роли через scripts/test/provision-pg.mjs, применяет миграции ролью krona_migrator (NOSUPERUSER), поднимает `tsx src/server.ts` под krona_staff_app с токенами/VAPID из crypto.randomBytes; три процесса: с токенами, без токенов (как корневой compose.yaml) и отдельный для проверки rate limit; логи серверов пишутся в tests/e2e/evidence/. - *.e2e.ts и отдельный vitest.e2e.config.ts: e2e не попадают в `npm test`, поэтому `npm run check` не требует БД и не краснеет от известных дефектов. - [HARNESS]/[CTRL] — позитивные контроли (версия PG >= 17, атрибуты ролей, app-роль не видит schema_migrations, RBAC, уволенный теряет доступ): если они красные, красные дефектные сценарии ничего не доказывают. - scripts/test/run-e2e.mjs: «храповик» известных дефектов по tests/e2e/known-defects.json (новый красный или исправленный-но-не- вычеркнутый ID = exit 1), таблица e2e-table.md и проверка, что в evidence нет ничего похожего на секреты стенда. Логи названы *.txt, т.к. *.log в .gitignore.
119 lines
5.7 KiB
TypeScript
119 lines
5.7 KiB
TypeScript
// [HARNESS] Позитивный контроль стенда: эти тесты обязаны быть зелёными всегда.
|
|
// Если они красные — сломан сам стенд, и красные дефектные сценарии ничего не доказывают.
|
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
import pg from 'pg';
|
|
import { describe, expect, it } from 'vitest';
|
|
import { api, ctx, describeResponse } from './harness/client.js';
|
|
|
|
describe('HARNESS: реальный сервер + PostgreSQL 17', () => {
|
|
it('[HARNESS] сервер отвечает live/ready (ready проверяет БД)', async () => {
|
|
const live = await api('GET', '/live');
|
|
expect(live.status).toBe(200);
|
|
const ready = await api('GET', '/ready');
|
|
expect(ready.status, describeResponse(ready)).toBe(200);
|
|
expect(ready.body).toEqual({ status: 'ready' });
|
|
});
|
|
|
|
it('[HARNESS] БД: PostgreSQL 17, приложение под ролью без SUPERUSER/BYPASSRLS, мигратор без SUPERUSER', async () => {
|
|
const client = new pg.Client({ connectionString: ctx().appDatabaseUrl });
|
|
await client.connect();
|
|
try {
|
|
const version = await client.query<{ server_version_num: string }>('SHOW server_version_num');
|
|
expect(Number(version.rows[0]!.server_version_num)).toBeGreaterThanOrEqual(170000);
|
|
const roles = await client.query<{
|
|
rolname: string;
|
|
rolsuper: boolean;
|
|
rolbypassrls: boolean;
|
|
rolcreaterole: boolean;
|
|
}>(
|
|
`SELECT rolname, rolsuper, rolbypassrls, rolcreaterole FROM pg_roles
|
|
WHERE rolname IN (current_user, 'krona_migrator') ORDER BY rolname`,
|
|
);
|
|
expect(roles.rows).toEqual([
|
|
{ rolname: 'krona_migrator', rolsuper: false, rolbypassrls: false, rolcreaterole: false },
|
|
{ rolname: 'krona_staff_app', rolsuper: false, rolbypassrls: false, rolcreaterole: false },
|
|
]);
|
|
} finally {
|
|
await client.end();
|
|
}
|
|
});
|
|
|
|
it('[HARNESS] миграции применены мигратором, app-роль не видит schema_migrations', async () => {
|
|
const migrator = new pg.Client({ connectionString: ctx().migrationDatabaseUrl });
|
|
await migrator.connect();
|
|
try {
|
|
const applied = await migrator.query<{ count: string }>(
|
|
'SELECT count(*)::text AS count FROM schema_migrations',
|
|
);
|
|
expect(Number(applied.rows[0]!.count)).toBeGreaterThanOrEqual(11);
|
|
} finally {
|
|
await migrator.end();
|
|
}
|
|
const app = new pg.Client({ connectionString: ctx().appDatabaseUrl });
|
|
await app.connect();
|
|
try {
|
|
await expect(app.query('SELECT 1 FROM schema_migrations')).rejects.toThrow(
|
|
/permission denied/,
|
|
);
|
|
} finally {
|
|
await app.end();
|
|
}
|
|
});
|
|
|
|
it('[HARNESS] токены: без токена 401, случайный токен 401, manager-токен 200', async () => {
|
|
expect((await api('GET', '/portal')).status).toBe(401);
|
|
expect((await api('GET', '/portal', { token: 'x'.repeat(64) })).status).toBe(401);
|
|
const ok = await api('GET', '/portal', { token: ctx().managerToken });
|
|
expect(ok.status, describeResponse(ok)).toBe(200);
|
|
expect(ok.body.viewer.role).toBe('manager');
|
|
});
|
|
|
|
it('[HARNESS] терминал: отметка сотрудника из импорта принимается (201)', async () => {
|
|
const { importEmployees, clock, daysAgo } = await import('./harness/fixtures.js');
|
|
const [employee] = await importEmployees(1);
|
|
const response = await clock(employee!.id, 'check_in', daysAgo(1));
|
|
expect(response.status, describeResponse(response)).toBe(201);
|
|
});
|
|
|
|
// [CTRL] Позитивные контроли безопасности, которые аудит признал рабочими: они
|
|
// должны оставаться зелёными и доказывают, что сценарии умеют быть зелёными.
|
|
it('[CTRL] RBAC: терминал не видит портал и не создаёт сотрудников; сотрудник видит только себя', async () => {
|
|
const { importEmployees, issueEmployeeToken } = await import('./harness/fixtures.js');
|
|
const [self, other] = await importEmployees(2);
|
|
const employeeToken = await issueEmployeeToken(self!.id);
|
|
const terminalPortal = await api('GET', '/portal', { token: ctx().terminalToken });
|
|
const terminalCreate = await api('POST', '/employees', {
|
|
token: ctx().terminalToken,
|
|
body: { personnel_number: 'E2E-CTRL', display_name: 'E2E-CTRL' },
|
|
});
|
|
const own = await api('GET', '/portal', { token: employeeToken });
|
|
const rows = (own.body?.employees ?? []) as Array<{ id: string }>;
|
|
const ids = rows.map((row) => row.id);
|
|
expect({
|
|
terminalPortal: terminalPortal.status,
|
|
terminalCreate: terminalCreate.status,
|
|
own: own.status,
|
|
seesSelf: ids.includes(self!.id),
|
|
seesOther: ids.includes(other!.id),
|
|
}).toEqual({
|
|
terminalPortal: 403,
|
|
terminalCreate: 403,
|
|
own: 200,
|
|
seesSelf: true,
|
|
seesOther: false,
|
|
});
|
|
});
|
|
|
|
it('[CTRL] уволенный сотрудник теряет доступ (401)', async () => {
|
|
const { importEmployees, issueEmployeeToken } = await import('./harness/fixtures.js');
|
|
const [employee] = await importEmployees(1);
|
|
const employeeToken = await issueEmployeeToken(employee!.id);
|
|
expect((await api('GET', '/portal', { token: employeeToken })).status).toBe(200);
|
|
const dismiss = await api('PATCH', `/employees/${employee!.id}`, {
|
|
token: ctx().managerToken,
|
|
body: { status: 'dismissed' },
|
|
});
|
|
expect(dismiss.status, describeResponse(dismiss)).toBe(200);
|
|
expect((await api('GET', '/portal', { token: employeeToken })).status).toBe(401);
|
|
});
|
|
});
|