@application-kit/testing 0.4.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 +21 -0
- package/README.md +82 -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/stryker/bin/application-mutation-layer.d.ts +2 -0
- package/dist/cjs/stryker/bin/application-mutation-layer.js +64 -0
- package/dist/cjs/stryker/bin/index.d.ts +1 -0
- package/dist/cjs/stryker/bin/index.js +17 -0
- package/dist/cjs/stryker/code-without-logic.d.ts +1 -0
- package/dist/cjs/stryker/code-without-logic.js +31 -0
- package/dist/cjs/stryker/decide-layer-run.d.ts +12 -0
- package/dist/cjs/stryker/decide-layer-run.js +40 -0
- package/dist/cjs/stryker/define-stryker-config.d.ts +2 -0
- package/dist/cjs/stryker/define-stryker-config.js +24 -0
- package/dist/cjs/stryker/define-stryker-layers.d.ts +2 -0
- package/dist/cjs/stryker/define-stryker-layers.js +113 -0
- package/dist/cjs/stryker/index.d.ts +7 -0
- package/dist/cjs/stryker/index.js +23 -0
- package/dist/cjs/stryker/mutation-layer-cli.d.ts +9 -0
- package/dist/cjs/stryker/mutation-layer-cli.js +64 -0
- package/dist/cjs/stryker/stryker-config.interface.d.ts +23 -0
- package/dist/cjs/stryker/stryker-config.interface.js +2 -0
- package/dist/cjs/stryker/stryker-layers.interface.d.ts +31 -0
- package/dist/cjs/stryker/stryker-layers.interface.js +2 -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/stryker/bin/application-mutation-layer.d.ts +2 -0
- package/dist/esm/stryker/bin/application-mutation-layer.js +26 -0
- package/dist/esm/stryker/bin/index.d.ts +1 -0
- package/dist/esm/stryker/bin/index.js +1 -0
- package/dist/esm/stryker/code-without-logic.d.ts +1 -0
- package/dist/esm/stryker/code-without-logic.js +28 -0
- package/dist/esm/stryker/decide-layer-run.d.ts +12 -0
- package/dist/esm/stryker/decide-layer-run.js +36 -0
- package/dist/esm/stryker/define-stryker-config.d.ts +2 -0
- package/dist/esm/stryker/define-stryker-config.js +20 -0
- package/dist/esm/stryker/define-stryker-layers.d.ts +2 -0
- package/dist/esm/stryker/define-stryker-layers.js +106 -0
- package/dist/esm/stryker/index.d.ts +7 -0
- package/dist/esm/stryker/index.js +7 -0
- package/dist/esm/stryker/mutation-layer-cli.d.ts +9 -0
- package/dist/esm/stryker/mutation-layer-cli.js +60 -0
- package/dist/esm/stryker/stryker-config.interface.d.ts +23 -0
- package/dist/esm/stryker/stryker-config.interface.js +1 -0
- package/dist/esm/stryker/stryker-layers.interface.d.ts +31 -0
- package/dist/esm/stryker/stryker-layers.interface.js +1 -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 +32 -4
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const isUnder = (file, directory) => file.startsWith(`${directory}/`);
|
|
2
|
+
const isInLayer = (file, layerPaths) => {
|
|
3
|
+
let isHit = false;
|
|
4
|
+
for (const entry of layerPaths) {
|
|
5
|
+
if (entry.startsWith('!')) {
|
|
6
|
+
if (isUnder(file, entry.slice(1))) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
else if (isUnder(file, entry)) {
|
|
11
|
+
isHit = true;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return isHit;
|
|
15
|
+
};
|
|
16
|
+
const runBecause = (reason) => ({ reason, run: true, touched: [] });
|
|
17
|
+
export const decideLayerRun = (input) => {
|
|
18
|
+
if (input.changed.length === 0) {
|
|
19
|
+
return runBecause('дифф пуст');
|
|
20
|
+
}
|
|
21
|
+
if (input.changed.some((file) => input.configFiles.test(file))) {
|
|
22
|
+
return runBecause('тронут конфиг прогона');
|
|
23
|
+
}
|
|
24
|
+
const source = input.changed.filter((file) => isUnder(file, input.sourceRoot));
|
|
25
|
+
if (source.some((file) => input.roots.every((root) => !isUnder(file, root)))) {
|
|
26
|
+
return runBecause(`тронут ${input.sourceRoot}/ вне групп`);
|
|
27
|
+
}
|
|
28
|
+
const shared = input.sharedDirectories.find((directory) => input.changed.some((file) => isUnder(file, directory)));
|
|
29
|
+
if (shared !== undefined) {
|
|
30
|
+
return runBecause(`тронута общая тестовая инфраструктура ${shared}`);
|
|
31
|
+
}
|
|
32
|
+
const touched = input.changed.filter((file) => isInLayer(file, input.layerPaths));
|
|
33
|
+
return touched.length === 0
|
|
34
|
+
? { reason: 'дифф группу не трогает', run: false, touched }
|
|
35
|
+
: { reason: 'группа тронута', run: true, touched };
|
|
36
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { CODE_WITHOUT_LOGIC } from './code-without-logic.js';
|
|
2
|
+
const DEFAULT_IGNORE_PATTERNS = ['coverage', 'dist', 'node_modules', 'reports'];
|
|
3
|
+
const compare = (left, right) => left.localeCompare(right);
|
|
4
|
+
export const defineStrykerConfig = (options) => ({
|
|
5
|
+
clearTextReporter: { reportTests: false },
|
|
6
|
+
dryRunTimeoutMinutes: 10,
|
|
7
|
+
ignorePatterns: [...DEFAULT_IGNORE_PATTERNS, ...(options.ignorePatterns ?? [])].toSorted(compare),
|
|
8
|
+
ignoreStatic: true,
|
|
9
|
+
incremental: true,
|
|
10
|
+
incrementalFile: options.incrementalFile ?? '.stryker-cache/incremental.json',
|
|
11
|
+
logLevel: 'warn',
|
|
12
|
+
mutate: [
|
|
13
|
+
...options.mutate.include,
|
|
14
|
+
...[...CODE_WITHOUT_LOGIC, ...(options.mutate.exclude ?? [])].map((pattern) => `!${pattern}`),
|
|
15
|
+
],
|
|
16
|
+
tempDirName: options.tempDirName ?? '/tmp/.stryker-tmp',
|
|
17
|
+
testRunner: 'vitest',
|
|
18
|
+
thresholds: options.thresholds,
|
|
19
|
+
vitest: { configFile: options.vitestConfigFile, dir: './' },
|
|
20
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
const DEFAULT_LAYER_VARIABLE = 'TYPESCRIPT_MUTATION_LAYER';
|
|
5
|
+
const TEMPLATE_LAYER_SENTINEL = 'all';
|
|
6
|
+
const MATRIX_PATTERN = (variable) => {
|
|
7
|
+
const source = [variable, ':\\p{White_Space}*\\[([^\\]]*)\\]'].join('');
|
|
8
|
+
return new RegExp(source, 'u');
|
|
9
|
+
};
|
|
10
|
+
const sorted = (values) => [...values].toSorted((left, right) => left.localeCompare(right));
|
|
11
|
+
const isUnderDirectory = (file, directory) => file.startsWith(`${directory}/`);
|
|
12
|
+
const rootsOf = (options) => [
|
|
13
|
+
...options.scope.directories,
|
|
14
|
+
...options.scope.parents.flatMap((parent) => readdirSync(path.join(options.root, parent), { withFileTypes: true })
|
|
15
|
+
.filter((entry) => entry.isDirectory())
|
|
16
|
+
.map((entry) => `${parent}/${entry.name}`)),
|
|
17
|
+
];
|
|
18
|
+
const assertLayout = (layers, roots, configName) => {
|
|
19
|
+
const paths = Object.values(layers).flatMap((layer) => layer.paths);
|
|
20
|
+
const unassigned = roots.filter((root) => !paths.includes(root));
|
|
21
|
+
const duplicated = paths.filter((entry, index) => paths.indexOf(entry) !== index);
|
|
22
|
+
const orphaned = paths.filter((entry) => roots.every((root) => !(entry === root || isUnderDirectory(entry, root))));
|
|
23
|
+
if (unassigned.length > 0 || duplicated.length > 0 || orphaned.length > 0) {
|
|
24
|
+
throw new Error(`${configName}: каталог обязан быть ровно в одной группе — без группы: [${unassigned.join(', ')}], `
|
|
25
|
+
+ `в двух: [${duplicated.join(', ')}], вне корней: [${orphaned.join(', ')}]`);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const assertMatrix = (options, variable, configName) => {
|
|
29
|
+
const matrix = MATRIX_PATTERN(variable).exec(readFileSync(options.ciFile).toString());
|
|
30
|
+
const matrixLayers = sorted((matrix?.[1] ?? '').split(',').map((name) => name.trim())
|
|
31
|
+
.filter(Boolean));
|
|
32
|
+
const layerNames = sorted(Object.keys(options.layers));
|
|
33
|
+
if (matrixLayers.join(',') !== layerNames.join(',')) {
|
|
34
|
+
throw new Error(`${configName}: матрица [${matrixLayers.join(', ')}] в ${path.basename(options.ciFile)} `
|
|
35
|
+
+ `расходится с группами [${layerNames.join(', ')}]`);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const carvedOutOf = (allPaths, layer) => {
|
|
39
|
+
const isCarvedOut = (entry) => {
|
|
40
|
+
const isOwn = layer.paths.includes(entry);
|
|
41
|
+
return !isOwn && layer.paths.some((own) => isUnderDirectory(entry, own));
|
|
42
|
+
};
|
|
43
|
+
return allPaths.filter((entry) => isCarvedOut(entry));
|
|
44
|
+
};
|
|
45
|
+
const pruneIncrementalReport = (incrementalFile, isInScope, configName) => {
|
|
46
|
+
if (!existsSync(incrementalFile)) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const report = JSON.parse(readFileSync(incrementalFile, 'utf8'));
|
|
50
|
+
const files = report.files ?? {};
|
|
51
|
+
const foreign = Object.keys(files).filter((file) => !isInScope(file));
|
|
52
|
+
if (foreign.length === 0) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
report.files = Object.fromEntries(Object.entries(files).filter(([file]) => isInScope(file)));
|
|
56
|
+
writeFileSync(`${incrementalFile}.tmp`, JSON.stringify(report));
|
|
57
|
+
renameSync(`${incrementalFile}.tmp`, incrementalFile);
|
|
58
|
+
console.error(`${configName}: из ${incrementalFile} убрано файлов вне области группы: ${foreign.length}`);
|
|
59
|
+
};
|
|
60
|
+
export const defineStrykerLayers = (options) => {
|
|
61
|
+
const variable = options.layerVariable ?? DEFAULT_LAYER_VARIABLE;
|
|
62
|
+
const configName = options.configName ?? 'stryker layers';
|
|
63
|
+
const names = Object.keys(options.layers);
|
|
64
|
+
const allPaths = Object.values(options.layers).flatMap((layer) => layer.paths);
|
|
65
|
+
const roots = rootsOf(options);
|
|
66
|
+
assertLayout(options.layers, roots, configName);
|
|
67
|
+
assertMatrix(options, variable, configName);
|
|
68
|
+
const layerOf = (name) => {
|
|
69
|
+
const layer = options.layers[String(name)];
|
|
70
|
+
if (layer === undefined) {
|
|
71
|
+
throw new Error(`${variable}=${String(name)}: ожидается одна из ${names.join(', ')}`);
|
|
72
|
+
}
|
|
73
|
+
return { layer, name: String(name) };
|
|
74
|
+
};
|
|
75
|
+
const pathsOf = (name) => {
|
|
76
|
+
const { layer } = layerOf(name);
|
|
77
|
+
return [...layer.paths, ...carvedOutOf(allPaths, layer).map((entry) => `!${entry}`)];
|
|
78
|
+
};
|
|
79
|
+
const raw = process.env[variable];
|
|
80
|
+
const isLoadedByStryker = process.argv.some((argument) => argument.includes('@stryker-mutator'));
|
|
81
|
+
const isUnset = raw === undefined || (!isLoadedByStryker && raw === TEMPLATE_LAYER_SENTINEL);
|
|
82
|
+
const { layer, name } = layerOf(isUnset ? options.defaultLayer : raw);
|
|
83
|
+
const carvedOut = carvedOutOf(allPaths, layer);
|
|
84
|
+
const incrementalFile = `.stryker-cache/incremental-${name}.json`;
|
|
85
|
+
if (isLoadedByStryker) {
|
|
86
|
+
pruneIncrementalReport(path.join(options.root, incrementalFile), (file) => layer.paths.some((own) => isUnderDirectory(file, own))
|
|
87
|
+
&& carvedOut.every((entry) => !isUnderDirectory(file, entry)), configName);
|
|
88
|
+
}
|
|
89
|
+
const exclusions = options.base.mutate.filter((pattern) => pattern.startsWith('!'));
|
|
90
|
+
return {
|
|
91
|
+
config: {
|
|
92
|
+
...options.base,
|
|
93
|
+
incrementalFile,
|
|
94
|
+
mutate: [
|
|
95
|
+
...layer.paths.map((entry) => `${entry}/**/*.ts`),
|
|
96
|
+
...carvedOut.map((entry) => `!${entry}/**`),
|
|
97
|
+
...exclusions,
|
|
98
|
+
],
|
|
99
|
+
thresholds: { ...options.base.thresholds, break: layer.break },
|
|
100
|
+
},
|
|
101
|
+
names,
|
|
102
|
+
paths: pathsOf,
|
|
103
|
+
roots,
|
|
104
|
+
skip: options.skip,
|
|
105
|
+
};
|
|
106
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from './code-without-logic.js';
|
|
2
|
+
export * from './decide-layer-run.js';
|
|
3
|
+
export * from './define-stryker-config.js';
|
|
4
|
+
export * from './define-stryker-layers.js';
|
|
5
|
+
export * from './mutation-layer-cli.js';
|
|
6
|
+
export * from './stryker-config.interface.js';
|
|
7
|
+
export * from './stryker-layers.interface.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from './code-without-logic.js';
|
|
2
|
+
export * from './decide-layer-run.js';
|
|
3
|
+
export * from './define-stryker-config.js';
|
|
4
|
+
export * from './define-stryker-layers.js';
|
|
5
|
+
export * from './mutation-layer-cli.js';
|
|
6
|
+
export * from './stryker-config.interface.js';
|
|
7
|
+
export * from './stryker-layers.interface.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { StrykerLayersInterface } from './stryker-layers.interface.js';
|
|
2
|
+
export interface MutationLayerCliDependenciesInterface {
|
|
3
|
+
changedFiles(base: string): string[];
|
|
4
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
5
|
+
loadLayers(config: string): Promise<StrykerLayersInterface>;
|
|
6
|
+
print(line: string): void;
|
|
7
|
+
run(command: string): number;
|
|
8
|
+
}
|
|
9
|
+
export declare const executeMutationLayerCli: (argv: readonly string[], dependencies: MutationLayerCliDependenciesInterface) => Promise<number>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { decideLayerRun } from './decide-layer-run.js';
|
|
2
|
+
const DEFAULT_CONFIG = 'stryker.layer.conf.mjs';
|
|
3
|
+
const DEFAULT_LAYER_VARIABLE = 'TYPESCRIPT_MUTATION_LAYER';
|
|
4
|
+
const defaultCommand = (config) => `yarn stryker run ${config} --logLevel info --concurrency 2`;
|
|
5
|
+
const parse = (argv) => {
|
|
6
|
+
const positional = argv.filter((argument) => !argument.startsWith('--'));
|
|
7
|
+
const flag = argv.find((argument) => argument.startsWith('--config='));
|
|
8
|
+
return { base: positional[1], command: positional[0], config: flag?.slice('--config='.length) ?? DEFAULT_CONFIG };
|
|
9
|
+
};
|
|
10
|
+
const NO_BASE_REASON = 'базы диффа нет (не MR-пайплайн)';
|
|
11
|
+
const changedFilesOf = (dependencies, base) => {
|
|
12
|
+
try {
|
|
13
|
+
return dependencies.changedFiles(base);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
return Error.isError(error) ? error : new Error(String(error));
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const labelOf = (cell) => `ts:test:mutation [${String(cell.layer)}]`;
|
|
20
|
+
const launch = (cell, reason) => {
|
|
21
|
+
cell.dependencies.print(`${labelOf(cell)}: прогон — ${reason}`);
|
|
22
|
+
return cell.dependencies.run(cell.dependencies.env.MUTATION_LAYER_COMMAND ?? defaultCommand(cell.config));
|
|
23
|
+
};
|
|
24
|
+
const runCell = (cell) => {
|
|
25
|
+
const { dependencies, layer, layers } = cell;
|
|
26
|
+
const diffBase = cell.base ?? dependencies.env.CI_MERGE_REQUEST_DIFF_BASE_SHA;
|
|
27
|
+
if (diffBase === undefined || diffBase === '') {
|
|
28
|
+
return launch(cell, NO_BASE_REASON);
|
|
29
|
+
}
|
|
30
|
+
const changed = changedFilesOf(dependencies, diffBase);
|
|
31
|
+
if (Error.isError(changed)) {
|
|
32
|
+
return launch(cell, `дифф к ${diffBase} не читается: ${changed.message.replace(/\n[\s\S]*/u, '')}`);
|
|
33
|
+
}
|
|
34
|
+
const decision = decideLayerRun({ ...layers.skip, changed, layerPaths: layers.paths(layer), roots: layers.roots });
|
|
35
|
+
if (!decision.run) {
|
|
36
|
+
dependencies.print(`${labelOf(cell)}: дифф MR (база ${diffBase}) группу не трогает — прогон пропущен, кеш и score группы без изменений`);
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
if (decision.touched.length > 0) {
|
|
40
|
+
dependencies.print(`${labelOf(cell)}: дифф MR трогает группу:\n${decision.touched.map((file) => ` ${file}`).join('\n')}`);
|
|
41
|
+
}
|
|
42
|
+
return launch(cell, decision.reason);
|
|
43
|
+
};
|
|
44
|
+
export const executeMutationLayerCli = async (argv, dependencies) => {
|
|
45
|
+
const { base, command, config } = parse(argv);
|
|
46
|
+
const layers = await dependencies.loadLayers(config);
|
|
47
|
+
if (command === 'names') {
|
|
48
|
+
dependencies.print(layers.names.join(' '));
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
if (command === 'paths') {
|
|
52
|
+
dependencies.print(layers.paths(dependencies.env[DEFAULT_LAYER_VARIABLE]).join('\n'));
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
if (command === 'run') {
|
|
56
|
+
return runCell({ base, config, dependencies, layer: dependencies.env[DEFAULT_LAYER_VARIABLE], layers });
|
|
57
|
+
}
|
|
58
|
+
dependencies.print(`application-mutation-layer: ожидается names | paths | run, получено ${String(command)}`);
|
|
59
|
+
return 2;
|
|
60
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface StrykerConfigInterface {
|
|
2
|
+
readonly incrementalFile: string;
|
|
3
|
+
readonly mutate: readonly string[];
|
|
4
|
+
readonly [option: string]: unknown;
|
|
5
|
+
readonly thresholds: StrykerThresholdsInterface;
|
|
6
|
+
}
|
|
7
|
+
export interface StrykerConfigOptionsInterface {
|
|
8
|
+
ignorePatterns?: readonly string[];
|
|
9
|
+
incrementalFile?: string;
|
|
10
|
+
mutate: StrykerMutateScopeInterface;
|
|
11
|
+
tempDirName?: string;
|
|
12
|
+
thresholds: StrykerThresholdsInterface;
|
|
13
|
+
vitestConfigFile: string;
|
|
14
|
+
}
|
|
15
|
+
export interface StrykerMutateScopeInterface {
|
|
16
|
+
exclude?: readonly string[];
|
|
17
|
+
include: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
export interface StrykerThresholdsInterface {
|
|
20
|
+
readonly break: number;
|
|
21
|
+
readonly high: number;
|
|
22
|
+
readonly low: number;
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { StrykerConfigInterface } from './stryker-config.interface.js';
|
|
2
|
+
export interface StrykerLayerInterface {
|
|
3
|
+
break: number;
|
|
4
|
+
paths: readonly string[];
|
|
5
|
+
}
|
|
6
|
+
export interface StrykerLayersInterface {
|
|
7
|
+
config: StrykerConfigInterface;
|
|
8
|
+
names: readonly string[];
|
|
9
|
+
paths(name: string | undefined): string[];
|
|
10
|
+
roots: readonly string[];
|
|
11
|
+
skip: StrykerLayerSkipOptionsInterface;
|
|
12
|
+
}
|
|
13
|
+
export interface StrykerLayerSkipOptionsInterface {
|
|
14
|
+
configFiles: RegExp;
|
|
15
|
+
sharedDirectories: readonly string[];
|
|
16
|
+
sourceRoot: string;
|
|
17
|
+
}
|
|
18
|
+
export interface StrykerLayersOptionsInterface {
|
|
19
|
+
base: StrykerConfigInterface;
|
|
20
|
+
ciFile: string;
|
|
21
|
+
configName?: string;
|
|
22
|
+
defaultLayer: string;
|
|
23
|
+
layers: Readonly<Record<string, StrykerLayerInterface>>;
|
|
24
|
+
layerVariable?: string;
|
|
25
|
+
root: string;
|
|
26
|
+
scope: {
|
|
27
|
+
directories: readonly string[];
|
|
28
|
+
parents: readonly string[];
|
|
29
|
+
};
|
|
30
|
+
skip: StrykerLayerSkipOptionsInterface;
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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",
|
|
@@ -34,6 +34,16 @@
|
|
|
34
34
|
}
|
|
35
35
|
},
|
|
36
36
|
"./package.json": "./package.json",
|
|
37
|
+
"./stryker": {
|
|
38
|
+
"import": {
|
|
39
|
+
"types": "./dist/esm/stryker/index.d.ts",
|
|
40
|
+
"default": "./dist/esm/stryker/index.js"
|
|
41
|
+
},
|
|
42
|
+
"require": {
|
|
43
|
+
"types": "./dist/cjs/stryker/index.d.ts",
|
|
44
|
+
"default": "./dist/cjs/stryker/index.js"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
37
47
|
"./vitest": {
|
|
38
48
|
"import": {
|
|
39
49
|
"types": "./dist/esm/vitest/index.d.ts",
|
|
@@ -43,22 +53,35 @@
|
|
|
43
53
|
"types": "./dist/cjs/vitest/index.d.ts",
|
|
44
54
|
"default": "./dist/cjs/vitest/index.js"
|
|
45
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
|
+
}
|
|
46
66
|
}
|
|
47
67
|
},
|
|
48
68
|
"main": "./dist/cjs/index.js",
|
|
49
69
|
"module": "./dist/esm/index.js",
|
|
50
70
|
"types": "./dist/cjs/index.d.ts",
|
|
71
|
+
"bin": {
|
|
72
|
+
"application-mutation-layer": "./dist/esm/stryker/bin/application-mutation-layer.js"
|
|
73
|
+
},
|
|
51
74
|
"files": [
|
|
52
75
|
"dist"
|
|
53
76
|
],
|
|
54
77
|
"scripts": {
|
|
55
|
-
"build": "rm -rf ./dist && node ../../scripts/barrels.mjs create ./src --entry-only=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",
|
|
56
79
|
"lint:tsc": "tsc --noEmit --pretty --project .",
|
|
57
80
|
"test:mutation": "stryker run",
|
|
58
81
|
"test:unit": "vitest run"
|
|
59
82
|
},
|
|
60
83
|
"dependencies": {
|
|
61
|
-
"@application-kit/bootstrap": "^0.2.
|
|
84
|
+
"@application-kit/bootstrap": "^0.2.2"
|
|
62
85
|
},
|
|
63
86
|
"devDependencies": {
|
|
64
87
|
"@nestjs/common": "^11.2.7",
|
|
@@ -70,6 +93,7 @@
|
|
|
70
93
|
"@stryker-mutator/core": "^10.0.0",
|
|
71
94
|
"@stryker-mutator/vitest-runner": "^10.0.0",
|
|
72
95
|
"@types/node": "^26.2.0",
|
|
96
|
+
"bcrypt": "^6.0.0",
|
|
73
97
|
"nestjs-pino": "^4.6.1",
|
|
74
98
|
"oxide.ts": "^1.1.0",
|
|
75
99
|
"pino": "^10.3.1",
|
|
@@ -89,6 +113,7 @@
|
|
|
89
113
|
"@nestjs/microservices": "^11.0.0",
|
|
90
114
|
"@nestjs/platform-fastify": "^11.0.0",
|
|
91
115
|
"@nestjs/testing": "^11.0.0",
|
|
116
|
+
"bcrypt": "^6.0.0",
|
|
92
117
|
"nestjs-pino": "^4.6.1",
|
|
93
118
|
"oxide.ts": "^1.1.0",
|
|
94
119
|
"rxjs": "^7.8.2",
|
|
@@ -98,6 +123,9 @@
|
|
|
98
123
|
"vitest": "^4.1.0"
|
|
99
124
|
},
|
|
100
125
|
"peerDependenciesMeta": {
|
|
126
|
+
"bcrypt": {
|
|
127
|
+
"optional": true
|
|
128
|
+
},
|
|
101
129
|
"vite": {
|
|
102
130
|
"optional": true
|
|
103
131
|
},
|