@application-kit/testing 0.5.0 → 0.6.0
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 +10 -0
- package/README.md +23 -4
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/postgres/index.d.ts +1 -0
- package/dist/cjs/postgres/index.js +1 -0
- package/dist/cjs/postgres/start-postgres-web-application.d.ts +15 -0
- package/dist/cjs/postgres/start-postgres-web-application.js +36 -0
- package/dist/cjs/postgres/test-data-source.d.ts +2 -1
- package/dist/cjs/postgres/test-data-source.js +17 -15
- package/dist/cjs/replace-process-environment.d.ts +1 -0
- package/dist/cjs/replace-process-environment.js +60 -0
- package/dist/cjs/vitest/setup/fast-bcrypt-cost.d.ts +5 -0
- package/dist/cjs/vitest/setup/fast-bcrypt-cost.js +17 -0
- package/dist/cjs/vitest/setup/fast-bcrypt.d.ts +1 -0
- package/dist/cjs/vitest/setup/fast-bcrypt.js +9 -0
- package/dist/cjs/vitest/setup/index.d.ts +2 -0
- package/dist/cjs/vitest/setup/index.js +18 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/postgres/index.d.ts +1 -0
- package/dist/esm/postgres/index.js +1 -0
- package/dist/esm/postgres/start-postgres-web-application.d.ts +15 -0
- package/dist/esm/postgres/start-postgres-web-application.js +32 -0
- package/dist/esm/postgres/test-data-source.d.ts +2 -1
- package/dist/esm/postgres/test-data-source.js +15 -14
- package/dist/esm/replace-process-environment.d.ts +1 -0
- package/dist/esm/replace-process-environment.js +53 -0
- package/dist/esm/vitest/setup/fast-bcrypt-cost.d.ts +5 -0
- package/dist/esm/vitest/setup/fast-bcrypt-cost.js +13 -0
- package/dist/esm/vitest/setup/fast-bcrypt.d.ts +1 -0
- package/dist/esm/vitest/setup/fast-bcrypt.js +7 -0
- package/dist/esm/vitest/setup/index.d.ts +2 -0
- package/dist/esm/vitest/setup/index.js +2 -0
- package/package.json +19 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @application-kit/testing
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d790ac9: Обвязка e2e и unit-прогона приложения: `replaceProcessEnvironment(environment)` задаёт окружение прогона целиком (снимает
|
|
8
|
+
всё, кроме переменных инструментария, и выставляет набор приложения), `startPostgresWebApplication(options)` поднимает
|
|
9
|
+
настоящий web-граф на временном Postgres с подменой конфига БД по токену, `postgresContainerDataSourceOptions` — общие
|
|
10
|
+
опции подключения к контейнеру для `testDataSource` и запуска. Setup-файл `@application-kit/testing/vitest/fast-bcrypt`
|
|
11
|
+
подменяет `bcrypt` дешёвой стоимостью с сохранением запрошенной в строке хеша; `bcrypt` — optional peer.
|
|
12
|
+
|
|
3
13
|
## 0.5.0
|
|
4
14
|
|
|
5
15
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @application-kit/testing
|
|
2
2
|
|
|
3
3
|
Тестовая инфраструктура NestJS-приложений: стабы шин CQRS, логгер-накопитель, фейковый microservice-транспорт,
|
|
4
|
-
сборка web-приложения для e2e, Postgres в testcontainers,
|
|
5
|
-
vitest (
|
|
4
|
+
сборка и запуск web-приложения для e2e, Postgres в testcontainers, изоляция окружения прогона, конфиги vitest приложения,
|
|
5
|
+
дешёвый bcrypt для unit-прогона и раскладка ячеек CI по проектам vitest (входы `/vitest`, `/vitest/fast-bcrypt`).
|
|
6
6
|
Для `devDependencies` приложения, в прод не попадает.
|
|
7
7
|
|
|
8
8
|
## Установка
|
|
@@ -14,7 +14,7 @@ yarn add --dev @application-kit/testing
|
|
|
14
14
|
Peer-зависимости: `@nestjs/common`, `@nestjs/core`, `@nestjs/cqrs`,
|
|
15
15
|
`@nestjs/microservices`, `@nestjs/platform-fastify`, `@nestjs/testing`, `nestjs-pino`, `oxide.ts`, `rxjs`,
|
|
16
16
|
`testcontainers`, `typeorm` — обязательные для корневого входа. Вход `/vitest` несёт свои optional peers `vitest` и
|
|
17
|
-
`vite` (`mergeConfig` фабрик конфигов)
|
|
17
|
+
`vite` (`mergeConfig` фабрик конфигов), `/vitest/fast-bcrypt` — `bcrypt`.
|
|
18
18
|
|
|
19
19
|
## Состав
|
|
20
20
|
|
|
@@ -26,8 +26,18 @@ Peer-зависимости: `@nestjs/common`, `@nestjs/core`, `@nestjs/cqrs`,
|
|
|
26
26
|
`testQueryRunner` закрепляет `QueryRunner` (отключается `pinQueryRunner: false`).
|
|
27
27
|
- `startPostgresContainer(image)`, `testDataSource({ image, migrations, entities, … })` — Postgres в контейнере и
|
|
28
28
|
инициализированный `DataSource`. Образ и стратегию имён передаёт приложение: версия БД и именование — его решение, у пакета их нет.
|
|
29
|
+
Опции подключения к контейнеру — `postgresContainerDataSourceOptions(container, namingStrategy)`.
|
|
30
|
+
- `startPostgresWebApplication({ databaseConfigToken, image, imports, namingStrategy, …buildOptions })` — настоящий web-граф
|
|
31
|
+
на временном Postgres: конфиг БД по токену подменяется объектом с `createDataSourceOptions()` поверх контейнера, схема —
|
|
32
|
+
`synchronize` из entities, `stop()` закрывает приложение, затем контейнер. Звать из `beforeAll` с таймаутом.
|
|
33
|
+
- `replaceProcessEnvironment(environment)` — окружение прогона целиком: из `process.env` снимается всё, кроме переменных
|
|
34
|
+
инструментария (node, yarn, vitest, Docker, прокси, локаль), и выставляется набор приложения. Звать из `setupFiles`
|
|
35
|
+
e2e-профиля: конфиг-модули снимают окружение на импорте.
|
|
29
36
|
|
|
30
37
|
```ts
|
|
38
|
+
// src/testing/e2e-environment.ts — setupFiles e2e-профиля
|
|
39
|
+
replaceProcessEnvironment({ DB_HOST: 'localhost', JWT_SECRET: randomUUID() })
|
|
40
|
+
|
|
31
41
|
const { dataSource } = await testDataSource({
|
|
32
42
|
entities: [UserOrmEntity],
|
|
33
43
|
image: 'postgres:18.3-alpine3.23',
|
|
@@ -55,7 +65,7 @@ export default defineApplicationUnitConfig({
|
|
|
55
65
|
coverage: { exclude: ['**/*.module.ts'], include: ['src'], thresholds: { lines: 95 } },
|
|
56
66
|
excludeSpecs: E2E_SPECS,
|
|
57
67
|
root: import.meta.dirname,
|
|
58
|
-
setupFiles: ['
|
|
68
|
+
setupFiles: ['@application-kit/testing/vitest/fast-bcrypt'],
|
|
59
69
|
specs: ['src/**/*.spec.ts'],
|
|
60
70
|
viteConfig,
|
|
61
71
|
})
|
|
@@ -82,6 +92,15 @@ export default defineApplicationEndToEndConfig({
|
|
|
82
92
|
- e2e — окружение `node` и алиас `graphql` на CJS-вход `node_modules/graphql/index.js` под `root`: Vite для SSR берёт
|
|
83
93
|
ESM-сборку `graphql`, внешний `mercurius` — CJS, и без алиаса схема и валидация запроса живут в разных realm.
|
|
84
94
|
|
|
95
|
+
## Вход `/vitest/fast-bcrypt`: дешёвый bcrypt
|
|
96
|
+
|
|
97
|
+
Setup-файл unit-профиля (`setupFiles: ['@application-kit/testing/vitest/fast-bcrypt']`): `vi.mock('bcrypt')` считает хеш
|
|
98
|
+
с cost 4 (≈ 1 мс против ≈ 300 мс у cost 12), а поле стоимости в строке переписывает на запрошенную — `getRounds` и
|
|
99
|
+
решение о перехешировании видят настоящую стоимость; `compare` перед сверкой возвращает поле на 4. Сверяются только хеши,
|
|
100
|
+
выпущенные подменой: литерал хеша с настоящим cost 12 не сойдётся. Подмена видна коду, прошедшему трансформ vitest
|
|
101
|
+
(исходники приложения); внешний пакет, импортирующий `bcrypt`, ускоряется только через `inlineDependencies`. E2e-профилю
|
|
102
|
+
файл не нужен.
|
|
103
|
+
|
|
85
104
|
## Ячейки CI по проектам
|
|
86
105
|
|
|
87
106
|
`vitest --shard=i/n` сам режет спеки хешем, и при двух проектах разных трансформов (`server` в `node`, `client` в
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './fake-microservice/index.js';
|
|
2
2
|
export * from './postgres/index.js';
|
|
3
3
|
export * from './build-web-application.js';
|
|
4
|
+
export * from './replace-process-environment.js';
|
|
4
5
|
export * from './test-command-bus.js';
|
|
5
6
|
export * from './test-event-bus.js';
|
|
6
7
|
export * from './test-logger.js';
|
package/dist/cjs/index.js
CHANGED
|
@@ -17,6 +17,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
17
17
|
__exportStar(require("./fake-microservice/index.js"), exports);
|
|
18
18
|
__exportStar(require("./postgres/index.js"), exports);
|
|
19
19
|
__exportStar(require("./build-web-application.js"), exports);
|
|
20
|
+
__exportStar(require("./replace-process-environment.js"), exports);
|
|
20
21
|
__exportStar(require("./test-command-bus.js"), exports);
|
|
21
22
|
__exportStar(require("./test-event-bus.js"), exports);
|
|
22
23
|
__exportStar(require("./test-logger.js"), exports);
|
|
@@ -14,4 +14,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./start-postgres-web-application.js"), exports);
|
|
17
18
|
__exportStar(require("./test-data-source.js"), exports);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { InjectionToken, ModuleMetadata } from '@nestjs/common';
|
|
2
|
+
import type { NestFastifyApplication } from '@nestjs/platform-fastify';
|
|
3
|
+
import type { NamingStrategyInterface } from 'typeorm';
|
|
4
|
+
import type { BuildWebApplicationOptionsInterface } from '../build-web-application.js';
|
|
5
|
+
export interface PostgresWebApplicationOptionsInterface extends BuildWebApplicationOptionsInterface {
|
|
6
|
+
databaseConfigToken: InjectionToken;
|
|
7
|
+
image: string;
|
|
8
|
+
imports: NonNullable<ModuleMetadata['imports']>;
|
|
9
|
+
namingStrategy: NamingStrategyInterface;
|
|
10
|
+
}
|
|
11
|
+
export interface StartedWebApplicationInterface {
|
|
12
|
+
app: NestFastifyApplication;
|
|
13
|
+
stop(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export declare const startPostgresWebApplication: (options: PostgresWebApplicationOptionsInterface) => Promise<StartedWebApplicationInterface>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.startPostgresWebApplication = void 0;
|
|
4
|
+
const build_web_application_js_1 = require("../build-web-application.js");
|
|
5
|
+
const test_data_source_js_1 = require("./test-data-source.js");
|
|
6
|
+
const startPostgresWebApplication = async (options) => {
|
|
7
|
+
const { databaseConfigToken, image, imports, namingStrategy, ...buildOptions } = options;
|
|
8
|
+
const container = await (0, test_data_source_js_1.startPostgresContainer)(image);
|
|
9
|
+
const dataSourceOptions = {
|
|
10
|
+
...(0, test_data_source_js_1.postgresContainerDataSourceOptions)(container, namingStrategy),
|
|
11
|
+
migrationsRun: false,
|
|
12
|
+
synchronize: true,
|
|
13
|
+
};
|
|
14
|
+
try {
|
|
15
|
+
const app = await (0, build_web_application_js_1.buildWebApplication)(imports, [
|
|
16
|
+
{
|
|
17
|
+
provide: databaseConfigToken,
|
|
18
|
+
useValue: { createDataSourceOptions: () => dataSourceOptions },
|
|
19
|
+
},
|
|
20
|
+
], buildOptions);
|
|
21
|
+
await app.getHttpAdapter().getInstance()
|
|
22
|
+
.ready();
|
|
23
|
+
return {
|
|
24
|
+
app,
|
|
25
|
+
stop: async () => {
|
|
26
|
+
await app.close();
|
|
27
|
+
await container.stop();
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
await container.stop();
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
exports.startPostgresWebApplication = startPostgresWebApplication;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { StartedTestContainer } from 'testcontainers';
|
|
2
|
-
import type { EntitySchema, MixedList, NamingStrategyInterface, QueryRunner } from 'typeorm';
|
|
2
|
+
import type { DataSourceOptions, EntitySchema, MixedList, NamingStrategyInterface, QueryRunner } from 'typeorm';
|
|
3
3
|
import { DataSource } from 'typeorm';
|
|
4
4
|
export declare const TEST_DATABASE_NAME = "db";
|
|
5
5
|
export declare const TEST_DATABASE_PORT = 5432;
|
|
6
6
|
export declare const startPostgresContainer: (image: string) => Promise<StartedTestContainer>;
|
|
7
|
+
export declare const postgresContainerDataSourceOptions: (container: StartedTestContainer, namingStrategy: NamingStrategyInterface) => DataSourceOptions;
|
|
7
8
|
export interface TestDataSourceOptionsInterface {
|
|
8
9
|
entities: MixedList<EntitySchema | Function | string>;
|
|
9
10
|
image: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.testDataSource = exports.startPostgresContainer = exports.TEST_DATABASE_PORT = exports.TEST_DATABASE_NAME = void 0;
|
|
3
|
+
exports.testDataSource = exports.postgresContainerDataSourceOptions = exports.startPostgresContainer = exports.TEST_DATABASE_PORT = exports.TEST_DATABASE_NAME = void 0;
|
|
4
4
|
const testcontainers_1 = require("testcontainers");
|
|
5
5
|
const typeorm_1 = require("typeorm");
|
|
6
6
|
exports.TEST_DATABASE_NAME = 'db';
|
|
@@ -16,28 +16,30 @@ const startPostgresContainer = async (image) => new testcontainers_1.GenericCont
|
|
|
16
16
|
.withExposedPorts(exports.TEST_DATABASE_PORT)
|
|
17
17
|
.start();
|
|
18
18
|
exports.startPostgresContainer = startPostgresContainer;
|
|
19
|
+
const postgresContainerDataSourceOptions = (container, namingStrategy) => ({
|
|
20
|
+
database: exports.TEST_DATABASE_NAME,
|
|
21
|
+
host: container.getHost(),
|
|
22
|
+
logging: false,
|
|
23
|
+
namingStrategy,
|
|
24
|
+
password: exports.TEST_DATABASE_NAME,
|
|
25
|
+
port: container.getMappedPort(exports.TEST_DATABASE_PORT),
|
|
26
|
+
ssl: false,
|
|
27
|
+
subscribers: [],
|
|
28
|
+
type: 'postgres',
|
|
29
|
+
username: exports.TEST_DATABASE_NAME,
|
|
30
|
+
});
|
|
31
|
+
exports.postgresContainerDataSourceOptions = postgresContainerDataSourceOptions;
|
|
19
32
|
const testDataSource = async ({ entities, image, isLogging = false, migrations, namingStrategy, shouldSynchronize = false, }) => {
|
|
20
|
-
const databaseName = exports.TEST_DATABASE_NAME;
|
|
21
|
-
const databasePort = exports.TEST_DATABASE_PORT;
|
|
22
33
|
const container = await (0, exports.startPostgresContainer)(image);
|
|
23
|
-
const
|
|
24
|
-
|
|
34
|
+
const dataSource = new typeorm_1.DataSource({
|
|
35
|
+
...(0, exports.postgresContainerDataSourceOptions)(container, namingStrategy),
|
|
25
36
|
entities,
|
|
26
|
-
host: container.getHost(),
|
|
27
37
|
logging: isLogging,
|
|
28
38
|
maxQueryExecutionTime: 1000,
|
|
29
39
|
migrations,
|
|
30
40
|
migrationsRun: !shouldSynchronize,
|
|
31
|
-
namingStrategy,
|
|
32
|
-
password: databaseName,
|
|
33
|
-
port: container.getMappedPort(databasePort),
|
|
34
|
-
ssl: false,
|
|
35
|
-
subscribers: [],
|
|
36
41
|
synchronize: shouldSynchronize,
|
|
37
|
-
|
|
38
|
-
username: databaseName,
|
|
39
|
-
};
|
|
40
|
-
const dataSource = new typeorm_1.DataSource(databaseConfig);
|
|
42
|
+
});
|
|
41
43
|
await dataSource.initialize();
|
|
42
44
|
const queryRunner = dataSource.createQueryRunner();
|
|
43
45
|
Object.assign(dataSource.manager, { queryRunner });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const replaceProcessEnvironment: (environment: Readonly<Record<string, string>>) => void;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.replaceProcessEnvironment = void 0;
|
|
7
|
+
const node_process_1 = __importDefault(require("node:process"));
|
|
8
|
+
const TOOLCHAIN_NAMES = new Set([
|
|
9
|
+
'_',
|
|
10
|
+
'CI',
|
|
11
|
+
'COLORTERM',
|
|
12
|
+
'FORCE_COLOR',
|
|
13
|
+
'HOME',
|
|
14
|
+
'HOSTNAME',
|
|
15
|
+
'INIT_CWD',
|
|
16
|
+
'LANG',
|
|
17
|
+
'OLDPWD',
|
|
18
|
+
'PATH',
|
|
19
|
+
'PROJECT_CWD',
|
|
20
|
+
'PWD',
|
|
21
|
+
'SHELL',
|
|
22
|
+
'SHLVL',
|
|
23
|
+
'TEMP',
|
|
24
|
+
'TERM',
|
|
25
|
+
'TMP',
|
|
26
|
+
'TMPDIR',
|
|
27
|
+
'TZ',
|
|
28
|
+
'USER',
|
|
29
|
+
]);
|
|
30
|
+
const TOOLCHAIN_PREFIXES = [
|
|
31
|
+
'BERRY_',
|
|
32
|
+
'DOCKER_',
|
|
33
|
+
'HTTP_PROXY',
|
|
34
|
+
'HTTPS_PROXY',
|
|
35
|
+
'LC_',
|
|
36
|
+
'NODE_',
|
|
37
|
+
'NO_PROXY',
|
|
38
|
+
'NPM_',
|
|
39
|
+
'npm_',
|
|
40
|
+
'TESTCONTAINERS_',
|
|
41
|
+
'VITE_',
|
|
42
|
+
'VITEST_',
|
|
43
|
+
'XDG_',
|
|
44
|
+
'YARN_',
|
|
45
|
+
'http_proxy',
|
|
46
|
+
'https_proxy',
|
|
47
|
+
'no_proxy',
|
|
48
|
+
];
|
|
49
|
+
const isToolchainVariable = (name) => TOOLCHAIN_NAMES.has(name)
|
|
50
|
+
|| TOOLCHAIN_PREFIXES.some((prefix) => name.startsWith(prefix));
|
|
51
|
+
const replaceProcessEnvironment = (environment) => {
|
|
52
|
+
const runtimeEnvironment = node_process_1.default.env;
|
|
53
|
+
for (const name of Object.keys(runtimeEnvironment)) {
|
|
54
|
+
if (!isToolchainVariable(name)) {
|
|
55
|
+
Reflect.deleteProperty(runtimeEnvironment, name);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
Object.assign(runtimeEnvironment, environment);
|
|
59
|
+
};
|
|
60
|
+
exports.replaceProcessEnvironment = replaceProcessEnvironment;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export interface BcryptInterface {
|
|
2
|
+
compare(data: Buffer | string, encrypted: string): Promise<boolean>;
|
|
3
|
+
hash(data: Buffer | string, saltOrRounds: number | string): Promise<string>;
|
|
4
|
+
}
|
|
5
|
+
export declare const withFastBcryptCost: (bcrypt: BcryptInterface) => BcryptInterface;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.withFastBcryptCost = void 0;
|
|
4
|
+
const FAST_ROUNDS = 4;
|
|
5
|
+
const COST_FIELD_WIDTH = 2;
|
|
6
|
+
const COST_FIELD = /^(?<prefix>\$2[aby]\$)\d{2}\$/u;
|
|
7
|
+
const withCost = (hashed, rounds) => {
|
|
8
|
+
const cost = String(rounds).padStart(COST_FIELD_WIDTH, '0');
|
|
9
|
+
return hashed.replace(COST_FIELD, (_match, prefix) => `${prefix}${cost}$`);
|
|
10
|
+
};
|
|
11
|
+
const withFastBcryptCost = (bcrypt) => ({
|
|
12
|
+
compare: async (data, encrypted) => bcrypt.compare(data, withCost(encrypted, FAST_ROUNDS)),
|
|
13
|
+
hash: async (data, saltOrRounds) => (typeof saltOrRounds === 'number'
|
|
14
|
+
? withCost(await bcrypt.hash(data, Math.min(saltOrRounds, FAST_ROUNDS)), saltOrRounds)
|
|
15
|
+
: bcrypt.hash(data, saltOrRounds)),
|
|
16
|
+
});
|
|
17
|
+
exports.withFastBcryptCost = withFastBcryptCost;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const fast_bcrypt_cost_js_1 = require("./fast-bcrypt-cost.js");
|
|
5
|
+
vitest_1.vi.mock('bcrypt', async (importOriginal) => {
|
|
6
|
+
const bcrypt = await importOriginal();
|
|
7
|
+
const fast = (0, fast_bcrypt_cost_js_1.withFastBcryptCost)(bcrypt);
|
|
8
|
+
return { ...bcrypt, ...fast, default: { ...bcrypt.default, ...fast } };
|
|
9
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./fast-bcrypt.js"), exports);
|
|
18
|
+
__exportStar(require("./fast-bcrypt-cost.js"), exports);
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './fake-microservice/index.js';
|
|
2
2
|
export * from './postgres/index.js';
|
|
3
3
|
export * from './build-web-application.js';
|
|
4
|
+
export * from './replace-process-environment.js';
|
|
4
5
|
export * from './test-command-bus.js';
|
|
5
6
|
export * from './test-event-bus.js';
|
|
6
7
|
export * from './test-logger.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './fake-microservice/index.js';
|
|
2
2
|
export * from './postgres/index.js';
|
|
3
3
|
export * from './build-web-application.js';
|
|
4
|
+
export * from './replace-process-environment.js';
|
|
4
5
|
export * from './test-command-bus.js';
|
|
5
6
|
export * from './test-event-bus.js';
|
|
6
7
|
export * from './test-logger.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { InjectionToken, ModuleMetadata } from '@nestjs/common';
|
|
2
|
+
import type { NestFastifyApplication } from '@nestjs/platform-fastify';
|
|
3
|
+
import type { NamingStrategyInterface } from 'typeorm';
|
|
4
|
+
import type { BuildWebApplicationOptionsInterface } from '../build-web-application.js';
|
|
5
|
+
export interface PostgresWebApplicationOptionsInterface extends BuildWebApplicationOptionsInterface {
|
|
6
|
+
databaseConfigToken: InjectionToken;
|
|
7
|
+
image: string;
|
|
8
|
+
imports: NonNullable<ModuleMetadata['imports']>;
|
|
9
|
+
namingStrategy: NamingStrategyInterface;
|
|
10
|
+
}
|
|
11
|
+
export interface StartedWebApplicationInterface {
|
|
12
|
+
app: NestFastifyApplication;
|
|
13
|
+
stop(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export declare const startPostgresWebApplication: (options: PostgresWebApplicationOptionsInterface) => Promise<StartedWebApplicationInterface>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { buildWebApplication } from '../build-web-application.js';
|
|
2
|
+
import { postgresContainerDataSourceOptions, startPostgresContainer } from './test-data-source.js';
|
|
3
|
+
export const startPostgresWebApplication = async (options) => {
|
|
4
|
+
const { databaseConfigToken, image, imports, namingStrategy, ...buildOptions } = options;
|
|
5
|
+
const container = await startPostgresContainer(image);
|
|
6
|
+
const dataSourceOptions = {
|
|
7
|
+
...postgresContainerDataSourceOptions(container, namingStrategy),
|
|
8
|
+
migrationsRun: false,
|
|
9
|
+
synchronize: true,
|
|
10
|
+
};
|
|
11
|
+
try {
|
|
12
|
+
const app = await buildWebApplication(imports, [
|
|
13
|
+
{
|
|
14
|
+
provide: databaseConfigToken,
|
|
15
|
+
useValue: { createDataSourceOptions: () => dataSourceOptions },
|
|
16
|
+
},
|
|
17
|
+
], buildOptions);
|
|
18
|
+
await app.getHttpAdapter().getInstance()
|
|
19
|
+
.ready();
|
|
20
|
+
return {
|
|
21
|
+
app,
|
|
22
|
+
stop: async () => {
|
|
23
|
+
await app.close();
|
|
24
|
+
await container.stop();
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
await container.stop();
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { StartedTestContainer } from 'testcontainers';
|
|
2
|
-
import type { EntitySchema, MixedList, NamingStrategyInterface, QueryRunner } from 'typeorm';
|
|
2
|
+
import type { DataSourceOptions, EntitySchema, MixedList, NamingStrategyInterface, QueryRunner } from 'typeorm';
|
|
3
3
|
import { DataSource } from 'typeorm';
|
|
4
4
|
export declare const TEST_DATABASE_NAME = "db";
|
|
5
5
|
export declare const TEST_DATABASE_PORT = 5432;
|
|
6
6
|
export declare const startPostgresContainer: (image: string) => Promise<StartedTestContainer>;
|
|
7
|
+
export declare const postgresContainerDataSourceOptions: (container: StartedTestContainer, namingStrategy: NamingStrategyInterface) => DataSourceOptions;
|
|
7
8
|
export interface TestDataSourceOptionsInterface {
|
|
8
9
|
entities: MixedList<EntitySchema | Function | string>;
|
|
9
10
|
image: string;
|
|
@@ -12,28 +12,29 @@ export const startPostgresContainer = async (image) => new GenericContainer(imag
|
|
|
12
12
|
})
|
|
13
13
|
.withExposedPorts(TEST_DATABASE_PORT)
|
|
14
14
|
.start();
|
|
15
|
+
export const postgresContainerDataSourceOptions = (container, namingStrategy) => ({
|
|
16
|
+
database: TEST_DATABASE_NAME,
|
|
17
|
+
host: container.getHost(),
|
|
18
|
+
logging: false,
|
|
19
|
+
namingStrategy,
|
|
20
|
+
password: TEST_DATABASE_NAME,
|
|
21
|
+
port: container.getMappedPort(TEST_DATABASE_PORT),
|
|
22
|
+
ssl: false,
|
|
23
|
+
subscribers: [],
|
|
24
|
+
type: 'postgres',
|
|
25
|
+
username: TEST_DATABASE_NAME,
|
|
26
|
+
});
|
|
15
27
|
export const testDataSource = async ({ entities, image, isLogging = false, migrations, namingStrategy, shouldSynchronize = false, }) => {
|
|
16
|
-
const databaseName = TEST_DATABASE_NAME;
|
|
17
|
-
const databasePort = TEST_DATABASE_PORT;
|
|
18
28
|
const container = await startPostgresContainer(image);
|
|
19
|
-
const
|
|
20
|
-
|
|
29
|
+
const dataSource = new DataSource({
|
|
30
|
+
...postgresContainerDataSourceOptions(container, namingStrategy),
|
|
21
31
|
entities,
|
|
22
|
-
host: container.getHost(),
|
|
23
32
|
logging: isLogging,
|
|
24
33
|
maxQueryExecutionTime: 1000,
|
|
25
34
|
migrations,
|
|
26
35
|
migrationsRun: !shouldSynchronize,
|
|
27
|
-
namingStrategy,
|
|
28
|
-
password: databaseName,
|
|
29
|
-
port: container.getMappedPort(databasePort),
|
|
30
|
-
ssl: false,
|
|
31
|
-
subscribers: [],
|
|
32
36
|
synchronize: shouldSynchronize,
|
|
33
|
-
|
|
34
|
-
username: databaseName,
|
|
35
|
-
};
|
|
36
|
-
const dataSource = new DataSource(databaseConfig);
|
|
37
|
+
});
|
|
37
38
|
await dataSource.initialize();
|
|
38
39
|
const queryRunner = dataSource.createQueryRunner();
|
|
39
40
|
Object.assign(dataSource.manager, { queryRunner });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const replaceProcessEnvironment: (environment: Readonly<Record<string, string>>) => void;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
const TOOLCHAIN_NAMES = new Set([
|
|
3
|
+
'_',
|
|
4
|
+
'CI',
|
|
5
|
+
'COLORTERM',
|
|
6
|
+
'FORCE_COLOR',
|
|
7
|
+
'HOME',
|
|
8
|
+
'HOSTNAME',
|
|
9
|
+
'INIT_CWD',
|
|
10
|
+
'LANG',
|
|
11
|
+
'OLDPWD',
|
|
12
|
+
'PATH',
|
|
13
|
+
'PROJECT_CWD',
|
|
14
|
+
'PWD',
|
|
15
|
+
'SHELL',
|
|
16
|
+
'SHLVL',
|
|
17
|
+
'TEMP',
|
|
18
|
+
'TERM',
|
|
19
|
+
'TMP',
|
|
20
|
+
'TMPDIR',
|
|
21
|
+
'TZ',
|
|
22
|
+
'USER',
|
|
23
|
+
]);
|
|
24
|
+
const TOOLCHAIN_PREFIXES = [
|
|
25
|
+
'BERRY_',
|
|
26
|
+
'DOCKER_',
|
|
27
|
+
'HTTP_PROXY',
|
|
28
|
+
'HTTPS_PROXY',
|
|
29
|
+
'LC_',
|
|
30
|
+
'NODE_',
|
|
31
|
+
'NO_PROXY',
|
|
32
|
+
'NPM_',
|
|
33
|
+
'npm_',
|
|
34
|
+
'TESTCONTAINERS_',
|
|
35
|
+
'VITE_',
|
|
36
|
+
'VITEST_',
|
|
37
|
+
'XDG_',
|
|
38
|
+
'YARN_',
|
|
39
|
+
'http_proxy',
|
|
40
|
+
'https_proxy',
|
|
41
|
+
'no_proxy',
|
|
42
|
+
];
|
|
43
|
+
const isToolchainVariable = (name) => TOOLCHAIN_NAMES.has(name)
|
|
44
|
+
|| TOOLCHAIN_PREFIXES.some((prefix) => name.startsWith(prefix));
|
|
45
|
+
export const replaceProcessEnvironment = (environment) => {
|
|
46
|
+
const runtimeEnvironment = process.env;
|
|
47
|
+
for (const name of Object.keys(runtimeEnvironment)) {
|
|
48
|
+
if (!isToolchainVariable(name)) {
|
|
49
|
+
Reflect.deleteProperty(runtimeEnvironment, name);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
Object.assign(runtimeEnvironment, environment);
|
|
53
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export interface BcryptInterface {
|
|
2
|
+
compare(data: Buffer | string, encrypted: string): Promise<boolean>;
|
|
3
|
+
hash(data: Buffer | string, saltOrRounds: number | string): Promise<string>;
|
|
4
|
+
}
|
|
5
|
+
export declare const withFastBcryptCost: (bcrypt: BcryptInterface) => BcryptInterface;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const FAST_ROUNDS = 4;
|
|
2
|
+
const COST_FIELD_WIDTH = 2;
|
|
3
|
+
const COST_FIELD = /^(?<prefix>\$2[aby]\$)\d{2}\$/u;
|
|
4
|
+
const withCost = (hashed, rounds) => {
|
|
5
|
+
const cost = String(rounds).padStart(COST_FIELD_WIDTH, '0');
|
|
6
|
+
return hashed.replace(COST_FIELD, (_match, prefix) => `${prefix}${cost}$`);
|
|
7
|
+
};
|
|
8
|
+
export const withFastBcryptCost = (bcrypt) => ({
|
|
9
|
+
compare: async (data, encrypted) => bcrypt.compare(data, withCost(encrypted, FAST_ROUNDS)),
|
|
10
|
+
hash: async (data, saltOrRounds) => (typeof saltOrRounds === 'number'
|
|
11
|
+
? withCost(await bcrypt.hash(data, Math.min(saltOrRounds, FAST_ROUNDS)), saltOrRounds)
|
|
12
|
+
: bcrypt.hash(data, saltOrRounds)),
|
|
13
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
import { withFastBcryptCost } from './fast-bcrypt-cost.js';
|
|
3
|
+
vi.mock('bcrypt', async (importOriginal) => {
|
|
4
|
+
const bcrypt = await importOriginal();
|
|
5
|
+
const fast = withFastBcryptCost(bcrypt);
|
|
6
|
+
return { ...bcrypt, ...fast, default: { ...bcrypt.default, ...fast } };
|
|
7
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@application-kit/testing",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Тестовая инфраструктура NestJS-приложений: шины CQRS, логгер, фейковый microservice, сборка web-приложения, Postgres в testcontainers, раскладка ячеек CI по проектам vitest и фабрики конфигов vitest",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Тестовая инфраструктура NestJS-приложений: шины CQRS, логгер, фейковый microservice, сборка web-приложения, Postgres в testcontainers, запуск e2e-приложения, изоляция окружения прогона, дешёвый bcrypt для unit-прогона, раскладка ячеек CI по проектам vitest и фабрики конфигов vitest",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cqrs",
|
|
7
7
|
"nestjs",
|
|
@@ -53,6 +53,16 @@
|
|
|
53
53
|
"types": "./dist/cjs/vitest/index.d.ts",
|
|
54
54
|
"default": "./dist/cjs/vitest/index.js"
|
|
55
55
|
}
|
|
56
|
+
},
|
|
57
|
+
"./vitest/fast-bcrypt": {
|
|
58
|
+
"import": {
|
|
59
|
+
"types": "./dist/esm/vitest/setup/fast-bcrypt.d.ts",
|
|
60
|
+
"default": "./dist/esm/vitest/setup/fast-bcrypt.js"
|
|
61
|
+
},
|
|
62
|
+
"require": {
|
|
63
|
+
"types": "./dist/cjs/vitest/setup/fast-bcrypt.d.ts",
|
|
64
|
+
"default": "./dist/cjs/vitest/setup/fast-bcrypt.js"
|
|
65
|
+
}
|
|
56
66
|
}
|
|
57
67
|
},
|
|
58
68
|
"main": "./dist/cjs/index.js",
|
|
@@ -65,13 +75,13 @@
|
|
|
65
75
|
"dist"
|
|
66
76
|
],
|
|
67
77
|
"scripts": {
|
|
68
|
-
"build": "rm -rf ./dist && node ../../scripts/barrels.mjs create ./src --entry-only=bin,stryker,vitest && tsc --project tsconfig.build.json && tsc --project tsconfig.build.cjs.json && node ../../scripts/barrels.mjs clean ./src && node ../../scripts/mark-cjs.mjs ./dist/cjs",
|
|
78
|
+
"build": "rm -rf ./dist && node ../../scripts/barrels.mjs create ./src --entry-only=bin,setup,stryker,vitest && tsc --project tsconfig.build.json && tsc --project tsconfig.build.cjs.json && node ../../scripts/barrels.mjs clean ./src && node ../../scripts/mark-cjs.mjs ./dist/cjs",
|
|
69
79
|
"lint:tsc": "tsc --noEmit --pretty --project .",
|
|
70
80
|
"test:mutation": "stryker run",
|
|
71
81
|
"test:unit": "vitest run"
|
|
72
82
|
},
|
|
73
83
|
"dependencies": {
|
|
74
|
-
"@application-kit/bootstrap": "^0.2.
|
|
84
|
+
"@application-kit/bootstrap": "^0.2.2"
|
|
75
85
|
},
|
|
76
86
|
"devDependencies": {
|
|
77
87
|
"@nestjs/common": "^11.2.7",
|
|
@@ -83,6 +93,7 @@
|
|
|
83
93
|
"@stryker-mutator/core": "^10.0.0",
|
|
84
94
|
"@stryker-mutator/vitest-runner": "^10.0.0",
|
|
85
95
|
"@types/node": "^26.2.0",
|
|
96
|
+
"bcrypt": "^6.0.0",
|
|
86
97
|
"nestjs-pino": "^4.6.1",
|
|
87
98
|
"oxide.ts": "^1.1.0",
|
|
88
99
|
"pino": "^10.3.1",
|
|
@@ -102,6 +113,7 @@
|
|
|
102
113
|
"@nestjs/microservices": "^11.0.0",
|
|
103
114
|
"@nestjs/platform-fastify": "^11.0.0",
|
|
104
115
|
"@nestjs/testing": "^11.0.0",
|
|
116
|
+
"bcrypt": "^6.0.0",
|
|
105
117
|
"nestjs-pino": "^4.6.1",
|
|
106
118
|
"oxide.ts": "^1.1.0",
|
|
107
119
|
"rxjs": "^7.8.2",
|
|
@@ -111,6 +123,9 @@
|
|
|
111
123
|
"vitest": "^4.1.0"
|
|
112
124
|
},
|
|
113
125
|
"peerDependenciesMeta": {
|
|
126
|
+
"bcrypt": {
|
|
127
|
+
"optional": true
|
|
128
|
+
},
|
|
114
129
|
"vite": {
|
|
115
130
|
"optional": true
|
|
116
131
|
},
|