@verdeect/kit 0.0.0-stage → 1.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +132 -0
- package/README.md +804 -2
- package/alerts/verdeect-kit.rules.yml +133 -0
- package/dist/cli/commands/crypto.d.ts +9 -0
- package/dist/cli/commands/crypto.js +87 -0
- package/dist/cli/commands/db.d.ts +11 -0
- package/dist/cli/commands/db.js +151 -0
- package/dist/cli/commands/openapi.d.ts +9 -0
- package/dist/cli/commands/openapi.js +144 -0
- package/dist/cli/commands/rabbitmq.d.ts +11 -0
- package/dist/cli/commands/rabbitmq.js +124 -0
- package/dist/cli/commands/redis.d.ts +11 -0
- package/dist/cli/commands/redis.js +119 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +31 -0
- package/dist/config/cli-schemas.d.ts +29 -0
- package/dist/config/cli-schemas.js +15 -0
- package/dist/config/define-env.d.ts +21 -0
- package/dist/config/define-env.js +43 -0
- package/dist/config/fragments.d.ts +105 -0
- package/dist/config/fragments.js +86 -0
- package/dist/config/helpers.d.ts +26 -0
- package/dist/config/helpers.js +62 -0
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.js +7 -0
- package/dist/config/kit-config.module.d.ts +20 -0
- package/dist/config/kit-config.module.js +23 -0
- package/dist/config/parse-env.d.ts +14 -0
- package/dist/config/parse-env.js +28 -0
- package/dist/crypto/cipher.d.ts +61 -0
- package/dist/crypto/cipher.js +136 -0
- package/dist/crypto/index.d.ts +2 -0
- package/dist/crypto/index.js +2 -0
- package/dist/database/batches.d.ts +42 -0
- package/dist/database/batches.js +49 -0
- package/dist/database/db.d.ts +93 -0
- package/dist/database/db.js +234 -0
- package/dist/database/if-newer.d.ts +9 -0
- package/dist/database/if-newer.js +15 -0
- package/dist/database/index.d.ts +17 -0
- package/dist/database/index.js +13 -0
- package/dist/database/kit-database.module.d.ts +47 -0
- package/dist/database/kit-database.module.js +181 -0
- package/dist/database/migrations.d.ts +42 -0
- package/dist/database/migrations.js +142 -0
- package/dist/database/pg.d.ts +20 -0
- package/dist/database/pg.js +19 -0
- package/dist/database/prepare-database.d.ts +39 -0
- package/dist/database/prepare-database.js +75 -0
- package/dist/database/run-exclusive.d.ts +19 -0
- package/dist/database/run-exclusive.js +62 -0
- package/dist/database/tenant.d.ts +34 -0
- package/dist/database/tenant.js +28 -0
- package/dist/http/api/api.decorator.d.ts +18 -0
- package/dist/http/api/api.decorator.js +9 -0
- package/dist/http/api/openapi-document.d.ts +40 -0
- package/dist/http/api/openapi-document.js +452 -0
- package/dist/http/bootstrap-service.d.ts +33 -0
- package/dist/http/bootstrap-service.js +107 -0
- package/dist/http/create-http-app.d.ts +38 -0
- package/dist/http/create-http-app.js +137 -0
- package/dist/http/errors.d.ts +118 -0
- package/dist/http/errors.js +94 -0
- package/dist/http/index.d.ts +12 -0
- package/dist/http/index.js +8 -0
- package/dist/http/kit-exception.filter.d.ts +18 -0
- package/dist/http/kit-exception.filter.js +151 -0
- package/dist/http/kit-http-core.module.d.ts +19 -0
- package/dist/http/kit-http-core.module.js +122 -0
- package/dist/http/pagination.d.ts +30 -0
- package/dist/http/pagination.js +73 -0
- package/dist/http/zod-validation.pipe.d.ts +15 -0
- package/dist/http/zod-validation.pipe.js +47 -0
- package/dist/idempotency/idempotency.interceptor.d.ts +55 -0
- package/dist/idempotency/idempotency.interceptor.js +317 -0
- package/dist/idempotency/idempotent.decorator.d.ts +7 -0
- package/dist/idempotency/idempotent.decorator.js +16 -0
- package/dist/idempotency/index.d.ts +7 -0
- package/dist/idempotency/index.js +6 -0
- package/dist/idempotency/kit-idempotency.module.d.ts +47 -0
- package/dist/idempotency/kit-idempotency.module.js +173 -0
- package/dist/idempotency/peer.d.ts +1 -0
- package/dist/idempotency/peer.js +4 -0
- package/dist/idempotency/table.d.ts +203 -0
- package/dist/idempotency/table.js +86 -0
- package/dist/internal/cleanup-result.d.ts +7 -0
- package/dist/internal/cleanup-result.js +12 -0
- package/dist/internal/ids.d.ts +13 -0
- package/dist/internal/ids.js +41 -0
- package/dist/internal/metadata.d.ts +12 -0
- package/dist/internal/metadata.js +3 -0
- package/dist/internal/outbox-policy.d.ts +8 -0
- package/dist/internal/outbox-policy.js +10 -0
- package/dist/internal/peer.d.ts +16 -0
- package/dist/internal/peer.js +56 -0
- package/dist/internal/pg-errors.d.ts +21 -0
- package/dist/internal/pg-errors.js +115 -0
- package/dist/internal/preparsed-env.d.ts +4 -0
- package/dist/internal/preparsed-env.js +14 -0
- package/dist/internal/rabbitmq-definitions.d.ts +85 -0
- package/dist/internal/rabbitmq-definitions.js +73 -0
- package/dist/internal/redis-acl.d.ts +21 -0
- package/dist/internal/redis-acl.js +30 -0
- package/dist/internal/service-name.d.ts +4 -0
- package/dist/internal/service-name.js +10 -0
- package/dist/internal/sql-identifier.d.ts +5 -0
- package/dist/internal/sql-identifier.js +12 -0
- package/dist/internal/tokens.d.ts +10 -0
- package/dist/internal/tokens.js +12 -0
- package/dist/internal/tx-context.d.ts +5 -0
- package/dist/internal/tx-context.js +9 -0
- package/dist/messaging/cleanup.d.ts +49 -0
- package/dist/messaging/cleanup.js +129 -0
- package/dist/messaging/config.d.ts +43 -0
- package/dist/messaging/config.js +8 -0
- package/dist/messaging/define-event.d.ts +29 -0
- package/dist/messaging/define-event.js +38 -0
- package/dist/messaging/envelope.d.ts +66 -0
- package/dist/messaging/envelope.js +49 -0
- package/dist/messaging/errors.d.ts +45 -0
- package/dist/messaging/errors.js +63 -0
- package/dist/messaging/inbox.d.ts +4 -0
- package/dist/messaging/inbox.js +9 -0
- package/dist/messaging/index.d.ts +13 -0
- package/dist/messaging/index.js +10 -0
- package/dist/messaging/kit-messaging.module.d.ts +39 -0
- package/dist/messaging/kit-messaging.module.js +309 -0
- package/dist/messaging/outbox-relay.d.ts +67 -0
- package/dist/messaging/outbox-relay.js +260 -0
- package/dist/messaging/outbox-store.d.ts +75 -0
- package/dist/messaging/outbox-store.js +126 -0
- package/dist/messaging/peer.d.ts +1 -0
- package/dist/messaging/peer.js +5 -0
- package/dist/messaging/rabbit-connection.d.ts +144 -0
- package/dist/messaging/rabbit-connection.js +472 -0
- package/dist/messaging/redelivery.d.ts +6 -0
- package/dist/messaging/redelivery.js +17 -0
- package/dist/messaging/subscriber.d.ts +102 -0
- package/dist/messaging/subscriber.js +412 -0
- package/dist/messaging/tables.d.ts +315 -0
- package/dist/messaging/tables.js +72 -0
- package/dist/messaging/topology.d.ts +56 -0
- package/dist/messaging/topology.js +56 -0
- package/dist/observability/fail-startup.d.ts +14 -0
- package/dist/observability/fail-startup.js +51 -0
- package/dist/observability/health-registry.d.ts +34 -0
- package/dist/observability/health-registry.js +81 -0
- package/dist/observability/index.d.ts +16 -0
- package/dist/observability/index.js +10 -0
- package/dist/observability/lifecycle.d.ts +52 -0
- package/dist/observability/lifecycle.js +151 -0
- package/dist/observability/logger-options.d.ts +54 -0
- package/dist/observability/logger-options.js +91 -0
- package/dist/observability/metrics-server.d.ts +33 -0
- package/dist/observability/metrics-server.js +108 -0
- package/dist/observability/metrics.d.ts +30 -0
- package/dist/observability/metrics.js +53 -0
- package/dist/observability/redaction.d.ts +23 -0
- package/dist/observability/redaction.js +65 -0
- package/dist/observability/sanitize-error.d.ts +47 -0
- package/dist/observability/sanitize-error.js +194 -0
- package/dist/observability/sentry.d.ts +64 -0
- package/dist/observability/sentry.js +233 -0
- package/dist/redis/channels.d.ts +34 -0
- package/dist/redis/channels.js +128 -0
- package/dist/redis/client.d.ts +42 -0
- package/dist/redis/client.js +85 -0
- package/dist/redis/index.d.ts +7 -0
- package/dist/redis/index.js +7 -0
- package/dist/redis/kit-redis.module.d.ts +18 -0
- package/dist/redis/kit-redis.module.js +131 -0
- package/dist/redis/peer.d.ts +1 -0
- package/dist/redis/peer.js +4 -0
- package/dist/redis/rate-limiter.d.ts +49 -0
- package/dist/redis/rate-limiter.js +139 -0
- package/dist/testing/count-queries.d.ts +30 -0
- package/dist/testing/count-queries.js +58 -0
- package/dist/testing/create-test-app.d.ts +26 -0
- package/dist/testing/create-test-app.js +32 -0
- package/dist/testing/expect-matches-api.d.ts +8 -0
- package/dist/testing/expect-matches-api.js +49 -0
- package/dist/testing/expect-problem.d.ts +24 -0
- package/dist/testing/expect-problem.js +23 -0
- package/dist/testing/images.d.ts +10 -0
- package/dist/testing/images.js +14 -0
- package/dist/testing/index.d.ts +23 -0
- package/dist/testing/index.js +17 -0
- package/dist/testing/launch-command.d.ts +52 -0
- package/dist/testing/launch-command.js +510 -0
- package/dist/testing/outbox-coverage.d.ts +26 -0
- package/dist/testing/outbox-coverage.js +98 -0
- package/dist/testing/rabbitmq-admin.d.ts +81 -0
- package/dist/testing/rabbitmq-admin.js +146 -0
- package/dist/testing/redis-worker.d.ts +10 -0
- package/dist/testing/redis-worker.js +15 -0
- package/dist/testing/repo-conventions.d.ts +10 -0
- package/dist/testing/repo-conventions.js +524 -0
- package/dist/testing/repo-files.d.ts +113 -0
- package/dist/testing/repo-files.js +401 -0
- package/dist/testing/scratch-database.d.ts +19 -0
- package/dist/testing/scratch-database.js +75 -0
- package/dist/testing/start-test-stack.d.ts +85 -0
- package/dist/testing/start-test-stack.js +342 -0
- package/dist/testing/tcp-proxy.d.ts +40 -0
- package/dist/testing/tcp-proxy.js +164 -0
- package/dist/testing/tenant-isolation.d.ts +31 -0
- package/dist/testing/tenant-isolation.js +205 -0
- package/dist/testing/test-events.d.ts +37 -0
- package/dist/testing/test-events.js +150 -0
- package/dist/testing/timeout-scale.d.ts +4 -0
- package/dist/testing/timeout-scale.js +17 -0
- package/dist/testing/timing-guard.d.ts +22 -0
- package/dist/testing/timing-guard.js +54 -0
- package/dist/testing/worker-scope.d.ts +31 -0
- package/dist/testing/worker-scope.js +68 -0
- package/package.json +177 -4
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// expectOutbox, assertOutboxCoverage (prd.md §3.8 E5, GAP-2, UC-14, AC-5.13; П16). Покрытие — файлы воркеров
|
|
2
|
+
// w<VITEST_POOL_ID>.txt в каталоге прогона startTestStack({outboxEvents}); teardown при KIT_OUTBOX_COVERAGE=1. vitest —
|
|
3
|
+
// loadPeer внутри функций: /testing импортируется без vitest; /messaging не импортируется (без amqplib).
|
|
4
|
+
import { appendFileSync, readdirSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { sql } from 'drizzle-orm';
|
|
7
|
+
import { OUTBOX_TABLE } from '../internal/outbox-policy.js';
|
|
8
|
+
import { loadPeer } from '../internal/peer.js';
|
|
9
|
+
import { quoteIdentifier } from '../internal/sql-identifier.js';
|
|
10
|
+
let coverageDir;
|
|
11
|
+
let workerCoverageDir;
|
|
12
|
+
/** @internal Каталог прогона в процессе globalSetup (startTestStack). */
|
|
13
|
+
export function setCoverageDir(dir) {
|
|
14
|
+
coverageDir = dir;
|
|
15
|
+
}
|
|
16
|
+
/** @internal Каталог покрытия воркера вместо `inject('kitOutboxCoverageDir')` (тесты пакета). */
|
|
17
|
+
export function setWorkerCoverageDir(dir) {
|
|
18
|
+
workerCoverageDir = dir;
|
|
19
|
+
}
|
|
20
|
+
/** Ключ покрытия события. */
|
|
21
|
+
function coverageKey(event) {
|
|
22
|
+
return `${event.type}.v${event.version}`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Проверяет запись события в **текущей** транзакции `tx` (строки других транзакций не учитываются): строка
|
|
26
|
+
* `<schema>.outbox` с типом и версией события есть, `data` проходит схему события и (если задано) совпадает с
|
|
27
|
+
* `expected.data` (`toMatchObject`), `subjectId` (если задан) совпадает. После успешной проверки ключ `<type>.v<version>`
|
|
28
|
+
* дописывается в файл покрытия воркера (`assertOutboxCoverage`).
|
|
29
|
+
* @throws утверждение vitest — строки нет, `data` не по схеме или не совпала.
|
|
30
|
+
* @throws {TypeError} `startTestStack` без `postgres`.
|
|
31
|
+
*/
|
|
32
|
+
export async function expectOutbox(tx, event, expected = {}) {
|
|
33
|
+
const { expect, inject } = await loadPeer('vitest', 'testing');
|
|
34
|
+
const schema = inject('kitDatabaseService');
|
|
35
|
+
if (schema === undefined) {
|
|
36
|
+
throw new TypeError('expectOutbox: startTestStack запущен без postgres');
|
|
37
|
+
}
|
|
38
|
+
const key = coverageKey(event);
|
|
39
|
+
const table = sql.raw(`${quoteIdentifier(schema)}.${quoteIdentifier(OUTBOX_TABLE)}`);
|
|
40
|
+
const subject = expected.subjectId === undefined ? sql `` : sql ` and subject_id = ${expected.subjectId}`;
|
|
41
|
+
const { rows } = await tx.execute(sql `select id, subject_id, data from ${table}
|
|
42
|
+
where type = ${event.type} and version = ${event.version}
|
|
43
|
+
and xmin = pg_current_xact_id()::xid${subject} order by seq`);
|
|
44
|
+
const where = expected.subjectId === undefined ? '' : ` (subjectId ${expected.subjectId})`;
|
|
45
|
+
expect(rows, `expectOutbox: нет строки ${key}${where} в транзакции`).not.toHaveLength(0);
|
|
46
|
+
for (const row of rows) {
|
|
47
|
+
const parsed = event.data.safeParse(row.data);
|
|
48
|
+
if (!parsed.success) {
|
|
49
|
+
const paths = parsed.error.issues.map((issue) => issue.path.join('.') || '(root)');
|
|
50
|
+
expect.fail(`expectOutbox: data строки ${row.id} (${key}) не по схеме: ${paths.join(', ')}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (expected.data !== undefined) {
|
|
54
|
+
const matching = rows.some((row) => {
|
|
55
|
+
try {
|
|
56
|
+
expect(row.data).toMatchObject(expected.data);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
if (!matching) {
|
|
64
|
+
expect(rows[0]?.data, `expectOutbox: data ${key} не совпала`).toMatchObject(expected.data);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const dir = workerCoverageDir ?? inject('kitOutboxCoverageDir');
|
|
68
|
+
if (dir !== undefined) {
|
|
69
|
+
appendFileSync(join(dir, `w${process.env.VITEST_POOL_ID ?? '0'}.txt`), `${key}\n`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Падает со списком событий без успешного `expectOutbox` за прогон (GAP-2): объединяет файлы воркеров каталога
|
|
74
|
+
* прогона. Вызывает teardown `startTestStack({outboxEvents})` при `KIT_OUTBOX_COVERAGE=1`.
|
|
75
|
+
* @throws {TypeError} вызов вне teardown `startTestStack({outboxEvents})`.
|
|
76
|
+
* @throws {Error} «assertOutboxCoverage: события без успешного expectOutbox: <ключи>».
|
|
77
|
+
*/
|
|
78
|
+
export function assertOutboxCoverage(events) {
|
|
79
|
+
const dir = coverageDir;
|
|
80
|
+
if (dir === undefined) {
|
|
81
|
+
throw new TypeError('assertOutboxCoverage: вызывается из teardown startTestStack({outboxEvents})');
|
|
82
|
+
}
|
|
83
|
+
const covered = new Set();
|
|
84
|
+
for (const file of readdirSync(dir).filter((name) => name.endsWith('.txt'))) {
|
|
85
|
+
for (const line of readFileSync(join(dir, file), 'utf8').split('\n')) {
|
|
86
|
+
if (line !== '') {
|
|
87
|
+
covered.add(line);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const missing = [...new Set(events.map(coverageKey))].filter((key) => !covered.has(key));
|
|
92
|
+
if (missing.length > 0) {
|
|
93
|
+
const message = `assertOutboxCoverage: события без успешного expectOutbox: ${missing.join(', ')}`;
|
|
94
|
+
// Vite теряет текст ошибок globalSetup: сообщение — в stderr до throw.
|
|
95
|
+
process.stderr.write(`${message}\n`);
|
|
96
|
+
throw new Error(message);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type RabbitDefinitions } from '../internal/rabbitmq-definitions.js';
|
|
2
|
+
/** vhost воркера: пользователь воркера (`url`) и администратор (`adminUrl`) на том же vhost. */
|
|
3
|
+
export interface KitRabbitWorker {
|
|
4
|
+
/** Имя vhost. */
|
|
5
|
+
readonly vhost: string;
|
|
6
|
+
/** amqp://<пользователь воркера>@host:port/<vhost> — права M3 сервиса. */
|
|
7
|
+
readonly url: string;
|
|
8
|
+
/** amqp://<администратор>@host:port/<vhost> — полные права (тесты, publishTestEvent). */
|
|
9
|
+
readonly adminUrl: string;
|
|
10
|
+
}
|
|
11
|
+
/** Состояние очереди по management API. */
|
|
12
|
+
export interface RabbitQueueInfo {
|
|
13
|
+
/** Переживает перезапуск. */
|
|
14
|
+
readonly durable: boolean;
|
|
15
|
+
/** Удаляется без потребителей. */
|
|
16
|
+
readonly autoDelete: boolean;
|
|
17
|
+
/** Эксклюзивная. */
|
|
18
|
+
readonly exclusive: boolean;
|
|
19
|
+
/** Аргументы. */
|
|
20
|
+
readonly arguments: Readonly<Record<string, unknown>>;
|
|
21
|
+
/** Сообщений всего. */
|
|
22
|
+
readonly messages: number;
|
|
23
|
+
/** Готовых к выдаче. */
|
|
24
|
+
readonly messagesReady: number;
|
|
25
|
+
/** Без подтверждения. */
|
|
26
|
+
readonly messagesUnacknowledged: number;
|
|
27
|
+
/** Потребителей. */
|
|
28
|
+
readonly consumers: number;
|
|
29
|
+
}
|
|
30
|
+
/** Запросы к management API с Basic auth администратора. */
|
|
31
|
+
export declare class RabbitAdmin {
|
|
32
|
+
private readonly managementUrl;
|
|
33
|
+
/** Имя пользователя; по умолчанию — имя сервиса. */
|
|
34
|
+
readonly user: string;
|
|
35
|
+
private readonly auth;
|
|
36
|
+
constructor(managementUrl: string,
|
|
37
|
+
/** Имя пользователя; по умолчанию — имя сервиса. */
|
|
38
|
+
user: string, password: string);
|
|
39
|
+
private request;
|
|
40
|
+
/** Ожидание готовности management API (15672 поднимается позже 5672). */
|
|
41
|
+
waitReady(timeoutMs?: number): Promise<void>;
|
|
42
|
+
/** Импорт definitions.json (vhosts, users, permissions, exchanges). */
|
|
43
|
+
loadDefinitions(definitions: RabbitDefinitions): Promise<void>;
|
|
44
|
+
/** Права пользователя на vhost. */
|
|
45
|
+
setPermissions(vhost: string, user: string, permissions: {
|
|
46
|
+
readonly configure: string;
|
|
47
|
+
readonly write: string;
|
|
48
|
+
readonly read: string;
|
|
49
|
+
}): Promise<void>;
|
|
50
|
+
/** Имена vhost. */
|
|
51
|
+
listVhosts(): Promise<string[]>;
|
|
52
|
+
/** Имена пользователей. */
|
|
53
|
+
listUsers(): Promise<string[]>;
|
|
54
|
+
/** Удаление vhost; нет — успех. */
|
|
55
|
+
deleteVhost(name: string): Promise<void>;
|
|
56
|
+
/** Удаление пользователя; нет — успех. */
|
|
57
|
+
deleteUser(name: string): Promise<void>;
|
|
58
|
+
/** Очередь vhost или `null`. */
|
|
59
|
+
queue(vhost: string, name: string): Promise<RabbitQueueInfo | null>;
|
|
60
|
+
}
|
|
61
|
+
/** Администратор из `kitRabbitmqUrl` (учётные данные в URL amqp) и `kitRabbitmqManagementUrl` (без них). */
|
|
62
|
+
export declare function rabbitAdminFromProvided(managementUrl: string, amqpAdminUrl: string): RabbitAdmin;
|
|
63
|
+
/** Удаление vhost и пользователей с префиксом. */
|
|
64
|
+
export declare function deleteTestVhosts(admin: RabbitAdmin, prefix: string): Promise<{
|
|
65
|
+
readonly vhosts: number;
|
|
66
|
+
readonly users: number;
|
|
67
|
+
}>;
|
|
68
|
+
/**
|
|
69
|
+
* vhost и пользователь на каждый воркер `1..workers` (`<service>-test-<runId>-w<n>`) из `buildDefinitions` — те же
|
|
70
|
+
* права, что CLI; администратору — полные права на vhost. До создания удаляются остатки прошлых прогонов с префиксом.
|
|
71
|
+
*/
|
|
72
|
+
export declare function prepareWorkerVhosts(admin: RabbitAdmin, options: {
|
|
73
|
+
/** Имя сервиса — права на его очереди. */
|
|
74
|
+
readonly service: string;
|
|
75
|
+
readonly workers: number;
|
|
76
|
+
readonly runId: string;
|
|
77
|
+
readonly amqpAdminUrl: string;
|
|
78
|
+
}): Promise<{
|
|
79
|
+
readonly removed: readonly string[];
|
|
80
|
+
readonly workers: readonly KitRabbitWorker[];
|
|
81
|
+
}>;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Клиент management API RabbitMQ тестового стека (prd.md §3.8 E5, AC-5.18, UC-14; П2): definitions.json, vhost и
|
|
2
|
+
// пользователь на воркер, удаление по префиксу <svc>-test-. Эталон — B test/support/broker.ts (BrokerAdmin). Внутренний
|
|
3
|
+
// модуль /testing: глобальный fetch, без amqplib.
|
|
4
|
+
import { randomBytes } from 'node:crypto';
|
|
5
|
+
import { buildDefinitions, hashRabbitPassword, } from '../internal/rabbitmq-definitions.js';
|
|
6
|
+
const ALL = { configure: '.*', write: '.*', read: '.*' };
|
|
7
|
+
/** Запросы к management API с Basic auth администратора. */
|
|
8
|
+
export class RabbitAdmin {
|
|
9
|
+
managementUrl;
|
|
10
|
+
user;
|
|
11
|
+
auth;
|
|
12
|
+
constructor(managementUrl,
|
|
13
|
+
/** Имя пользователя; по умолчанию — имя сервиса. */
|
|
14
|
+
user, password) {
|
|
15
|
+
this.managementUrl = managementUrl;
|
|
16
|
+
this.user = user;
|
|
17
|
+
this.auth = `Basic ${Buffer.from(`${user}:${password}`).toString('base64')}`;
|
|
18
|
+
}
|
|
19
|
+
async request(method, path, body, ok = [200, 201, 204]) {
|
|
20
|
+
const response = await fetch(new URL(path, this.managementUrl), {
|
|
21
|
+
method,
|
|
22
|
+
headers: { authorization: this.auth, 'content-type': 'application/json' },
|
|
23
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
24
|
+
});
|
|
25
|
+
if (!ok.includes(response.status)) {
|
|
26
|
+
// Без тела и пароля: только метод, путь и статус.
|
|
27
|
+
throw new Error(`RabbitMQ management ${method} ${path}: статус ${response.status}`);
|
|
28
|
+
}
|
|
29
|
+
return response;
|
|
30
|
+
}
|
|
31
|
+
/** Ожидание готовности management API (15672 поднимается позже 5672). */
|
|
32
|
+
async waitReady(timeoutMs = 60_000) {
|
|
33
|
+
const deadline = Date.now() + timeoutMs;
|
|
34
|
+
for (;;) {
|
|
35
|
+
try {
|
|
36
|
+
await this.request('GET', 'api/overview');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (Date.now() > deadline) {
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Импорт definitions.json (vhosts, users, permissions, exchanges). */
|
|
48
|
+
async loadDefinitions(definitions) {
|
|
49
|
+
await this.request('POST', 'api/definitions', definitions);
|
|
50
|
+
}
|
|
51
|
+
/** Права пользователя на vhost. */
|
|
52
|
+
async setPermissions(vhost, user, permissions) {
|
|
53
|
+
await this.request('PUT', `api/permissions/${encodeURIComponent(vhost)}/${encodeURIComponent(user)}`, permissions);
|
|
54
|
+
}
|
|
55
|
+
/** Имена vhost. */
|
|
56
|
+
async listVhosts() {
|
|
57
|
+
const response = await this.request('GET', 'api/vhosts');
|
|
58
|
+
return (await response.json()).map(({ name }) => name);
|
|
59
|
+
}
|
|
60
|
+
/** Имена пользователей. */
|
|
61
|
+
async listUsers() {
|
|
62
|
+
const response = await this.request('GET', 'api/users');
|
|
63
|
+
return (await response.json()).map(({ name }) => name);
|
|
64
|
+
}
|
|
65
|
+
/** Удаление vhost; нет — успех. */
|
|
66
|
+
async deleteVhost(name) {
|
|
67
|
+
await this.request('DELETE', `api/vhosts/${encodeURIComponent(name)}`, undefined, [204, 404]);
|
|
68
|
+
}
|
|
69
|
+
/** Удаление пользователя; нет — успех. */
|
|
70
|
+
async deleteUser(name) {
|
|
71
|
+
await this.request('DELETE', `api/users/${encodeURIComponent(name)}`, undefined, [204, 404]);
|
|
72
|
+
}
|
|
73
|
+
/** Очередь vhost или `null`. */
|
|
74
|
+
async queue(vhost, name) {
|
|
75
|
+
const response = await this.request('GET', `api/queues/${encodeURIComponent(vhost)}/${encodeURIComponent(name)}`, undefined, [200, 404]);
|
|
76
|
+
if (response.status === 404) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const raw = (await response.json());
|
|
80
|
+
const count = (key) => (typeof raw[key] === 'number' ? raw[key] : 0);
|
|
81
|
+
return {
|
|
82
|
+
durable: raw['durable'] === true,
|
|
83
|
+
autoDelete: raw['auto_delete'] === true,
|
|
84
|
+
exclusive: raw['exclusive'] === true,
|
|
85
|
+
arguments: raw['arguments'] ?? {},
|
|
86
|
+
messages: count('messages'),
|
|
87
|
+
messagesReady: count('messages_ready'),
|
|
88
|
+
messagesUnacknowledged: count('messages_unacknowledged'),
|
|
89
|
+
consumers: count('consumers'),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** Администратор из `kitRabbitmqUrl` (учётные данные в URL amqp) и `kitRabbitmqManagementUrl` (без них). */
|
|
94
|
+
export function rabbitAdminFromProvided(managementUrl, amqpAdminUrl) {
|
|
95
|
+
const url = new URL(amqpAdminUrl);
|
|
96
|
+
return new RabbitAdmin(managementUrl, decodeURIComponent(url.username), decodeURIComponent(url.password));
|
|
97
|
+
}
|
|
98
|
+
function amqpUrl(base, user, password, vhost) {
|
|
99
|
+
const url = new URL(base);
|
|
100
|
+
url.username = encodeURIComponent(user);
|
|
101
|
+
url.password = encodeURIComponent(password);
|
|
102
|
+
url.pathname = `/${encodeURIComponent(vhost)}`;
|
|
103
|
+
return url.toString();
|
|
104
|
+
}
|
|
105
|
+
/** Удаление vhost и пользователей с префиксом. */
|
|
106
|
+
export async function deleteTestVhosts(admin, prefix) {
|
|
107
|
+
const [vhosts, users] = await Promise.all([admin.listVhosts(), admin.listUsers()]);
|
|
108
|
+
const ownVhosts = vhosts.filter((name) => name.startsWith(prefix));
|
|
109
|
+
const ownUsers = users.filter((name) => name.startsWith(prefix));
|
|
110
|
+
await Promise.all(ownVhosts.map((name) => admin.deleteVhost(name)));
|
|
111
|
+
await Promise.all(ownUsers.map((name) => admin.deleteUser(name)));
|
|
112
|
+
return { vhosts: ownVhosts.length, users: ownUsers.length };
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* vhost и пользователь на каждый воркер `1..workers` (`<service>-test-<runId>-w<n>`) из `buildDefinitions` — те же
|
|
116
|
+
* права, что CLI; администратору — полные права на vhost. До создания удаляются остатки прошлых прогонов с префиксом.
|
|
117
|
+
*/
|
|
118
|
+
export async function prepareWorkerVhosts(admin, options) {
|
|
119
|
+
const prefix = `${options.service}-test-`;
|
|
120
|
+
const leftovers = (await admin.listVhosts()).filter((name) => name.startsWith(prefix));
|
|
121
|
+
await deleteTestVhosts(admin, prefix);
|
|
122
|
+
const admins = new URL(options.amqpAdminUrl);
|
|
123
|
+
const adminPassword = decodeURIComponent(admins.password);
|
|
124
|
+
const workers = [];
|
|
125
|
+
for (let n = 1; n <= options.workers; n += 1) {
|
|
126
|
+
const vhost = `${prefix}${options.runId}-w${n}`;
|
|
127
|
+
const password = randomBytes(16).toString('hex');
|
|
128
|
+
await admin.loadDefinitions(buildDefinitions({
|
|
129
|
+
vhost,
|
|
130
|
+
users: [
|
|
131
|
+
{
|
|
132
|
+
service: options.service,
|
|
133
|
+
user: vhost,
|
|
134
|
+
passwordHash: hashRabbitPassword(password),
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
}));
|
|
138
|
+
await admin.setPermissions(vhost, admin.user, ALL);
|
|
139
|
+
workers.push({
|
|
140
|
+
vhost,
|
|
141
|
+
url: amqpUrl(options.amqpAdminUrl, vhost, password, vhost),
|
|
142
|
+
adminUrl: amqpUrl(options.amqpAdminUrl, admin.user, adminPassword, vhost),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return { removed: leftovers, workers };
|
|
146
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { KitTestOverride } from './create-test-app.js';
|
|
2
|
+
/**
|
|
3
|
+
* Подмена префикса ключей и каналов `KitRedisModule` префиксом воркера `workerScope().redisPrefix` (AC-8.5).
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* vi.stubEnv('REDIS_URL', inject('kitRedisUrl') ?? '');
|
|
7
|
+
* const app = await createTestApp({ module: AppModule, service: 'svc', overrides: [redisWorkerOverride()] });
|
|
8
|
+
* @throws {TypeError} стек без `redis: true`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function redisWorkerOverride(): KitTestOverride;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// redisWorkerOverride (prd.md §3.8, AC-8.5, UC-14; D2 плана E8): подмена префикса Redis приложения префиксом воркера
|
|
2
|
+
// <svc>:w<N>: для createTestApp({overrides}). Токен префикса — внутренний, из /redis не экспортируется.
|
|
3
|
+
import { KIT_REDIS_PREFIX } from '../internal/tokens.js';
|
|
4
|
+
import { workerScope } from './worker-scope.js';
|
|
5
|
+
/**
|
|
6
|
+
* Подмена префикса ключей и каналов `KitRedisModule` префиксом воркера `workerScope().redisPrefix` (AC-8.5).
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* vi.stubEnv('REDIS_URL', inject('kitRedisUrl') ?? '');
|
|
10
|
+
* const app = await createTestApp({ module: AppModule, service: 'svc', overrides: [redisWorkerOverride()] });
|
|
11
|
+
* @throws {TypeError} стек без `redis: true`.
|
|
12
|
+
*/
|
|
13
|
+
export function redisWorkerOverride() {
|
|
14
|
+
return { provide: KIT_REDIS_PREFIX, useValue: workerScope().redisPrefix };
|
|
15
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type RepoFiles } from './repo-files.js';
|
|
2
|
+
/** Параметры assertRepoConventions (§3.8). */
|
|
3
|
+
export interface RepoConventionsOptions {
|
|
4
|
+
/** Корень репозитория сервиса. */
|
|
5
|
+
readonly root: string;
|
|
6
|
+
}
|
|
7
|
+
/** Базовый файл Compose репозитория — первый найденный по порядку поиска Compose; null — базового файла нет. */
|
|
8
|
+
export declare function baseComposeFile(files: RepoFiles): string | null;
|
|
9
|
+
/** Проверяет репозиторий сервиса по правилам §3.8; при нарушениях бросает ошибку со списком. */
|
|
10
|
+
export declare function assertRepoConventions(options: RepoConventionsOptions): void;
|