krona-staff/tests/e2e/harness/client.ts
grok d7b235248a test(e2e): PILOT-REHOME — приёмка будущего scripts/rehome-tenant.mjs
Почему: TASK-GROK-3 п.5 / TASK-CODEX-2 п.2.2 — данные пилота живут под заглушкой
01J00000000000000000000000, перенос на 01M2R6AC5652NPT3MG34DKN4ZP нельзя проверить руками.
[PILOT-REHOME-SEED] (должен быть зелёным): таблицы с tenant_id берутся из information_schema,
в каждую сеется 2+ строки заглушки и постороннего арендатора; новая таблица без сида → красный.
[PILOT-REHOME]: --dry-run печатает те же числа и ничего не меняет; перенос админом БД; под
заглушкой 0 строк, счётчики и SUM(amount_minor) ledger совпадают, посторонний не тронут, запись
в staff_audit_log нового арендатора, krona_staff_app видит тех же сотрудников под новым tenant и
никого под старым, UPDATE staff_payroll_ledger отклоняется, повторный запуск отказывает.
Пока скрипта нет — красный с понятным сообщением, ID в known-defects.json.
Сценарий проверен временным эталоном (не коммитился): зелёный; мутации без аудита / без отказа
на повтор / с пропущенной таблицей / с записью в dry-run — красные (report-evidence/round3-pilot-rehome.txt).
client.ts: dbQueryAs(tenant, …) — запрос ролью приложения под заданным арендатором.
2026-09-26 16:10:07 +07:00

94 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// HTTP-клиент e2e. Тело ответа типизировано как `any` намеренно: тесты проверяют
// внешний контракт JSON, а не типы src/. Правила no-unsafe-* отключены только здесь
// и в файлах сценариев.
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment */
import pg from 'pg';
import { inject } from 'vitest';
import type { E2EContext } from './context.js';
export interface ApiResponse {
status: number;
body: any;
text: string;
headers: Headers;
}
export interface RequestOptions {
token?: string | undefined;
body?: unknown;
headers?: Record<string, string>;
base?: 'secured' | 'open' | 'ratelimit';
}
export function ctx(): E2EContext {
return inject('e2e');
}
export async function api(
method: string,
path: string,
options: RequestOptions = {},
): Promise<ApiResponse> {
const context = ctx();
const base =
options.base === 'open'
? context.openBaseUrl
: options.base === 'ratelimit'
? context.rateLimitBaseUrl
: context.baseUrl;
const headers: Record<string, string> = { ...(options.headers ?? {}) };
if (options.token) headers.authorization = `Bearer ${options.token}`;
let payload: string | undefined;
if (options.body !== undefined) {
headers['content-type'] = 'application/json';
payload = JSON.stringify(options.body);
}
const url = path.startsWith('/api/') || path.startsWith('/staff') ? path : `/api/v1/staff${path}`;
const response = await fetch(`${base}${url}`, {
method,
headers,
...(payload === undefined ? {} : { body: payload }),
redirect: 'manual',
});
const text = await response.text();
let body: any = null;
try {
body = text ? JSON.parse(text) : null;
} catch {
body = null;
}
return { status: response.status, body, text, headers: response.headers };
}
/** Краткое описание ответа для сообщений assert (без токенов). */
export function describeResponse(response: ApiResponse): string {
const snippet = response.text.length > 300 ? `${response.text.slice(0, 300)}…` : response.text;
return `HTTP ${response.status} ${snippet}`;
}
/** Запрос к БД ролью приложения внутри tenant-транзакции (RLS действует). */
export async function dbQuery<T extends pg.QueryResultRow = Record<string, unknown>>(
sql: string,
params: unknown[] = [],
): Promise<T[]> {
return dbQueryAs<T>(ctx().tenantId, sql, params);
}
/** Запрос ролью приложения (krona_staff_app) под указанным арендатором (app.tenant_id). */
export async function dbQueryAs<T extends pg.QueryResultRow = Record<string, unknown>>(
tenantId: string,
sql: string,
params: unknown[] = [],
): Promise<T[]> {
const client = new pg.Client({ connectionString: ctx().appDatabaseUrl });
await client.connect();
try {
await client.query('BEGIN');
await client.query("SELECT set_config('app.tenant_id', $1, true)", [tenantId]);
const result = await client.query<T>(sql, params);
await client.query('COMMIT');
return result.rows;
} finally {
await client.end();
}
}