@application-kit/testing 0.5.0 → 0.7.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 +28 -0
- package/README.md +33 -6
- 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/stryker/define-stryker-layers.js +11 -5
- package/dist/cjs/vitest/application-vitest-config.d.ts +1 -0
- package/dist/cjs/vitest/application-vitest-config.js +13 -2
- 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/stryker/define-stryker-layers.js +11 -5
- package/dist/esm/vitest/application-vitest-config.d.ts +1 -0
- package/dist/esm/vitest/application-vitest-config.js +11 -1
- 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 +20 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# @application-kit/testing
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- fb35f7c: Поддержка vitest 5 (peer `^4.1.0 || ^5.0.0`): фабрики кладут кеш модулей ключом `fsModuleCache` верхнего уровня на
|
|
8
|
+
vitest 5 и `experimental.fsModuleCache` на vitest 4, без deprecation в каждом прогоне. Новый
|
|
9
|
+
`defineApplicationMutationConfig(unitConfig)` собирает конфиг vitest для Stryker: кеш модулей выключен, на vitest 5
|
|
10
|
+
файлы спек изолированы — без этого vitest-runner Stryker собирает покрытие по тестам только с первого файла спек
|
|
11
|
+
воркера. Переход приложения на vitest 5 — с этой функцией в `vitest.stryker.config.mts`: ручное
|
|
12
|
+
`experimental.fsModuleCache: false` поверх фабрики там не действует.
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- aa9114b: `defineStrykerLayers` перед прогоном выбрасывает из incremental-отчёта группы мутанты со статусом NoCoverage. Stryker
|
|
17
|
+
переиспользует их, не сверяя мутант с `ignoreStatic`, и статус, записанный прогоном с потерянным покрытием (vitest 5
|
|
18
|
+
без изоляции спек), переживал починку раннера: статические мутанты оставались NoCoverage и держали score ниже порога.
|
|
19
|
+
Заново такой мутант классифицируется по покрытию dry run, тесты на него не тратятся.
|
|
20
|
+
|
|
21
|
+
## 0.6.0
|
|
22
|
+
|
|
23
|
+
### Minor Changes
|
|
24
|
+
|
|
25
|
+
- d790ac9: Обвязка e2e и unit-прогона приложения: `replaceProcessEnvironment(environment)` задаёт окружение прогона целиком (снимает
|
|
26
|
+
всё, кроме переменных инструментария, и выставляет набор приложения), `startPostgresWebApplication(options)` поднимает
|
|
27
|
+
настоящий web-граф на временном Postgres с подменой конфига БД по токену, `postgresContainerDataSourceOptions` — общие
|
|
28
|
+
опции подключения к контейнеру для `testDataSource` и запуска. Setup-файл `@application-kit/testing/vitest/fast-bcrypt`
|
|
29
|
+
подменяет `bcrypt` дешёвой стоимостью с сохранением запрошенной в строке хеша; `bcrypt` — optional peer.
|
|
30
|
+
|
|
3
31
|
## 0.5.0
|
|
4
32
|
|
|
5
33
|
### 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
|
})
|
|
@@ -74,13 +84,27 @@ export default defineApplicationEndToEndConfig({
|
|
|
74
84
|
`server.deps.inline` (`inlineDependencies`) — у приложения. Фабрики решают сами:
|
|
75
85
|
|
|
76
86
|
- оба профиля — `globals`, `isolate: false`, `testTimeout` 20 с (таймаут ловит зависший промис, а не медленный раннер),
|
|
77
|
-
кеш трансформов `.vitest-module-cache` (путь — контракт кеша джоб `ts:test:*` шаблона CI
|
|
87
|
+
кеш трансформов `.vitest-module-cache` (путь — контракт кеша джоб `ts:test:*` шаблона CI; ключ — `fsModuleCache`
|
|
88
|
+
верхнего уровня на vitest 5 и `experimental.fsModuleCache` на vitest 4), покрытие v8 с отчётом
|
|
78
89
|
`text` и `skipFull`; `@application-kit/graphql` идёт через Vite, а не нативно через Node — иначе ошибка из пакета и код
|
|
79
90
|
приложения видят разные копии `graphql`, и `instanceof GraphQLError` ложен;
|
|
80
91
|
- unit — проекты `server` (`node`, спеки `specs` без `excludeSpecs` и `clientSpecs`) и `client` (`jsdom`, `clientSpecs`),
|
|
81
92
|
ячейки шарда CI и `ProjectShardSequencer` (ниже);
|
|
82
93
|
- e2e — окружение `node` и алиас `graphql` на CJS-вход `node_modules/graphql/index.js` под `root`: Vite для SSR берёт
|
|
83
|
-
ESM-сборку `graphql`, внешний `mercurius` — CJS, и без алиаса схема и валидация запроса живут в разных realm
|
|
94
|
+
ESM-сборку `graphql`, внешний `mercurius` — CJS, и без алиаса схема и валидация запроса живут в разных realm;
|
|
95
|
+
- мутации — `defineApplicationMutationConfig(unitConfig)`: unit-конфиг для `vitestConfigFile` Stryker без кеша модулей
|
|
96
|
+
(под Stryker он отдаёт тестам трансформ без активного мутанта), на vitest 5 — ещё и с `isolate: true`: без изоляции
|
|
97
|
+
vitest-runner Stryker собирает покрытие по тестам только с первого файла спек воркера. Ручное
|
|
98
|
+
`experimental.fsModuleCache: false` поверх фабрики на vitest 5 не действует — верхний ключ фабрики главнее.
|
|
99
|
+
|
|
100
|
+
## Вход `/vitest/fast-bcrypt`: дешёвый bcrypt
|
|
101
|
+
|
|
102
|
+
Setup-файл unit-профиля (`setupFiles: ['@application-kit/testing/vitest/fast-bcrypt']`): `vi.mock('bcrypt')` считает хеш
|
|
103
|
+
с cost 4 (≈ 1 мс против ≈ 300 мс у cost 12), а поле стоимости в строке переписывает на запрошенную — `getRounds` и
|
|
104
|
+
решение о перехешировании видят настоящую стоимость; `compare` перед сверкой возвращает поле на 4. Сверяются только хеши,
|
|
105
|
+
выпущенные подменой: литерал хеша с настоящим cost 12 не сойдётся. Подмена видна коду, прошедшему трансформ vitest
|
|
106
|
+
(исходники приложения); внешний пакет, импортирующий `bcrypt`, ускоряется только через `inlineDependencies`. E2e-профилю
|
|
107
|
+
файл не нужен.
|
|
84
108
|
|
|
85
109
|
## Ячейки CI по проектам
|
|
86
110
|
|
|
@@ -117,6 +141,9 @@ export default defineStrykerConfig({
|
|
|
117
141
|
|
|
118
142
|
// vitest.config.mts
|
|
119
143
|
coverage: { exclude: [...CODE_WITHOUT_LOGIC, '**/*.html'], include: ['src'], thresholds: { lines: 95 } }
|
|
144
|
+
|
|
145
|
+
// vitest.stryker.config.mts — defineApplicationMutationConfig из входа /vitest
|
|
146
|
+
export default defineApplicationMutationConfig(unitConfig)
|
|
120
147
|
```
|
|
121
148
|
|
|
122
149
|
`defineStrykerLayers` режет прогон на группы каталогов: ячейка CI мутирует одну группу со своим порогом `break` (общий
|
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;
|
|
@@ -15,6 +15,7 @@ const MATRIX_PATTERN = (variable) => {
|
|
|
15
15
|
};
|
|
16
16
|
const sorted = (values) => [...values].toSorted((left, right) => left.localeCompare(right));
|
|
17
17
|
const isUnderDirectory = (file, directory) => file.startsWith(`${directory}/`);
|
|
18
|
+
const isUncovered = (mutant) => mutant.status === 'NoCoverage';
|
|
18
19
|
const rootsOf = (options) => [
|
|
19
20
|
...options.scope.directories,
|
|
20
21
|
...options.scope.parents.flatMap((parent) => (0, node_fs_1.readdirSync)(node_path_1.default.join(options.root, parent), { withFileTypes: true })
|
|
@@ -53,15 +54,20 @@ const pruneIncrementalReport = (incrementalFile, isInScope, configName) => {
|
|
|
53
54
|
return;
|
|
54
55
|
}
|
|
55
56
|
const report = JSON.parse((0, node_fs_1.readFileSync)(incrementalFile, 'utf8'));
|
|
56
|
-
const files = report.files ?? {};
|
|
57
|
-
const
|
|
58
|
-
|
|
57
|
+
const files = Object.entries(report.files ?? {});
|
|
58
|
+
const own = files.filter(([file]) => isInScope(file));
|
|
59
|
+
const uncovered = own.flatMap(([, data]) => data.mutants).filter((mutant) => isUncovered(mutant));
|
|
60
|
+
if (own.length === files.length && uncovered.length === 0) {
|
|
59
61
|
return;
|
|
60
62
|
}
|
|
61
|
-
report.files = Object.fromEntries(
|
|
63
|
+
report.files = Object.fromEntries(own.map(([file, data]) => [
|
|
64
|
+
file,
|
|
65
|
+
{ ...data, mutants: data.mutants.filter((mutant) => !isUncovered(mutant)) },
|
|
66
|
+
]));
|
|
62
67
|
(0, node_fs_1.writeFileSync)(`${incrementalFile}.tmp`, JSON.stringify(report));
|
|
63
68
|
(0, node_fs_1.renameSync)(`${incrementalFile}.tmp`, incrementalFile);
|
|
64
|
-
console.error(`${configName}: из ${incrementalFile} убрано файлов вне области группы: ${
|
|
69
|
+
console.error(`${configName}: из ${incrementalFile} убрано файлов вне области группы: ${files.length - own.length}, `
|
|
70
|
+
+ `мутантов NoCoverage: ${uncovered.length}`);
|
|
65
71
|
};
|
|
66
72
|
const defineStrykerLayers = (options) => {
|
|
67
73
|
const variable = options.layerVariable ?? DEFAULT_LAYER_VARIABLE;
|
|
@@ -2,3 +2,4 @@ import type { ViteUserConfig } from 'vitest/config';
|
|
|
2
2
|
import type { ApplicationTestConfigOptionsInterface, ApplicationUnitConfigOptionsInterface } from './application-vitest-config-options.interface.js';
|
|
3
3
|
export declare const defineApplicationUnitConfig: (options: ApplicationUnitConfigOptionsInterface) => ViteUserConfig;
|
|
4
4
|
export declare const defineApplicationEndToEndConfig: (options: ApplicationTestConfigOptionsInterface) => ViteUserConfig;
|
|
5
|
+
export declare const defineApplicationMutationConfig: (unitConfig: ViteUserConfig) => ViteUserConfig;
|
|
@@ -3,13 +3,20 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.defineApplicationEndToEndConfig = exports.defineApplicationUnitConfig = void 0;
|
|
6
|
+
exports.defineApplicationMutationConfig = exports.defineApplicationEndToEndConfig = exports.defineApplicationUnitConfig = void 0;
|
|
7
7
|
const node_path_1 = __importDefault(require("node:path"));
|
|
8
8
|
const node_process_1 = __importDefault(require("node:process"));
|
|
9
9
|
const vite_1 = require("vite");
|
|
10
|
+
const node_1 = require("vitest/node");
|
|
10
11
|
const project_shard_sequencer_js_1 = require("./project-shard-sequencer.js");
|
|
11
12
|
const PACKAGES_THROUGH_VITE = ['@application-kit/graphql'];
|
|
12
13
|
const TEST_TIMEOUT = 20_000;
|
|
14
|
+
const MODULE_CACHE_PATH = '.vitest-module-cache';
|
|
15
|
+
const IS_VITEST_5 = Number(node_1.version.split('.', 1)[0]) >= 5;
|
|
16
|
+
const moduleCacheOf = (isEnabled) => {
|
|
17
|
+
const cache = { fsModuleCache: isEnabled, fsModuleCachePath: MODULE_CACHE_PATH };
|
|
18
|
+
return IS_VITEST_5 ? cache : { experimental: cache };
|
|
19
|
+
};
|
|
13
20
|
const cellOf = (argv) => {
|
|
14
21
|
const shard = (0, project_shard_sequencer_js_1.parseShard)(argv);
|
|
15
22
|
if (shard === undefined) {
|
|
@@ -28,7 +35,7 @@ const coverageOf = (scope) => ({
|
|
|
28
35
|
thresholds: scope.thresholds,
|
|
29
36
|
});
|
|
30
37
|
const sharedTestOptions = (options) => ({
|
|
31
|
-
|
|
38
|
+
...moduleCacheOf(true),
|
|
32
39
|
globals: true,
|
|
33
40
|
isolate: false,
|
|
34
41
|
server: { deps: { inline: [...PACKAGES_THROUGH_VITE, ...(options.inlineDependencies ?? [])] } },
|
|
@@ -85,3 +92,7 @@ const defineApplicationEndToEndConfig = (options) => {
|
|
|
85
92
|
});
|
|
86
93
|
};
|
|
87
94
|
exports.defineApplicationEndToEndConfig = defineApplicationEndToEndConfig;
|
|
95
|
+
const defineApplicationMutationConfig = (unitConfig) => (0, vite_1.mergeConfig)(unitConfig, {
|
|
96
|
+
test: { ...moduleCacheOf(false), ...(IS_VITEST_5 && { isolate: true }) },
|
|
97
|
+
});
|
|
98
|
+
exports.defineApplicationMutationConfig = defineApplicationMutationConfig;
|
|
@@ -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
|
+
};
|
|
@@ -9,6 +9,7 @@ const MATRIX_PATTERN = (variable) => {
|
|
|
9
9
|
};
|
|
10
10
|
const sorted = (values) => [...values].toSorted((left, right) => left.localeCompare(right));
|
|
11
11
|
const isUnderDirectory = (file, directory) => file.startsWith(`${directory}/`);
|
|
12
|
+
const isUncovered = (mutant) => mutant.status === 'NoCoverage';
|
|
12
13
|
const rootsOf = (options) => [
|
|
13
14
|
...options.scope.directories,
|
|
14
15
|
...options.scope.parents.flatMap((parent) => readdirSync(path.join(options.root, parent), { withFileTypes: true })
|
|
@@ -47,15 +48,20 @@ const pruneIncrementalReport = (incrementalFile, isInScope, configName) => {
|
|
|
47
48
|
return;
|
|
48
49
|
}
|
|
49
50
|
const report = JSON.parse(readFileSync(incrementalFile, 'utf8'));
|
|
50
|
-
const files = report.files ?? {};
|
|
51
|
-
const
|
|
52
|
-
|
|
51
|
+
const files = Object.entries(report.files ?? {});
|
|
52
|
+
const own = files.filter(([file]) => isInScope(file));
|
|
53
|
+
const uncovered = own.flatMap(([, data]) => data.mutants).filter((mutant) => isUncovered(mutant));
|
|
54
|
+
if (own.length === files.length && uncovered.length === 0) {
|
|
53
55
|
return;
|
|
54
56
|
}
|
|
55
|
-
report.files = Object.fromEntries(
|
|
57
|
+
report.files = Object.fromEntries(own.map(([file, data]) => [
|
|
58
|
+
file,
|
|
59
|
+
{ ...data, mutants: data.mutants.filter((mutant) => !isUncovered(mutant)) },
|
|
60
|
+
]));
|
|
56
61
|
writeFileSync(`${incrementalFile}.tmp`, JSON.stringify(report));
|
|
57
62
|
renameSync(`${incrementalFile}.tmp`, incrementalFile);
|
|
58
|
-
console.error(`${configName}: из ${incrementalFile} убрано файлов вне области группы: ${
|
|
63
|
+
console.error(`${configName}: из ${incrementalFile} убрано файлов вне области группы: ${files.length - own.length}, `
|
|
64
|
+
+ `мутантов NoCoverage: ${uncovered.length}`);
|
|
59
65
|
};
|
|
60
66
|
export const defineStrykerLayers = (options) => {
|
|
61
67
|
const variable = options.layerVariable ?? DEFAULT_LAYER_VARIABLE;
|
|
@@ -2,3 +2,4 @@ import type { ViteUserConfig } from 'vitest/config';
|
|
|
2
2
|
import type { ApplicationTestConfigOptionsInterface, ApplicationUnitConfigOptionsInterface } from './application-vitest-config-options.interface.js';
|
|
3
3
|
export declare const defineApplicationUnitConfig: (options: ApplicationUnitConfigOptionsInterface) => ViteUserConfig;
|
|
4
4
|
export declare const defineApplicationEndToEndConfig: (options: ApplicationTestConfigOptionsInterface) => ViteUserConfig;
|
|
5
|
+
export declare const defineApplicationMutationConfig: (unitConfig: ViteUserConfig) => ViteUserConfig;
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import process from 'node:process';
|
|
3
3
|
import { mergeConfig } from 'vite';
|
|
4
|
+
import { version } from 'vitest/node';
|
|
4
5
|
import { parseShard, ProjectShardSequencer } from './project-shard-sequencer.js';
|
|
5
6
|
const PACKAGES_THROUGH_VITE = ['@application-kit/graphql'];
|
|
6
7
|
const TEST_TIMEOUT = 20_000;
|
|
8
|
+
const MODULE_CACHE_PATH = '.vitest-module-cache';
|
|
9
|
+
const IS_VITEST_5 = Number(version.split('.', 1)[0]) >= 5;
|
|
10
|
+
const moduleCacheOf = (isEnabled) => {
|
|
11
|
+
const cache = { fsModuleCache: isEnabled, fsModuleCachePath: MODULE_CACHE_PATH };
|
|
12
|
+
return IS_VITEST_5 ? cache : { experimental: cache };
|
|
13
|
+
};
|
|
7
14
|
const cellOf = (argv) => {
|
|
8
15
|
const shard = parseShard(argv);
|
|
9
16
|
if (shard === undefined) {
|
|
@@ -22,7 +29,7 @@ const coverageOf = (scope) => ({
|
|
|
22
29
|
thresholds: scope.thresholds,
|
|
23
30
|
});
|
|
24
31
|
const sharedTestOptions = (options) => ({
|
|
25
|
-
|
|
32
|
+
...moduleCacheOf(true),
|
|
26
33
|
globals: true,
|
|
27
34
|
isolate: false,
|
|
28
35
|
server: { deps: { inline: [...PACKAGES_THROUGH_VITE, ...(options.inlineDependencies ?? [])] } },
|
|
@@ -77,3 +84,6 @@ export const defineApplicationEndToEndConfig = (options) => {
|
|
|
77
84
|
},
|
|
78
85
|
});
|
|
79
86
|
};
|
|
87
|
+
export const defineApplicationMutationConfig = (unitConfig) => mergeConfig(unitConfig, {
|
|
88
|
+
test: { ...moduleCacheOf(false), ...(IS_VITEST_5 && { isolate: true }) },
|
|
89
|
+
});
|
|
@@ -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.7.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,15 +113,19 @@
|
|
|
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",
|
|
108
120
|
"testcontainers": "^12.1.0",
|
|
109
121
|
"typeorm": "^1.0.0",
|
|
110
122
|
"vite": "^8.0.0",
|
|
111
|
-
"vitest": "^4.1.0"
|
|
123
|
+
"vitest": "^4.1.0 || ^5.0.0"
|
|
112
124
|
},
|
|
113
125
|
"peerDependenciesMeta": {
|
|
126
|
+
"bcrypt": {
|
|
127
|
+
"optional": true
|
|
128
|
+
},
|
|
114
129
|
"vite": {
|
|
115
130
|
"optional": true
|
|
116
131
|
},
|