nestjs-ddd-cli 2.2.1 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +247 -408
- package/ddd.schema.json +111 -0
- package/dist/commands/aggregate-validator.d.ts +9 -0
- package/dist/commands/aggregate-validator.js +953 -0
- package/dist/commands/aggregate-validator.js.map +1 -0
- package/dist/commands/ai-assist.d.ts +8 -0
- package/dist/commands/ai-assist.js +337 -0
- package/dist/commands/ai-assist.js.map +1 -0
- package/dist/commands/api-contracts.d.ts +9 -0
- package/dist/commands/api-contracts.js +1368 -0
- package/dist/commands/api-contracts.js.map +1 -0
- package/dist/commands/api-docs.d.ts +8 -0
- package/dist/commands/api-docs.js +408 -0
- package/dist/commands/api-docs.js.map +1 -0
- package/dist/commands/api-versioning.d.ts +11 -0
- package/dist/commands/api-versioning.js +643 -0
- package/dist/commands/api-versioning.js.map +1 -0
- package/dist/commands/audit-logging.d.ts +9 -0
- package/dist/commands/audit-logging.js +1129 -0
- package/dist/commands/audit-logging.js.map +1 -0
- package/dist/commands/batch-generate.d.ts +10 -0
- package/dist/commands/batch-generate.js +405 -0
- package/dist/commands/batch-generate.js.map +1 -0
- package/dist/commands/caching-strategies.d.ts +9 -0
- package/dist/commands/caching-strategies.js +874 -0
- package/dist/commands/caching-strategies.js.map +1 -0
- package/dist/commands/code-analyzer.d.ts +42 -0
- package/dist/commands/code-analyzer.js +474 -0
- package/dist/commands/code-analyzer.js.map +1 -0
- package/dist/commands/database-seeding.d.ts +6 -0
- package/dist/commands/database-seeding.js +621 -0
- package/dist/commands/database-seeding.js.map +1 -0
- package/dist/commands/db-optimization.d.ts +7 -0
- package/dist/commands/db-optimization.js +687 -0
- package/dist/commands/db-optimization.js.map +1 -0
- package/dist/commands/dependency-graph.d.ts +6 -0
- package/dist/commands/dependency-graph.js +329 -0
- package/dist/commands/dependency-graph.js.map +1 -0
- package/dist/commands/doctor-enhanced.d.ts +22 -0
- package/dist/commands/doctor-enhanced.js +543 -0
- package/dist/commands/doctor-enhanced.js.map +1 -0
- package/dist/commands/doctor.d.ts +4 -0
- package/dist/commands/doctor.js +151 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/env-manager.d.ts +6 -0
- package/dist/commands/env-manager.js +419 -0
- package/dist/commands/env-manager.js.map +1 -0
- package/dist/commands/event-sourcing-full.d.ts +10 -0
- package/dist/commands/event-sourcing-full.js +1107 -0
- package/dist/commands/event-sourcing-full.js.map +1 -0
- package/dist/commands/feature-flags.d.ts +9 -0
- package/dist/commands/feature-flags.js +824 -0
- package/dist/commands/feature-flags.js.map +1 -0
- package/dist/commands/filter-dsl.d.ts +10 -0
- package/dist/commands/filter-dsl.js +1407 -0
- package/dist/commands/filter-dsl.js.map +1 -0
- package/dist/commands/generate-all.js +485 -32
- package/dist/commands/generate-all.js.map +1 -1
- package/dist/commands/generate-deployment.d.ts +8 -0
- package/dist/commands/generate-deployment.js +746 -0
- package/dist/commands/generate-deployment.js.map +1 -0
- package/dist/commands/generate-domain-service.d.ts +14 -0
- package/dist/commands/generate-domain-service.js +796 -0
- package/dist/commands/generate-domain-service.js.map +1 -0
- package/dist/commands/generate-entity.js +82 -24
- package/dist/commands/generate-entity.js.map +1 -1
- package/dist/commands/generate-from-schema.d.ts +56 -0
- package/dist/commands/generate-from-schema.js +222 -0
- package/dist/commands/generate-from-schema.js.map +1 -0
- package/dist/commands/generate-orchestrator.d.ts +14 -0
- package/dist/commands/generate-orchestrator.js +887 -0
- package/dist/commands/generate-orchestrator.js.map +1 -0
- package/dist/commands/generate-repository.d.ts +14 -0
- package/dist/commands/generate-repository.js +1019 -0
- package/dist/commands/generate-repository.js.map +1 -0
- package/dist/commands/generate-shared.d.ts +4 -0
- package/dist/commands/generate-shared.js +388 -0
- package/dist/commands/generate-shared.js.map +1 -0
- package/dist/commands/generate-value-object.d.ts +32 -0
- package/dist/commands/generate-value-object.js +700 -0
- package/dist/commands/generate-value-object.js.map +1 -0
- package/dist/commands/graphql-subscriptions.d.ts +6 -0
- package/dist/commands/graphql-subscriptions.js +607 -0
- package/dist/commands/graphql-subscriptions.js.map +1 -0
- package/dist/commands/graphql-types.d.ts +5 -0
- package/dist/commands/graphql-types.js +423 -0
- package/dist/commands/graphql-types.js.map +1 -0
- package/dist/commands/health-probes-advanced.d.ts +6 -0
- package/dist/commands/health-probes-advanced.js +655 -0
- package/dist/commands/health-probes-advanced.js.map +1 -0
- package/dist/commands/i18n-setup.d.ts +10 -0
- package/dist/commands/i18n-setup.js +677 -0
- package/dist/commands/i18n-setup.js.map +1 -0
- package/dist/commands/init-config.d.ts +6 -0
- package/dist/commands/init-config.js +370 -0
- package/dist/commands/init-config.js.map +1 -0
- package/dist/commands/init-project.js +56 -6
- package/dist/commands/init-project.js.map +1 -1
- package/dist/commands/interactive-scaffold.d.ts +5 -0
- package/dist/commands/interactive-scaffold.js +271 -0
- package/dist/commands/interactive-scaffold.js.map +1 -0
- package/dist/commands/metrics-prometheus.d.ts +6 -0
- package/dist/commands/metrics-prometheus.js +681 -0
- package/dist/commands/metrics-prometheus.js.map +1 -0
- package/dist/commands/migration-engine.d.ts +6 -0
- package/dist/commands/migration-engine.js +446 -0
- package/dist/commands/migration-engine.js.map +1 -0
- package/dist/commands/migration.d.ts +12 -0
- package/dist/commands/migration.js +484 -0
- package/dist/commands/migration.js.map +1 -0
- package/dist/commands/monorepo.d.ts +8 -0
- package/dist/commands/monorepo.js +483 -0
- package/dist/commands/monorepo.js.map +1 -0
- package/dist/commands/multi-database.d.ts +5 -0
- package/dist/commands/multi-database.js +439 -0
- package/dist/commands/multi-database.js.map +1 -0
- package/dist/commands/observability-tracing.d.ts +10 -0
- package/dist/commands/observability-tracing.js +740 -0
- package/dist/commands/observability-tracing.js.map +1 -0
- package/dist/commands/openapi-export.d.ts +8 -0
- package/dist/commands/openapi-export.js +359 -0
- package/dist/commands/openapi-export.js.map +1 -0
- package/dist/commands/perf-analyzer.d.ts +8 -0
- package/dist/commands/perf-analyzer.js +423 -0
- package/dist/commands/perf-analyzer.js.map +1 -0
- package/dist/commands/rate-limiting.d.ts +10 -0
- package/dist/commands/rate-limiting.js +953 -0
- package/dist/commands/rate-limiting.js.map +1 -0
- package/dist/commands/recipe-plugin.d.ts +56 -0
- package/dist/commands/recipe-plugin.js +315 -0
- package/dist/commands/recipe-plugin.js.map +1 -0
- package/dist/commands/recipe.d.ts +6 -0
- package/dist/commands/recipe.js +3941 -0
- package/dist/commands/recipe.js.map +1 -0
- package/dist/commands/recipes/elasticsearch.recipe.d.ts +1 -0
- package/dist/commands/recipes/elasticsearch.recipe.js +761 -0
- package/dist/commands/recipes/elasticsearch.recipe.js.map +1 -0
- package/dist/commands/recipes/event-sourcing.recipe.d.ts +1 -0
- package/dist/commands/recipes/event-sourcing.recipe.js +889 -0
- package/dist/commands/recipes/event-sourcing.recipe.js.map +1 -0
- package/dist/commands/recipes/index.d.ts +7 -0
- package/dist/commands/recipes/index.js +24 -0
- package/dist/commands/recipes/index.js.map +1 -0
- package/dist/commands/recipes/message-queue.recipe.d.ts +1 -0
- package/dist/commands/recipes/message-queue.recipe.js +706 -0
- package/dist/commands/recipes/message-queue.recipe.js.map +1 -0
- package/dist/commands/recipes/middleware.recipe.d.ts +1 -0
- package/dist/commands/recipes/middleware.recipe.js +383 -0
- package/dist/commands/recipes/middleware.recipe.js.map +1 -0
- package/dist/commands/recipes/multi-tenancy.recipe.d.ts +1 -0
- package/dist/commands/recipes/multi-tenancy.recipe.js +520 -0
- package/dist/commands/recipes/multi-tenancy.recipe.js.map +1 -0
- package/dist/commands/recipes/oauth2.recipe.d.ts +1 -0
- package/dist/commands/recipes/oauth2.recipe.js +472 -0
- package/dist/commands/recipes/oauth2.recipe.js.map +1 -0
- package/dist/commands/recipes/websocket.recipe.d.ts +1 -0
- package/dist/commands/recipes/websocket.recipe.js +453 -0
- package/dist/commands/recipes/websocket.recipe.js.map +1 -0
- package/dist/commands/resilience-patterns.d.ts +13 -0
- package/dist/commands/resilience-patterns.js +1029 -0
- package/dist/commands/resilience-patterns.js.map +1 -0
- package/dist/commands/security-patterns.d.ts +11 -0
- package/dist/commands/security-patterns.js +2233 -0
- package/dist/commands/security-patterns.js.map +1 -0
- package/dist/commands/template-debug.d.ts +27 -0
- package/dist/commands/template-debug.js +388 -0
- package/dist/commands/template-debug.js.map +1 -0
- package/dist/commands/test-factory-full.d.ts +9 -0
- package/dist/commands/test-factory-full.js +1570 -0
- package/dist/commands/test-factory-full.js.map +1 -0
- package/dist/commands/test-scaffold.d.ts +7 -0
- package/dist/commands/test-scaffold.js +621 -0
- package/dist/commands/test-scaffold.js.map +1 -0
- package/dist/index.js +1088 -0
- package/dist/index.js.map +1 -1
- package/dist/templates/ai-context/CLAUDE.md.hbs +158 -0
- package/dist/templates/ai-context/conventions.md.hbs +154 -0
- package/dist/templates/command/create-command.hbs +6 -14
- package/dist/templates/command/delete-command.hbs +19 -0
- package/dist/templates/command/update-command.hbs +24 -0
- package/dist/templates/controller/controller.hbs +64 -17
- package/dist/templates/dto/create-dto.hbs +29 -5
- package/dist/templates/dto/filter-dto.hbs +52 -0
- package/dist/templates/dto/filter-query.dto.hbs +148 -0
- package/dist/templates/dto/paginated-response.dto.hbs +29 -0
- package/dist/templates/dto/pagination-query.dto.hbs +30 -0
- package/dist/templates/dto/response-dto.hbs +38 -0
- package/dist/templates/dto/update-dto.hbs +11 -0
- package/dist/templates/entity/entity.hbs +32 -1
- package/dist/templates/event/domain-event.hbs +33 -7
- package/dist/templates/event/event-handler.hbs +40 -0
- package/dist/templates/exception/base-exceptions.hbs +69 -0
- package/dist/templates/exception/entity-not-found.exception.hbs +7 -0
- package/dist/templates/mapper/mapper.hbs +49 -24
- package/dist/templates/module/module.hbs +34 -10
- package/dist/templates/orm-entity/orm-entity.hbs +63 -12
- package/dist/templates/prisma/prisma-mapper.hbs +71 -0
- package/dist/templates/prisma/prisma-repository.hbs +114 -0
- package/dist/templates/prisma/prisma-schema.hbs +20 -0
- package/dist/templates/prisma/prisma-service.hbs +51 -0
- package/dist/templates/query/get-all.query.hbs +50 -0
- package/dist/templates/query/get-by-id.query.hbs +31 -0
- package/dist/templates/repository/repository.hbs +55 -13
- package/dist/templates/resolver/graphql-input.hbs +54 -0
- package/dist/templates/resolver/graphql-type.hbs +58 -0
- package/dist/templates/resolver/pagination-args.hbs +33 -0
- package/dist/templates/resolver/resolver.hbs +62 -0
- package/dist/templates/shared/prisma-query-builder.util.hbs +189 -0
- package/dist/templates/shared/query-builder.util.hbs +218 -0
- package/dist/templates/test/controller.spec.hbs +124 -0
- package/dist/templates/test/repository.spec.hbs +158 -0
- package/dist/templates/test/usecase.spec.hbs +116 -0
- package/dist/templates/usecase/create-usecase.hbs +19 -7
- package/dist/templates/usecase/delete-usecase.hbs +17 -0
- package/dist/templates/usecase/update-usecase.hbs +31 -0
- package/dist/utils/config.utils.d.ts +45 -0
- package/dist/utils/config.utils.js +211 -0
- package/dist/utils/config.utils.js.map +1 -0
- package/dist/utils/error.utils.d.ts +145 -0
- package/dist/utils/error.utils.js +422 -0
- package/dist/utils/error.utils.js.map +1 -0
- package/dist/utils/field.utils.d.ts +54 -0
- package/dist/utils/field.utils.js +389 -0
- package/dist/utils/field.utils.js.map +1 -0
- package/dist/utils/file.utils.d.ts +19 -8
- package/dist/utils/file.utils.js +135 -4
- package/dist/utils/file.utils.js.map +1 -1
- package/dist/utils/idempotency.utils.d.ts +123 -0
- package/dist/utils/idempotency.utils.js +444 -0
- package/dist/utils/idempotency.utils.js.map +1 -0
- package/dist/utils/naming.utils.js +24 -5
- package/dist/utils/naming.utils.js.map +1 -1
- package/dist/utils/performance.utils.d.ts +37 -0
- package/dist/utils/performance.utils.js +158 -0
- package/dist/utils/performance.utils.js.map +1 -0
- package/dist/utils/relation.utils.d.ts +92 -0
- package/dist/utils/relation.utils.js +388 -0
- package/dist/utils/relation.utils.js.map +1 -0
- package/dist/utils/rollback.utils.d.ts +49 -0
- package/dist/utils/rollback.utils.js +306 -0
- package/dist/utils/rollback.utils.js.map +1 -0
- package/dist/utils/schema.utils.d.ts +123 -0
- package/dist/utils/schema.utils.js +419 -0
- package/dist/utils/schema.utils.js.map +1 -0
- package/dist/utils/security.utils.d.ts +57 -0
- package/dist/utils/security.utils.js +315 -0
- package/dist/utils/security.utils.js.map +1 -0
- package/dist/utils/template-engine.utils.d.ts +80 -0
- package/dist/utils/template-engine.utils.js +463 -0
- package/dist/utils/template-engine.utils.js.map +1 -0
- package/dist/utils/validation-registry.utils.d.ts +160 -0
- package/dist/utils/validation-registry.utils.js +526 -0
- package/dist/utils/validation-registry.utils.js.map +1 -0
- package/package.json +3 -1
|
@@ -0,0 +1,1570 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Enhanced Test Fixtures & Factories
|
|
4
|
+
* Complete test infrastructure generation
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
40
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
41
|
+
};
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.setupTestInfrastructure = setupTestInfrastructure;
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
46
|
+
const file_utils_1 = require("../utils/file.utils");
|
|
47
|
+
async function setupTestInfrastructure(basePath, options = {}) {
|
|
48
|
+
console.log(chalk_1.default.bold.blue('\n🧪 Setting up Test Infrastructure\n'));
|
|
49
|
+
const testPath = path.join(basePath, 'src/shared/testing');
|
|
50
|
+
await (0, file_utils_1.ensureDir)(testPath);
|
|
51
|
+
await (0, file_utils_1.ensureDir)(path.join(testPath, 'factories'));
|
|
52
|
+
await (0, file_utils_1.ensureDir)(path.join(testPath, 'fixtures'));
|
|
53
|
+
await (0, file_utils_1.ensureDir)(path.join(testPath, 'mocks'));
|
|
54
|
+
await (0, file_utils_1.ensureDir)(path.join(testPath, 'utils'));
|
|
55
|
+
// Generate base factory
|
|
56
|
+
await generateBaseFactory(testPath);
|
|
57
|
+
// Generate factory builder
|
|
58
|
+
await generateFactoryBuilder(testPath);
|
|
59
|
+
// Generate fixture loader
|
|
60
|
+
await generateFixtureLoader(testPath);
|
|
61
|
+
// Generate mock repository
|
|
62
|
+
await generateMockRepository(testPath);
|
|
63
|
+
// Generate database seeder
|
|
64
|
+
await generateDatabaseSeeder(testPath, options);
|
|
65
|
+
// Generate test module builder
|
|
66
|
+
await generateTestModuleBuilder(testPath);
|
|
67
|
+
// Generate test utils
|
|
68
|
+
await generateTestUtils(testPath);
|
|
69
|
+
// Generate sample factories
|
|
70
|
+
await generateSampleFactories(testPath);
|
|
71
|
+
// Generate index
|
|
72
|
+
await (0, file_utils_1.writeFile)(path.join(testPath, 'index.ts'), `export * from './factories/base.factory';
|
|
73
|
+
export * from './factories/factory.builder';
|
|
74
|
+
export * from './fixtures/fixture.loader';
|
|
75
|
+
export * from './mocks/mock.repository';
|
|
76
|
+
export * from './utils/database.seeder';
|
|
77
|
+
export * from './utils/test-module.builder';
|
|
78
|
+
export * from './utils/test.utils';
|
|
79
|
+
`);
|
|
80
|
+
console.log(chalk_1.default.green('\n✅ Test Infrastructure set up!'));
|
|
81
|
+
}
|
|
82
|
+
async function generateBaseFactory(testPath) {
|
|
83
|
+
const content = `import { faker } from '@faker-js/faker';
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Base factory class for generating test data
|
|
87
|
+
*/
|
|
88
|
+
export abstract class BaseFactory<T, CreateInput = Partial<T>> {
|
|
89
|
+
protected abstract getDefaults(): T;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Create a single instance
|
|
93
|
+
*/
|
|
94
|
+
create(overrides?: CreateInput): T {
|
|
95
|
+
return {
|
|
96
|
+
...this.getDefaults(),
|
|
97
|
+
...overrides,
|
|
98
|
+
} as T;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create multiple instances
|
|
103
|
+
*/
|
|
104
|
+
createMany(count: number, overrides?: CreateInput): T[] {
|
|
105
|
+
return Array.from({ length: count }, () => this.create(overrides));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Create with specific traits
|
|
110
|
+
*/
|
|
111
|
+
with(traits: Partial<T>): this {
|
|
112
|
+
const original = this.getDefaults.bind(this);
|
|
113
|
+
this.getDefaults = () => ({ ...original(), ...traits });
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Create a builder for fluent interface
|
|
119
|
+
*/
|
|
120
|
+
builder(): FactoryBuilder<T> {
|
|
121
|
+
return new FactoryBuilder<T>(this);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Factory builder for fluent creation
|
|
127
|
+
*/
|
|
128
|
+
export class FactoryBuilder<T> {
|
|
129
|
+
private traits: Partial<T> = {};
|
|
130
|
+
private afterCreate: Array<(entity: T) => T | Promise<T>> = [];
|
|
131
|
+
|
|
132
|
+
constructor(private factory: BaseFactory<T>) {}
|
|
133
|
+
|
|
134
|
+
with(traits: Partial<T>): this {
|
|
135
|
+
this.traits = { ...this.traits, ...traits };
|
|
136
|
+
return this;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
afterCreating(callback: (entity: T) => T | Promise<T>): this {
|
|
140
|
+
this.afterCreate.push(callback);
|
|
141
|
+
return this;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async create(): Promise<T> {
|
|
145
|
+
let entity = this.factory.create(this.traits as any);
|
|
146
|
+
for (const callback of this.afterCreate) {
|
|
147
|
+
entity = await callback(entity);
|
|
148
|
+
}
|
|
149
|
+
return entity;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async createMany(count: number): Promise<T[]> {
|
|
153
|
+
const entities: T[] = [];
|
|
154
|
+
for (let i = 0; i < count; i++) {
|
|
155
|
+
entities.push(await this.create());
|
|
156
|
+
}
|
|
157
|
+
return entities;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Faker helpers for common fields
|
|
163
|
+
*/
|
|
164
|
+
export const FakerHelpers = {
|
|
165
|
+
id: () => faker.string.uuid(),
|
|
166
|
+
email: () => faker.internet.email(),
|
|
167
|
+
password: () => faker.internet.password({ length: 12 }),
|
|
168
|
+
firstName: () => faker.person.firstName(),
|
|
169
|
+
lastName: () => faker.person.lastName(),
|
|
170
|
+
fullName: () => faker.person.fullName(),
|
|
171
|
+
username: () => faker.internet.username(),
|
|
172
|
+
phone: () => faker.phone.number(),
|
|
173
|
+
address: () => faker.location.streetAddress(),
|
|
174
|
+
city: () => faker.location.city(),
|
|
175
|
+
country: () => faker.location.country(),
|
|
176
|
+
zipCode: () => faker.location.zipCode(),
|
|
177
|
+
url: () => faker.internet.url(),
|
|
178
|
+
imageUrl: () => faker.image.url(),
|
|
179
|
+
avatarUrl: () => faker.image.avatar(),
|
|
180
|
+
paragraph: () => faker.lorem.paragraph(),
|
|
181
|
+
sentence: () => faker.lorem.sentence(),
|
|
182
|
+
word: () => faker.lorem.word(),
|
|
183
|
+
date: () => faker.date.past(),
|
|
184
|
+
futureDate: () => faker.date.future(),
|
|
185
|
+
recentDate: () => faker.date.recent(),
|
|
186
|
+
price: () => faker.number.float({ min: 1, max: 1000, fractionDigits: 2 }),
|
|
187
|
+
quantity: () => faker.number.int({ min: 1, max: 100 }),
|
|
188
|
+
boolean: () => faker.datatype.boolean(),
|
|
189
|
+
pick: <T>(arr: T[]) => faker.helpers.arrayElement(arr),
|
|
190
|
+
json: () => ({ key: faker.lorem.word(), value: faker.lorem.word() }),
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Type-safe factory creator
|
|
195
|
+
*/
|
|
196
|
+
export function defineFactory<T>(defaults: () => T): BaseFactory<T> {
|
|
197
|
+
return new (class extends BaseFactory<T> {
|
|
198
|
+
protected getDefaults(): T {
|
|
199
|
+
return defaults();
|
|
200
|
+
}
|
|
201
|
+
})();
|
|
202
|
+
}
|
|
203
|
+
`;
|
|
204
|
+
await (0, file_utils_1.writeFile)(path.join(testPath, 'factories/base.factory.ts'), content);
|
|
205
|
+
console.log(chalk_1.default.green(' ✓ Base factory'));
|
|
206
|
+
}
|
|
207
|
+
async function generateFactoryBuilder(testPath) {
|
|
208
|
+
const content = `import { faker } from '@faker-js/faker';
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Advanced factory builder with sequences and states
|
|
212
|
+
*/
|
|
213
|
+
export class AdvancedFactoryBuilder<T> {
|
|
214
|
+
private defaults: () => T;
|
|
215
|
+
private traits: Map<string, Partial<T>> = new Map();
|
|
216
|
+
private sequences: Map<keyof T, Generator<any>> = new Map();
|
|
217
|
+
private afterCreateHooks: Array<(entity: T) => T | Promise<T>> = [];
|
|
218
|
+
private afterBuildHooks: Array<(entity: T) => T> = [];
|
|
219
|
+
private currentTraits: string[] = [];
|
|
220
|
+
private overrides: Partial<T> = {};
|
|
221
|
+
|
|
222
|
+
constructor(defaults: () => T) {
|
|
223
|
+
this.defaults = defaults;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Define a named trait (state)
|
|
228
|
+
*/
|
|
229
|
+
trait(name: string, attributes: Partial<T>): this {
|
|
230
|
+
this.traits.set(name, attributes);
|
|
231
|
+
return this;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Define a sequence for a field
|
|
236
|
+
*/
|
|
237
|
+
sequence<K extends keyof T>(field: K, generator: () => Generator<T[K]>): this {
|
|
238
|
+
this.sequences.set(field, generator());
|
|
239
|
+
return this;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Add after-build hook (synchronous)
|
|
244
|
+
*/
|
|
245
|
+
afterBuild(callback: (entity: T) => T): this {
|
|
246
|
+
this.afterBuildHooks.push(callback);
|
|
247
|
+
return this;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Add after-create hook (can be async)
|
|
252
|
+
*/
|
|
253
|
+
afterCreate(callback: (entity: T) => T | Promise<T>): this {
|
|
254
|
+
this.afterCreateHooks.push(callback);
|
|
255
|
+
return this;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Use a trait
|
|
260
|
+
*/
|
|
261
|
+
use(...traitNames: string[]): AdvancedFactoryBuilder<T> {
|
|
262
|
+
const builder = this.clone();
|
|
263
|
+
builder.currentTraits = [...this.currentTraits, ...traitNames];
|
|
264
|
+
return builder;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Override specific attributes
|
|
269
|
+
*/
|
|
270
|
+
with(attrs: Partial<T>): AdvancedFactoryBuilder<T> {
|
|
271
|
+
const builder = this.clone();
|
|
272
|
+
builder.overrides = { ...this.overrides, ...attrs };
|
|
273
|
+
return builder;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Build an entity (synchronous, no hooks)
|
|
278
|
+
*/
|
|
279
|
+
build(): T {
|
|
280
|
+
let entity = this.defaults();
|
|
281
|
+
|
|
282
|
+
// Apply sequences
|
|
283
|
+
for (const [field, generator] of this.sequences) {
|
|
284
|
+
(entity as any)[field] = generator.next().value;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Apply traits
|
|
288
|
+
for (const traitName of this.currentTraits) {
|
|
289
|
+
const trait = this.traits.get(traitName);
|
|
290
|
+
if (trait) {
|
|
291
|
+
entity = { ...entity, ...trait };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Apply overrides
|
|
296
|
+
entity = { ...entity, ...this.overrides };
|
|
297
|
+
|
|
298
|
+
// Run after-build hooks
|
|
299
|
+
for (const hook of this.afterBuildHooks) {
|
|
300
|
+
entity = hook(entity);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return entity;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Build multiple entities
|
|
308
|
+
*/
|
|
309
|
+
buildMany(count: number): T[] {
|
|
310
|
+
return Array.from({ length: count }, () => this.build());
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Create an entity (runs after-create hooks)
|
|
315
|
+
*/
|
|
316
|
+
async create(): Promise<T> {
|
|
317
|
+
let entity = this.build();
|
|
318
|
+
|
|
319
|
+
for (const hook of this.afterCreateHooks) {
|
|
320
|
+
entity = await hook(entity);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return entity;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Create multiple entities
|
|
328
|
+
*/
|
|
329
|
+
async createMany(count: number): Promise<T[]> {
|
|
330
|
+
const entities: T[] = [];
|
|
331
|
+
for (let i = 0; i < count; i++) {
|
|
332
|
+
entities.push(await this.create());
|
|
333
|
+
}
|
|
334
|
+
return entities;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Clone the builder
|
|
339
|
+
*/
|
|
340
|
+
private clone(): AdvancedFactoryBuilder<T> {
|
|
341
|
+
const builder = new AdvancedFactoryBuilder(this.defaults);
|
|
342
|
+
builder.traits = new Map(this.traits);
|
|
343
|
+
builder.sequences = new Map(this.sequences);
|
|
344
|
+
builder.afterCreateHooks = [...this.afterCreateHooks];
|
|
345
|
+
builder.afterBuildHooks = [...this.afterBuildHooks];
|
|
346
|
+
builder.currentTraits = [...this.currentTraits];
|
|
347
|
+
builder.overrides = { ...this.overrides };
|
|
348
|
+
return builder;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Create an advanced factory
|
|
354
|
+
*/
|
|
355
|
+
export function factory<T>(defaults: () => T): AdvancedFactoryBuilder<T> {
|
|
356
|
+
return new AdvancedFactoryBuilder(defaults);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Sequence generator helpers
|
|
361
|
+
*/
|
|
362
|
+
export const sequences = {
|
|
363
|
+
/**
|
|
364
|
+
* Auto-incrementing number
|
|
365
|
+
*/
|
|
366
|
+
*autoIncrement(start: number = 1): Generator<number> {
|
|
367
|
+
let current = start;
|
|
368
|
+
while (true) {
|
|
369
|
+
yield current++;
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Unique email with counter
|
|
375
|
+
*/
|
|
376
|
+
*uniqueEmail(domain: string = 'test.com'): Generator<string> {
|
|
377
|
+
let counter = 1;
|
|
378
|
+
while (true) {
|
|
379
|
+
yield \`user\${counter++}@\${domain}\`;
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Unique username
|
|
385
|
+
*/
|
|
386
|
+
*uniqueUsername(prefix: string = 'user'): Generator<string> {
|
|
387
|
+
let counter = 1;
|
|
388
|
+
while (true) {
|
|
389
|
+
yield \`\${prefix}\${counter++}\`;
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Cycle through values
|
|
395
|
+
*/
|
|
396
|
+
*cycle<T>(values: T[]): Generator<T> {
|
|
397
|
+
let index = 0;
|
|
398
|
+
while (true) {
|
|
399
|
+
yield values[index % values.length];
|
|
400
|
+
index++;
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Random from array
|
|
406
|
+
*/
|
|
407
|
+
*randomFrom<T>(values: T[]): Generator<T> {
|
|
408
|
+
while (true) {
|
|
409
|
+
yield faker.helpers.arrayElement(values);
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
`;
|
|
414
|
+
await (0, file_utils_1.writeFile)(path.join(testPath, 'factories/factory.builder.ts'), content);
|
|
415
|
+
console.log(chalk_1.default.green(' ✓ Factory builder'));
|
|
416
|
+
}
|
|
417
|
+
async function generateFixtureLoader(testPath) {
|
|
418
|
+
const content = `import * as fs from 'fs';
|
|
419
|
+
import * as path from 'path';
|
|
420
|
+
import * as yaml from 'yaml';
|
|
421
|
+
|
|
422
|
+
export interface Fixture<T = any> {
|
|
423
|
+
name: string;
|
|
424
|
+
entity: string;
|
|
425
|
+
data: T[];
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Load and manage test fixtures
|
|
430
|
+
*/
|
|
431
|
+
export class FixtureLoader {
|
|
432
|
+
private fixtures: Map<string, Fixture> = new Map();
|
|
433
|
+
private basePath: string;
|
|
434
|
+
|
|
435
|
+
constructor(basePath?: string) {
|
|
436
|
+
this.basePath = basePath || path.join(process.cwd(), 'test/fixtures');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Load a fixture file (JSON or YAML)
|
|
441
|
+
*/
|
|
442
|
+
load<T = any>(name: string): Fixture<T> {
|
|
443
|
+
if (this.fixtures.has(name)) {
|
|
444
|
+
return this.fixtures.get(name)! as Fixture<T>;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const jsonPath = path.join(this.basePath, \`\${name}.json\`);
|
|
448
|
+
const yamlPath = path.join(this.basePath, \`\${name}.yaml\`);
|
|
449
|
+
const ymlPath = path.join(this.basePath, \`\${name}.yml\`);
|
|
450
|
+
|
|
451
|
+
let data: any;
|
|
452
|
+
|
|
453
|
+
if (fs.existsSync(jsonPath)) {
|
|
454
|
+
data = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));
|
|
455
|
+
} else if (fs.existsSync(yamlPath)) {
|
|
456
|
+
data = yaml.parse(fs.readFileSync(yamlPath, 'utf-8'));
|
|
457
|
+
} else if (fs.existsSync(ymlPath)) {
|
|
458
|
+
data = yaml.parse(fs.readFileSync(ymlPath, 'utf-8'));
|
|
459
|
+
} else {
|
|
460
|
+
throw new Error(\`Fixture not found: \${name}\`);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const fixture: Fixture<T> = {
|
|
464
|
+
name,
|
|
465
|
+
entity: data.entity || name,
|
|
466
|
+
data: Array.isArray(data) ? data : data.data || [],
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
this.fixtures.set(name, fixture);
|
|
470
|
+
return fixture;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Load all fixtures from directory
|
|
475
|
+
*/
|
|
476
|
+
loadAll(): Map<string, Fixture> {
|
|
477
|
+
if (!fs.existsSync(this.basePath)) {
|
|
478
|
+
return this.fixtures;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const files = fs.readdirSync(this.basePath);
|
|
482
|
+
|
|
483
|
+
for (const file of files) {
|
|
484
|
+
const ext = path.extname(file);
|
|
485
|
+
if (['.json', '.yaml', '.yml'].includes(ext)) {
|
|
486
|
+
const name = path.basename(file, ext);
|
|
487
|
+
this.load(name);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return this.fixtures;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Get fixture data by name
|
|
496
|
+
*/
|
|
497
|
+
get<T = any>(name: string): T[] {
|
|
498
|
+
return this.load<T>(name).data;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Get first item from fixture
|
|
503
|
+
*/
|
|
504
|
+
first<T = any>(name: string): T | undefined {
|
|
505
|
+
return this.get<T>(name)[0];
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Get random item from fixture
|
|
510
|
+
*/
|
|
511
|
+
random<T = any>(name: string): T | undefined {
|
|
512
|
+
const data = this.get<T>(name);
|
|
513
|
+
return data[Math.floor(Math.random() * data.length)];
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Get item by index
|
|
518
|
+
*/
|
|
519
|
+
at<T = any>(name: string, index: number): T | undefined {
|
|
520
|
+
return this.get<T>(name)[index];
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Filter fixture data
|
|
525
|
+
*/
|
|
526
|
+
filter<T = any>(name: string, predicate: (item: T) => boolean): T[] {
|
|
527
|
+
return this.get<T>(name).filter(predicate);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Find item in fixture
|
|
532
|
+
*/
|
|
533
|
+
find<T = any>(name: string, predicate: (item: T) => boolean): T | undefined {
|
|
534
|
+
return this.get<T>(name).find(predicate);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Clear cached fixtures
|
|
539
|
+
*/
|
|
540
|
+
clear(): void {
|
|
541
|
+
this.fixtures.clear();
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Create fixtures from factory
|
|
546
|
+
*/
|
|
547
|
+
static createFixture<T>(
|
|
548
|
+
name: string,
|
|
549
|
+
entity: string,
|
|
550
|
+
factory: () => T,
|
|
551
|
+
count: number = 10
|
|
552
|
+
): Fixture<T> {
|
|
553
|
+
return {
|
|
554
|
+
name,
|
|
555
|
+
entity,
|
|
556
|
+
data: Array.from({ length: count }, factory),
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Save fixture to file
|
|
562
|
+
*/
|
|
563
|
+
async save<T>(name: string, fixture: Fixture<T>, format: 'json' | 'yaml' = 'json'): Promise<void> {
|
|
564
|
+
const filePath = path.join(this.basePath, \`\${name}.\${format}\`);
|
|
565
|
+
|
|
566
|
+
if (!fs.existsSync(this.basePath)) {
|
|
567
|
+
fs.mkdirSync(this.basePath, { recursive: true });
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const content = format === 'json'
|
|
571
|
+
? JSON.stringify(fixture, null, 2)
|
|
572
|
+
: yaml.stringify(fixture);
|
|
573
|
+
|
|
574
|
+
fs.writeFileSync(filePath, content);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Create a fixture loader
|
|
580
|
+
*/
|
|
581
|
+
export function createFixtureLoader(basePath?: string): FixtureLoader {
|
|
582
|
+
return new FixtureLoader(basePath);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Fixture reference for lazy loading
|
|
587
|
+
*/
|
|
588
|
+
export class FixtureRef<T = any> {
|
|
589
|
+
constructor(
|
|
590
|
+
private loader: FixtureLoader,
|
|
591
|
+
private name: string,
|
|
592
|
+
private selector?: (data: T[]) => T
|
|
593
|
+
) {}
|
|
594
|
+
|
|
595
|
+
get(): T | undefined {
|
|
596
|
+
const data = this.loader.get<T>(this.name);
|
|
597
|
+
return this.selector ? this.selector(data) : data[0];
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
all(): T[] {
|
|
601
|
+
return this.loader.get<T>(this.name);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
`;
|
|
605
|
+
await (0, file_utils_1.writeFile)(path.join(testPath, 'fixtures/fixture.loader.ts'), content);
|
|
606
|
+
console.log(chalk_1.default.green(' ✓ Fixture loader'));
|
|
607
|
+
}
|
|
608
|
+
async function generateMockRepository(testPath) {
|
|
609
|
+
const content = `/**
|
|
610
|
+
* Mock Repository Implementation
|
|
611
|
+
* For testing without database
|
|
612
|
+
*/
|
|
613
|
+
|
|
614
|
+
export interface IMockRepository<T extends { id: string | number }> {
|
|
615
|
+
findAll(): Promise<T[]>;
|
|
616
|
+
findById(id: string | number): Promise<T | null>;
|
|
617
|
+
findOne(where: Partial<T>): Promise<T | null>;
|
|
618
|
+
findMany(where: Partial<T>): Promise<T[]>;
|
|
619
|
+
create(data: Partial<T>): Promise<T>;
|
|
620
|
+
update(id: string | number, data: Partial<T>): Promise<T | null>;
|
|
621
|
+
delete(id: string | number): Promise<void>;
|
|
622
|
+
clear(): void;
|
|
623
|
+
seed(data: T[]): void;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* In-memory mock repository
|
|
628
|
+
*/
|
|
629
|
+
export class MockRepository<T extends { id: string | number }> implements IMockRepository<T> {
|
|
630
|
+
private data: Map<string | number, T> = new Map();
|
|
631
|
+
private idGenerator: () => string | number;
|
|
632
|
+
|
|
633
|
+
constructor(
|
|
634
|
+
private options: {
|
|
635
|
+
idGenerator?: () => string | number;
|
|
636
|
+
initialData?: T[];
|
|
637
|
+
} = {}
|
|
638
|
+
) {
|
|
639
|
+
this.idGenerator = options.idGenerator || (() => \`mock_\${Date.now()}_\${Math.random().toString(36).slice(2, 9)}\`);
|
|
640
|
+
|
|
641
|
+
if (options.initialData) {
|
|
642
|
+
this.seed(options.initialData);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async findAll(): Promise<T[]> {
|
|
647
|
+
return Array.from(this.data.values());
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
async findById(id: string | number): Promise<T | null> {
|
|
651
|
+
return this.data.get(id) || null;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async findOne(where: Partial<T>): Promise<T | null> {
|
|
655
|
+
const all = Array.from(this.data.values());
|
|
656
|
+
return all.find(item => this.matches(item, where)) || null;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
async findMany(where: Partial<T>): Promise<T[]> {
|
|
660
|
+
const all = Array.from(this.data.values());
|
|
661
|
+
return all.filter(item => this.matches(item, where));
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async create(data: Partial<T>): Promise<T> {
|
|
665
|
+
const id = data.id || this.idGenerator();
|
|
666
|
+
const entity = { ...data, id } as T;
|
|
667
|
+
this.data.set(id, entity);
|
|
668
|
+
return entity;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
async update(id: string | number, data: Partial<T>): Promise<T | null> {
|
|
672
|
+
const existing = this.data.get(id);
|
|
673
|
+
if (!existing) return null;
|
|
674
|
+
|
|
675
|
+
const updated = { ...existing, ...data, id };
|
|
676
|
+
this.data.set(id, updated);
|
|
677
|
+
return updated;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
async delete(id: string | number): Promise<void> {
|
|
681
|
+
this.data.delete(id);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
clear(): void {
|
|
685
|
+
this.data.clear();
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
seed(data: T[]): void {
|
|
689
|
+
for (const item of data) {
|
|
690
|
+
this.data.set(item.id, item);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// For testing
|
|
695
|
+
getAll(): T[] {
|
|
696
|
+
return Array.from(this.data.values());
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
count(): number {
|
|
700
|
+
return this.data.size;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
private matches(item: T, where: Partial<T>): boolean {
|
|
704
|
+
return Object.entries(where).every(([key, value]) => {
|
|
705
|
+
return (item as any)[key] === value;
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Create a type-safe mock repository
|
|
712
|
+
*/
|
|
713
|
+
export function createMockRepository<T extends { id: string | number }>(
|
|
714
|
+
options?: {
|
|
715
|
+
idGenerator?: () => string | number;
|
|
716
|
+
initialData?: T[];
|
|
717
|
+
}
|
|
718
|
+
): MockRepository<T> {
|
|
719
|
+
return new MockRepository<T>(options);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Jest mock helpers for repositories
|
|
724
|
+
*/
|
|
725
|
+
export function createJestMockRepository<T extends { id: string | number }>(): {
|
|
726
|
+
findAll: jest.Mock<Promise<T[]>>;
|
|
727
|
+
findById: jest.Mock<Promise<T | null>>;
|
|
728
|
+
findOne: jest.Mock<Promise<T | null>>;
|
|
729
|
+
findMany: jest.Mock<Promise<T[]>>;
|
|
730
|
+
create: jest.Mock<Promise<T>>;
|
|
731
|
+
update: jest.Mock<Promise<T | null>>;
|
|
732
|
+
delete: jest.Mock<Promise<void>>;
|
|
733
|
+
save: jest.Mock<Promise<T>>;
|
|
734
|
+
remove: jest.Mock<Promise<void>>;
|
|
735
|
+
} {
|
|
736
|
+
return {
|
|
737
|
+
findAll: jest.fn().mockResolvedValue([]),
|
|
738
|
+
findById: jest.fn().mockResolvedValue(null),
|
|
739
|
+
findOne: jest.fn().mockResolvedValue(null),
|
|
740
|
+
findMany: jest.fn().mockResolvedValue([]),
|
|
741
|
+
create: jest.fn().mockImplementation((data) => Promise.resolve(data)),
|
|
742
|
+
update: jest.fn().mockImplementation((id, data) => Promise.resolve({ id, ...data })),
|
|
743
|
+
delete: jest.fn().mockResolvedValue(undefined),
|
|
744
|
+
save: jest.fn().mockImplementation((data) => Promise.resolve(data)),
|
|
745
|
+
remove: jest.fn().mockResolvedValue(undefined),
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Mock query builder for TypeORM
|
|
751
|
+
*/
|
|
752
|
+
export class MockQueryBuilder<T> {
|
|
753
|
+
private data: T[] = [];
|
|
754
|
+
private conditions: any[] = [];
|
|
755
|
+
private orderByFields: any[] = [];
|
|
756
|
+
private skipCount = 0;
|
|
757
|
+
private takeCount = 0;
|
|
758
|
+
private selectedFields: string[] = [];
|
|
759
|
+
private relations: string[] = [];
|
|
760
|
+
|
|
761
|
+
constructor(initialData: T[] = []) {
|
|
762
|
+
this.data = [...initialData];
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
select(selection: string | string[]): this {
|
|
766
|
+
this.selectedFields = Array.isArray(selection) ? selection : [selection];
|
|
767
|
+
return this;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
where(condition: string, params?: Record<string, any>): this {
|
|
771
|
+
this.conditions.push({ condition, params });
|
|
772
|
+
return this;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
andWhere(condition: string, params?: Record<string, any>): this {
|
|
776
|
+
return this.where(condition, params);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
orWhere(condition: string, params?: Record<string, any>): this {
|
|
780
|
+
return this.where(condition, params);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
orderBy(field: string, direction: 'ASC' | 'DESC' = 'ASC'): this {
|
|
784
|
+
this.orderByFields.push({ field, direction });
|
|
785
|
+
return this;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
addOrderBy(field: string, direction: 'ASC' | 'DESC' = 'ASC'): this {
|
|
789
|
+
return this.orderBy(field, direction);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
skip(count: number): this {
|
|
793
|
+
this.skipCount = count;
|
|
794
|
+
return this;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
take(count: number): this {
|
|
798
|
+
this.takeCount = count;
|
|
799
|
+
return this;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
leftJoinAndSelect(relation: string, alias: string): this {
|
|
803
|
+
this.relations.push(relation);
|
|
804
|
+
return this;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
async getMany(): Promise<T[]> {
|
|
808
|
+
let result = [...this.data];
|
|
809
|
+
|
|
810
|
+
if (this.skipCount > 0) {
|
|
811
|
+
result = result.slice(this.skipCount);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (this.takeCount > 0) {
|
|
815
|
+
result = result.slice(0, this.takeCount);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
return result;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
async getOne(): Promise<T | null> {
|
|
822
|
+
return this.data[0] || null;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
async getManyAndCount(): Promise<[T[], number]> {
|
|
826
|
+
const data = await this.getMany();
|
|
827
|
+
return [data, this.data.length];
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
async getCount(): Promise<number> {
|
|
831
|
+
return this.data.length;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
setData(data: T[]): this {
|
|
835
|
+
this.data = data;
|
|
836
|
+
return this;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
export function createMockQueryBuilder<T>(data: T[] = []): MockQueryBuilder<T> {
|
|
841
|
+
return new MockQueryBuilder(data);
|
|
842
|
+
}
|
|
843
|
+
`;
|
|
844
|
+
await (0, file_utils_1.writeFile)(path.join(testPath, 'mocks/mock.repository.ts'), content);
|
|
845
|
+
console.log(chalk_1.default.green(' ✓ Mock repository'));
|
|
846
|
+
}
|
|
847
|
+
async function generateDatabaseSeeder(testPath, options) {
|
|
848
|
+
const content = `import { DataSource, EntityTarget, Repository } from 'typeorm';
|
|
849
|
+
|
|
850
|
+
export interface SeederOptions {
|
|
851
|
+
truncate?: boolean;
|
|
852
|
+
order?: string[];
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
export type SeederFactory<T> = () => T | T[] | Promise<T | T[]>;
|
|
856
|
+
|
|
857
|
+
export interface SeederDefinition<T = any> {
|
|
858
|
+
entity: EntityTarget<T>;
|
|
859
|
+
factory: SeederFactory<T>;
|
|
860
|
+
count?: number;
|
|
861
|
+
dependencies?: EntityTarget<any>[];
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Database seeder for test setup
|
|
866
|
+
*/
|
|
867
|
+
export class DatabaseSeeder {
|
|
868
|
+
private seeders: Map<EntityTarget<any>, SeederDefinition> = new Map();
|
|
869
|
+
private seededData: Map<EntityTarget<any>, any[]> = new Map();
|
|
870
|
+
|
|
871
|
+
constructor(private dataSource: DataSource) {}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Register a seeder
|
|
875
|
+
*/
|
|
876
|
+
register<T>(definition: SeederDefinition<T>): this {
|
|
877
|
+
this.seeders.set(definition.entity, definition);
|
|
878
|
+
return this;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Run all seeders
|
|
883
|
+
*/
|
|
884
|
+
async seed(options: SeederOptions = {}): Promise<void> {
|
|
885
|
+
const order = options.order || this.getSeederOrder();
|
|
886
|
+
|
|
887
|
+
if (options.truncate) {
|
|
888
|
+
await this.truncateAll(order);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
for (const entity of order) {
|
|
892
|
+
const seeder = this.seeders.get(entity);
|
|
893
|
+
if (seeder) {
|
|
894
|
+
await this.runSeeder(seeder);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Run a single seeder
|
|
901
|
+
*/
|
|
902
|
+
private async runSeeder<T>(definition: SeederDefinition<T>): Promise<void> {
|
|
903
|
+
const repository = this.dataSource.getRepository(definition.entity);
|
|
904
|
+
const count = definition.count || 1;
|
|
905
|
+
const data: T[] = [];
|
|
906
|
+
|
|
907
|
+
for (let i = 0; i < count; i++) {
|
|
908
|
+
const result = await definition.factory();
|
|
909
|
+
const items = Array.isArray(result) ? result : [result];
|
|
910
|
+
data.push(...items);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const saved = await repository.save(data as any);
|
|
914
|
+
this.seededData.set(definition.entity, Array.isArray(saved) ? saved : [saved]);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Get seeded data for an entity
|
|
919
|
+
*/
|
|
920
|
+
getSeeded<T>(entity: EntityTarget<T>): T[] {
|
|
921
|
+
return this.seededData.get(entity) || [];
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Get first seeded item
|
|
926
|
+
*/
|
|
927
|
+
getFirst<T>(entity: EntityTarget<T>): T | undefined {
|
|
928
|
+
return this.getSeeded(entity)[0];
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Truncate all tables
|
|
933
|
+
*/
|
|
934
|
+
async truncateAll(order: EntityTarget<any>[]): Promise<void> {
|
|
935
|
+
// Reverse order for truncation to handle foreign keys
|
|
936
|
+
const reversed = [...order].reverse();
|
|
937
|
+
|
|
938
|
+
for (const entity of reversed) {
|
|
939
|
+
const repository = this.dataSource.getRepository(entity);
|
|
940
|
+
const tableName = repository.metadata.tableName;
|
|
941
|
+
|
|
942
|
+
try {
|
|
943
|
+
await this.dataSource.query(\`TRUNCATE TABLE "\${tableName}" CASCADE\`);
|
|
944
|
+
} catch {
|
|
945
|
+
// SQLite doesn't support TRUNCATE
|
|
946
|
+
await repository.clear();
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Clean up seeded data
|
|
953
|
+
*/
|
|
954
|
+
async cleanup(): Promise<void> {
|
|
955
|
+
const order = this.getSeederOrder().reverse();
|
|
956
|
+
|
|
957
|
+
for (const entity of order) {
|
|
958
|
+
const data = this.seededData.get(entity);
|
|
959
|
+
if (data && data.length > 0) {
|
|
960
|
+
const repository = this.dataSource.getRepository(entity);
|
|
961
|
+
await repository.remove(data);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
this.seededData.clear();
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Get seeder order based on dependencies
|
|
970
|
+
*/
|
|
971
|
+
private getSeederOrder(): EntityTarget<any>[] {
|
|
972
|
+
const ordered: EntityTarget<any>[] = [];
|
|
973
|
+
const visited = new Set<EntityTarget<any>>();
|
|
974
|
+
|
|
975
|
+
const visit = (entity: EntityTarget<any>) => {
|
|
976
|
+
if (visited.has(entity)) return;
|
|
977
|
+
visited.add(entity);
|
|
978
|
+
|
|
979
|
+
const seeder = this.seeders.get(entity);
|
|
980
|
+
if (seeder?.dependencies) {
|
|
981
|
+
for (const dep of seeder.dependencies) {
|
|
982
|
+
visit(dep);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
ordered.push(entity);
|
|
987
|
+
};
|
|
988
|
+
|
|
989
|
+
for (const entity of this.seeders.keys()) {
|
|
990
|
+
visit(entity);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
return ordered;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Create a database seeder
|
|
999
|
+
*/
|
|
1000
|
+
export function createSeeder(dataSource: DataSource): DatabaseSeeder {
|
|
1001
|
+
return new DatabaseSeeder(dataSource);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Test database helper
|
|
1006
|
+
*/
|
|
1007
|
+
export class TestDatabase {
|
|
1008
|
+
private dataSource: DataSource | null = null;
|
|
1009
|
+
|
|
1010
|
+
async connect(options?: any): Promise<DataSource> {
|
|
1011
|
+
this.dataSource = new DataSource({
|
|
1012
|
+
type: 'sqlite',
|
|
1013
|
+
database: ':memory:',
|
|
1014
|
+
synchronize: true,
|
|
1015
|
+
dropSchema: true,
|
|
1016
|
+
logging: false,
|
|
1017
|
+
entities: options?.entities || [],
|
|
1018
|
+
...options,
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
await this.dataSource.initialize();
|
|
1022
|
+
return this.dataSource;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
async disconnect(): Promise<void> {
|
|
1026
|
+
if (this.dataSource?.isInitialized) {
|
|
1027
|
+
await this.dataSource.destroy();
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
async reset(): Promise<void> {
|
|
1032
|
+
if (this.dataSource?.isInitialized) {
|
|
1033
|
+
await this.dataSource.synchronize(true);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
getDataSource(): DataSource {
|
|
1038
|
+
if (!this.dataSource) {
|
|
1039
|
+
throw new Error('Database not connected');
|
|
1040
|
+
}
|
|
1041
|
+
return this.dataSource;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
getRepository<T>(entity: EntityTarget<T>): Repository<T> {
|
|
1045
|
+
return this.getDataSource().getRepository(entity);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
export const testDb = new TestDatabase();
|
|
1050
|
+
`;
|
|
1051
|
+
await (0, file_utils_1.writeFile)(path.join(testPath, 'utils/database.seeder.ts'), content);
|
|
1052
|
+
console.log(chalk_1.default.green(' ✓ Database seeder'));
|
|
1053
|
+
}
|
|
1054
|
+
async function generateTestModuleBuilder(testPath) {
|
|
1055
|
+
const content = `import { Test, TestingModule, TestingModuleBuilder } from '@nestjs/testing';
|
|
1056
|
+
import { Type, Provider, ModuleMetadata, DynamicModule } from '@nestjs/common';
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* Fluent builder for test modules
|
|
1060
|
+
*/
|
|
1061
|
+
export class TestModuleBuilder {
|
|
1062
|
+
private imports: any[] = [];
|
|
1063
|
+
private providers: Provider[] = [];
|
|
1064
|
+
private controllers: Type<any>[] = [];
|
|
1065
|
+
private exports: any[] = [];
|
|
1066
|
+
private overrides: Map<Type<any> | string | symbol, any> = new Map();
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* Add imports
|
|
1070
|
+
*/
|
|
1071
|
+
withImports(...modules: any[]): this {
|
|
1072
|
+
this.imports.push(...modules);
|
|
1073
|
+
return this;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/**
|
|
1077
|
+
* Add providers
|
|
1078
|
+
*/
|
|
1079
|
+
withProviders(...providers: Provider[]): this {
|
|
1080
|
+
this.providers.push(...providers);
|
|
1081
|
+
return this;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* Add controllers
|
|
1086
|
+
*/
|
|
1087
|
+
withControllers(...controllers: Type<any>[]): this {
|
|
1088
|
+
this.controllers.push(...controllers);
|
|
1089
|
+
return this;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* Add exports
|
|
1094
|
+
*/
|
|
1095
|
+
withExports(...exports: any[]): this {
|
|
1096
|
+
this.exports.push(...exports);
|
|
1097
|
+
return this;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Override a provider
|
|
1102
|
+
*/
|
|
1103
|
+
override<T>(token: Type<T> | string | symbol, mock: any): this {
|
|
1104
|
+
this.overrides.set(token as any, mock);
|
|
1105
|
+
return this;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Override with a value
|
|
1110
|
+
*/
|
|
1111
|
+
overrideValue<T>(token: Type<T> | string | symbol, value: T): this {
|
|
1112
|
+
return this.override(token, { useValue: value });
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/**
|
|
1116
|
+
* Override with a factory
|
|
1117
|
+
*/
|
|
1118
|
+
overrideFactory<T>(
|
|
1119
|
+
token: Type<T> | string | symbol,
|
|
1120
|
+
factory: (...args: any[]) => T,
|
|
1121
|
+
inject?: any[]
|
|
1122
|
+
): this {
|
|
1123
|
+
return this.override(token, { useFactory: factory, inject });
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* Override with a class
|
|
1128
|
+
*/
|
|
1129
|
+
overrideClass<T>(token: Type<T> | string | symbol, mockClass: Type<T>): this {
|
|
1130
|
+
return this.override(token, { useClass: mockClass });
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* Mock a provider with jest mocks
|
|
1135
|
+
*/
|
|
1136
|
+
mock<T>(token: Type<T>): this {
|
|
1137
|
+
const mockProvider = this.createAutoMock(token);
|
|
1138
|
+
return this.override(token, { useValue: mockProvider });
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* Build the test module
|
|
1143
|
+
*/
|
|
1144
|
+
async compile(): Promise<TestingModule> {
|
|
1145
|
+
let builder = Test.createTestingModule({
|
|
1146
|
+
imports: this.imports,
|
|
1147
|
+
providers: this.providers,
|
|
1148
|
+
controllers: this.controllers,
|
|
1149
|
+
exports: this.exports,
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
// Apply overrides
|
|
1153
|
+
for (const [token, mock] of this.overrides) {
|
|
1154
|
+
if (mock.useValue !== undefined) {
|
|
1155
|
+
builder = builder.overrideProvider(token as any).useValue(mock.useValue);
|
|
1156
|
+
} else if (mock.useFactory !== undefined) {
|
|
1157
|
+
builder = builder.overrideProvider(token as any).useFactory({
|
|
1158
|
+
factory: mock.useFactory,
|
|
1159
|
+
inject: mock.inject,
|
|
1160
|
+
});
|
|
1161
|
+
} else if (mock.useClass !== undefined) {
|
|
1162
|
+
builder = builder.overrideProvider(token as any).useClass(mock.useClass);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
return builder.compile();
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/**
|
|
1170
|
+
* Create auto-mock for a class
|
|
1171
|
+
*/
|
|
1172
|
+
private createAutoMock<T>(classType: Type<T>): Record<string, jest.Mock> {
|
|
1173
|
+
const mock: Record<string, jest.Mock> = {};
|
|
1174
|
+
const prototype = classType.prototype;
|
|
1175
|
+
|
|
1176
|
+
const methodNames = Object.getOwnPropertyNames(prototype).filter(
|
|
1177
|
+
(name) => name !== 'constructor' && typeof prototype[name] === 'function'
|
|
1178
|
+
);
|
|
1179
|
+
|
|
1180
|
+
for (const methodName of methodNames) {
|
|
1181
|
+
mock[methodName] = jest.fn();
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
return mock;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* Create a new builder
|
|
1189
|
+
*/
|
|
1190
|
+
static create(): TestModuleBuilder {
|
|
1191
|
+
return new TestModuleBuilder();
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/**
|
|
1196
|
+
* Helper to create test module
|
|
1197
|
+
*/
|
|
1198
|
+
export function testModule(): TestModuleBuilder {
|
|
1199
|
+
return TestModuleBuilder.create();
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* Quick compile a module for testing
|
|
1204
|
+
*/
|
|
1205
|
+
export async function compileTestModule(metadata: ModuleMetadata): Promise<TestingModule> {
|
|
1206
|
+
return Test.createTestingModule(metadata).compile();
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/**
|
|
1210
|
+
* Test context with common utilities
|
|
1211
|
+
*/
|
|
1212
|
+
export interface TestContext<T = any> {
|
|
1213
|
+
module: TestingModule;
|
|
1214
|
+
service: T;
|
|
1215
|
+
cleanup: () => Promise<void>;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/**
|
|
1219
|
+
* Create a test context for a service
|
|
1220
|
+
*/
|
|
1221
|
+
export async function createTestContext<T>(
|
|
1222
|
+
serviceClass: Type<T>,
|
|
1223
|
+
options: {
|
|
1224
|
+
imports?: any[];
|
|
1225
|
+
providers?: Provider[];
|
|
1226
|
+
mocks?: Map<Type<any>, any>;
|
|
1227
|
+
} = {}
|
|
1228
|
+
): Promise<TestContext<T>> {
|
|
1229
|
+
const builder = testModule()
|
|
1230
|
+
.withImports(...(options.imports || []))
|
|
1231
|
+
.withProviders(serviceClass, ...(options.providers || []));
|
|
1232
|
+
|
|
1233
|
+
if (options.mocks) {
|
|
1234
|
+
for (const [token, mock] of options.mocks) {
|
|
1235
|
+
builder.override(token, { useValue: mock });
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
const module = await builder.compile();
|
|
1240
|
+
const service = module.get<T>(serviceClass);
|
|
1241
|
+
|
|
1242
|
+
return {
|
|
1243
|
+
module,
|
|
1244
|
+
service,
|
|
1245
|
+
cleanup: async () => {
|
|
1246
|
+
await module.close();
|
|
1247
|
+
},
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
`;
|
|
1251
|
+
await (0, file_utils_1.writeFile)(path.join(testPath, 'utils/test-module.builder.ts'), content);
|
|
1252
|
+
console.log(chalk_1.default.green(' ✓ Test module builder'));
|
|
1253
|
+
}
|
|
1254
|
+
async function generateTestUtils(testPath) {
|
|
1255
|
+
const content = `/**
|
|
1256
|
+
* Common test utilities
|
|
1257
|
+
*/
|
|
1258
|
+
|
|
1259
|
+
/**
|
|
1260
|
+
* Wait for a condition to be true
|
|
1261
|
+
*/
|
|
1262
|
+
export async function waitFor(
|
|
1263
|
+
condition: () => boolean | Promise<boolean>,
|
|
1264
|
+
options: { timeout?: number; interval?: number } = {}
|
|
1265
|
+
): Promise<void> {
|
|
1266
|
+
const { timeout = 5000, interval = 100 } = options;
|
|
1267
|
+
const start = Date.now();
|
|
1268
|
+
|
|
1269
|
+
while (Date.now() - start < timeout) {
|
|
1270
|
+
if (await condition()) {
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
await delay(interval);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
throw new Error('waitFor timeout exceeded');
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/**
|
|
1280
|
+
* Delay execution
|
|
1281
|
+
*/
|
|
1282
|
+
export function delay(ms: number): Promise<void> {
|
|
1283
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
/**
|
|
1287
|
+
* Retry a function until it succeeds
|
|
1288
|
+
*/
|
|
1289
|
+
export async function retry<T>(
|
|
1290
|
+
fn: () => Promise<T>,
|
|
1291
|
+
options: { maxAttempts?: number; delay?: number } = {}
|
|
1292
|
+
): Promise<T> {
|
|
1293
|
+
const { maxAttempts = 3, delay: delayMs = 100 } = options;
|
|
1294
|
+
let lastError: Error | undefined;
|
|
1295
|
+
|
|
1296
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1297
|
+
try {
|
|
1298
|
+
return await fn();
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
lastError = error as Error;
|
|
1301
|
+
if (attempt < maxAttempts) {
|
|
1302
|
+
await delay(delayMs * attempt);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
throw lastError;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
/**
|
|
1311
|
+
* Assert that a function throws
|
|
1312
|
+
*/
|
|
1313
|
+
export async function expectThrows(
|
|
1314
|
+
fn: () => Promise<any>,
|
|
1315
|
+
errorType?: new (...args: any[]) => Error
|
|
1316
|
+
): Promise<Error> {
|
|
1317
|
+
try {
|
|
1318
|
+
await fn();
|
|
1319
|
+
throw new Error('Expected function to throw');
|
|
1320
|
+
} catch (error) {
|
|
1321
|
+
if (errorType && !(error instanceof errorType)) {
|
|
1322
|
+
throw new Error(\`Expected \${errorType.name} but got \${(error as Error).constructor.name}\`);
|
|
1323
|
+
}
|
|
1324
|
+
return error as Error;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
/**
|
|
1329
|
+
* Assert async function rejects
|
|
1330
|
+
*/
|
|
1331
|
+
export async function expectRejects<T = Error>(
|
|
1332
|
+
promise: Promise<any>,
|
|
1333
|
+
errorType?: new (...args: any[]) => T
|
|
1334
|
+
): Promise<T> {
|
|
1335
|
+
try {
|
|
1336
|
+
await promise;
|
|
1337
|
+
throw new Error('Expected promise to reject');
|
|
1338
|
+
} catch (error) {
|
|
1339
|
+
if (errorType && !(error instanceof errorType)) {
|
|
1340
|
+
throw new Error(\`Expected \${errorType.name} but got \${(error as Error).constructor.name}\`);
|
|
1341
|
+
}
|
|
1342
|
+
return error as T;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
/**
|
|
1347
|
+
* Create a spy on a method
|
|
1348
|
+
*/
|
|
1349
|
+
export function spyOn<T extends object, K extends keyof T>(
|
|
1350
|
+
obj: T,
|
|
1351
|
+
method: K
|
|
1352
|
+
): jest.SpyInstance {
|
|
1353
|
+
return jest.spyOn(obj, method as any);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* Create a mock function with typed return
|
|
1358
|
+
*/
|
|
1359
|
+
export function mockFn<T>(): jest.Mock<T> {
|
|
1360
|
+
return jest.fn();
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
/**
|
|
1364
|
+
* Create a mock that resolves to a value
|
|
1365
|
+
*/
|
|
1366
|
+
export function mockResolve<T>(value: T): jest.Mock<Promise<T>> {
|
|
1367
|
+
return jest.fn().mockResolvedValue(value);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
/**
|
|
1371
|
+
* Create a mock that rejects with an error
|
|
1372
|
+
*/
|
|
1373
|
+
export function mockReject(error: Error): jest.Mock<Promise<never>> {
|
|
1374
|
+
return jest.fn().mockRejectedValue(error);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
/**
|
|
1378
|
+
* Reset all mocks
|
|
1379
|
+
*/
|
|
1380
|
+
export function resetMocks(...mocks: jest.Mock[]): void {
|
|
1381
|
+
mocks.forEach((mock) => mock.mockReset());
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
/**
|
|
1385
|
+
* Clear all mocks
|
|
1386
|
+
*/
|
|
1387
|
+
export function clearMocks(...mocks: jest.Mock[]): void {
|
|
1388
|
+
mocks.forEach((mock) => mock.mockClear());
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
/**
|
|
1392
|
+
* Assert object shape
|
|
1393
|
+
*/
|
|
1394
|
+
export function expectShape<T extends object>(
|
|
1395
|
+
actual: T,
|
|
1396
|
+
expected: Partial<T>
|
|
1397
|
+
): void {
|
|
1398
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
1399
|
+
expect((actual as any)[key]).toEqual(value);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
/**
|
|
1404
|
+
* Create a partial match for expect
|
|
1405
|
+
*/
|
|
1406
|
+
export function partialMatch<T>(expected: Partial<T>): T {
|
|
1407
|
+
return expect.objectContaining(expected);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
/**
|
|
1411
|
+
* Random data generators for tests
|
|
1412
|
+
*/
|
|
1413
|
+
export const random = {
|
|
1414
|
+
string: (length = 10) =>
|
|
1415
|
+
Math.random().toString(36).substring(2, 2 + length),
|
|
1416
|
+
number: (min = 0, max = 100) =>
|
|
1417
|
+
Math.floor(Math.random() * (max - min + 1)) + min,
|
|
1418
|
+
boolean: () => Math.random() > 0.5,
|
|
1419
|
+
email: () => \`test_\${Date.now()}_\${Math.random().toString(36).slice(2, 7)}@test.com\`,
|
|
1420
|
+
uuid: () =>
|
|
1421
|
+
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
1422
|
+
const r = (Math.random() * 16) | 0;
|
|
1423
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
1424
|
+
return v.toString(16);
|
|
1425
|
+
}),
|
|
1426
|
+
date: () => new Date(Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000),
|
|
1427
|
+
pick: <T>(arr: T[]) => arr[Math.floor(Math.random() * arr.length)],
|
|
1428
|
+
};
|
|
1429
|
+
|
|
1430
|
+
/**
|
|
1431
|
+
* Test timing utilities
|
|
1432
|
+
*/
|
|
1433
|
+
export class TestTimer {
|
|
1434
|
+
private startTime: number = 0;
|
|
1435
|
+
private endTime: number = 0;
|
|
1436
|
+
|
|
1437
|
+
start(): this {
|
|
1438
|
+
this.startTime = Date.now();
|
|
1439
|
+
return this;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
stop(): this {
|
|
1443
|
+
this.endTime = Date.now();
|
|
1444
|
+
return this;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
get elapsed(): number {
|
|
1448
|
+
return (this.endTime || Date.now()) - this.startTime;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
assertWithin(maxMs: number): void {
|
|
1452
|
+
if (this.elapsed > maxMs) {
|
|
1453
|
+
throw new Error(\`Expected to complete within \${maxMs}ms but took \${this.elapsed}ms\`);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
export function timer(): TestTimer {
|
|
1459
|
+
return new TestTimer().start();
|
|
1460
|
+
}
|
|
1461
|
+
`;
|
|
1462
|
+
await (0, file_utils_1.writeFile)(path.join(testPath, 'utils/test.utils.ts'), content);
|
|
1463
|
+
console.log(chalk_1.default.green(' ✓ Test utilities'));
|
|
1464
|
+
}
|
|
1465
|
+
async function generateSampleFactories(testPath) {
|
|
1466
|
+
const content = `/**
|
|
1467
|
+
* Sample factories demonstrating usage
|
|
1468
|
+
*/
|
|
1469
|
+
|
|
1470
|
+
import { faker } from '@faker-js/faker';
|
|
1471
|
+
import { BaseFactory, FakerHelpers, defineFactory } from './base.factory';
|
|
1472
|
+
import { factory, sequences } from './factory.builder';
|
|
1473
|
+
|
|
1474
|
+
/**
|
|
1475
|
+
* Example: User entity interface
|
|
1476
|
+
*/
|
|
1477
|
+
interface User {
|
|
1478
|
+
id: string;
|
|
1479
|
+
email: string;
|
|
1480
|
+
firstName: string;
|
|
1481
|
+
lastName: string;
|
|
1482
|
+
isActive: boolean;
|
|
1483
|
+
role: 'admin' | 'user' | 'guest';
|
|
1484
|
+
createdAt: Date;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
1488
|
+
* Example: Class-based factory
|
|
1489
|
+
*/
|
|
1490
|
+
export class UserFactory extends BaseFactory<User> {
|
|
1491
|
+
protected getDefaults(): User {
|
|
1492
|
+
return {
|
|
1493
|
+
id: FakerHelpers.id(),
|
|
1494
|
+
email: FakerHelpers.email(),
|
|
1495
|
+
firstName: FakerHelpers.firstName(),
|
|
1496
|
+
lastName: FakerHelpers.lastName(),
|
|
1497
|
+
isActive: true,
|
|
1498
|
+
role: 'user',
|
|
1499
|
+
createdAt: new Date(),
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
/**
|
|
1505
|
+
* Example: Function-based factory using defineFactory
|
|
1506
|
+
*/
|
|
1507
|
+
export const userFactory = defineFactory<User>(() => ({
|
|
1508
|
+
id: FakerHelpers.id(),
|
|
1509
|
+
email: FakerHelpers.email(),
|
|
1510
|
+
firstName: FakerHelpers.firstName(),
|
|
1511
|
+
lastName: FakerHelpers.lastName(),
|
|
1512
|
+
isActive: true,
|
|
1513
|
+
role: 'user',
|
|
1514
|
+
createdAt: new Date(),
|
|
1515
|
+
}));
|
|
1516
|
+
|
|
1517
|
+
/**
|
|
1518
|
+
* Example: Advanced factory with traits and sequences
|
|
1519
|
+
*/
|
|
1520
|
+
export const advancedUserFactory = factory<User>(() => ({
|
|
1521
|
+
id: FakerHelpers.id(),
|
|
1522
|
+
email: FakerHelpers.email(),
|
|
1523
|
+
firstName: FakerHelpers.firstName(),
|
|
1524
|
+
lastName: FakerHelpers.lastName(),
|
|
1525
|
+
isActive: true,
|
|
1526
|
+
role: 'user',
|
|
1527
|
+
createdAt: new Date(),
|
|
1528
|
+
}))
|
|
1529
|
+
// Define traits
|
|
1530
|
+
.trait('admin', { role: 'admin', isActive: true })
|
|
1531
|
+
.trait('inactive', { isActive: false })
|
|
1532
|
+
.trait('guest', { role: 'guest' })
|
|
1533
|
+
|
|
1534
|
+
// Define sequences
|
|
1535
|
+
.sequence('email', sequences.uniqueEmail)
|
|
1536
|
+
|
|
1537
|
+
// After build hook
|
|
1538
|
+
.afterBuild((user) => ({
|
|
1539
|
+
...user,
|
|
1540
|
+
email: user.email.toLowerCase(),
|
|
1541
|
+
}));
|
|
1542
|
+
|
|
1543
|
+
/**
|
|
1544
|
+
* Usage examples:
|
|
1545
|
+
*
|
|
1546
|
+
* // Basic usage
|
|
1547
|
+
* const user = new UserFactory().create();
|
|
1548
|
+
* const users = new UserFactory().createMany(5);
|
|
1549
|
+
*
|
|
1550
|
+
* // With overrides
|
|
1551
|
+
* const admin = new UserFactory().create({ role: 'admin' });
|
|
1552
|
+
*
|
|
1553
|
+
* // Using defineFactory
|
|
1554
|
+
* const user = userFactory.create();
|
|
1555
|
+
*
|
|
1556
|
+
* // Using advanced factory with traits
|
|
1557
|
+
* const adminUser = advancedUserFactory.use('admin').build();
|
|
1558
|
+
* const inactiveGuest = advancedUserFactory.use('inactive', 'guest').build();
|
|
1559
|
+
*
|
|
1560
|
+
* // With custom overrides
|
|
1561
|
+
* const customUser = advancedUserFactory
|
|
1562
|
+
* .use('admin')
|
|
1563
|
+
* .with({ firstName: 'John' })
|
|
1564
|
+
* .build();
|
|
1565
|
+
*/
|
|
1566
|
+
`;
|
|
1567
|
+
await (0, file_utils_1.writeFile)(path.join(testPath, 'factories/examples.ts'), content);
|
|
1568
|
+
console.log(chalk_1.default.green(' ✓ Sample factories'));
|
|
1569
|
+
}
|
|
1570
|
+
//# sourceMappingURL=test-factory-full.js.map
|