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,1368 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* API Contract Testing & Schema Validation Generator
|
|
4
|
+
* Generates contract testing infrastructure
|
|
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.setupApiContracts = setupApiContracts;
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
47
|
+
async function setupApiContracts(basePath, options = {}) {
|
|
48
|
+
console.log(chalk_1.default.bold.blue('\n📜 Setting up API Contract Testing Framework\n'));
|
|
49
|
+
const sharedPath = path.join(basePath, 'src/shared/contracts');
|
|
50
|
+
const testsPath = path.join(basePath, 'test/contracts');
|
|
51
|
+
for (const dir of [sharedPath, testsPath]) {
|
|
52
|
+
if (!fs.existsSync(dir)) {
|
|
53
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Generate contract validator
|
|
57
|
+
const validatorContent = generateContractValidator();
|
|
58
|
+
fs.writeFileSync(path.join(sharedPath, 'contract.validator.ts'), validatorContent);
|
|
59
|
+
console.log(chalk_1.default.green(` ✓ Created contract validator`));
|
|
60
|
+
// Generate schema validator
|
|
61
|
+
const schemaContent = generateSchemaValidator();
|
|
62
|
+
fs.writeFileSync(path.join(sharedPath, 'schema.validator.ts'), schemaContent);
|
|
63
|
+
console.log(chalk_1.default.green(` ✓ Created schema validator`));
|
|
64
|
+
// Generate consumer test helpers
|
|
65
|
+
const consumerContent = generateConsumerTestHelpers();
|
|
66
|
+
fs.writeFileSync(path.join(sharedPath, 'consumer-test.helpers.ts'), consumerContent);
|
|
67
|
+
console.log(chalk_1.default.green(` ✓ Created consumer test helpers`));
|
|
68
|
+
// Generate provider test helpers
|
|
69
|
+
const providerContent = generateProviderTestHelpers();
|
|
70
|
+
fs.writeFileSync(path.join(sharedPath, 'provider-test.helpers.ts'), providerContent);
|
|
71
|
+
console.log(chalk_1.default.green(` ✓ Created provider test helpers`));
|
|
72
|
+
// Generate contract store
|
|
73
|
+
const storeContent = generateContractStore();
|
|
74
|
+
fs.writeFileSync(path.join(sharedPath, 'contract.store.ts'), storeContent);
|
|
75
|
+
console.log(chalk_1.default.green(` ✓ Created contract store`));
|
|
76
|
+
// Generate sample contract test
|
|
77
|
+
const sampleTestContent = generateSampleContractTest();
|
|
78
|
+
fs.writeFileSync(path.join(testsPath, 'sample.contract.spec.ts'), sampleTestContent);
|
|
79
|
+
console.log(chalk_1.default.green(` ✓ Created sample contract test`));
|
|
80
|
+
console.log(chalk_1.default.bold.green('\n✅ API contract testing framework ready!\n'));
|
|
81
|
+
}
|
|
82
|
+
function generateContractValidator() {
|
|
83
|
+
return `/**
|
|
84
|
+
* Contract Validator
|
|
85
|
+
* Validates API responses against contracts
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
import Ajv, { ValidateFunction, ErrorObject } from 'ajv';
|
|
89
|
+
import addFormats from 'ajv-formats';
|
|
90
|
+
|
|
91
|
+
export interface ContractValidationResult {
|
|
92
|
+
valid: boolean;
|
|
93
|
+
errors: ContractError[];
|
|
94
|
+
warnings: ContractWarning[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ContractError {
|
|
98
|
+
path: string;
|
|
99
|
+
message: string;
|
|
100
|
+
expected?: any;
|
|
101
|
+
actual?: any;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface ContractWarning {
|
|
105
|
+
path: string;
|
|
106
|
+
message: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface Contract {
|
|
110
|
+
name: string;
|
|
111
|
+
version: string;
|
|
112
|
+
provider: string;
|
|
113
|
+
consumer?: string;
|
|
114
|
+
endpoints: EndpointContract[];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface EndpointContract {
|
|
118
|
+
method: string;
|
|
119
|
+
path: string;
|
|
120
|
+
description?: string;
|
|
121
|
+
request?: {
|
|
122
|
+
headers?: Record<string, SchemaDefinition>;
|
|
123
|
+
query?: Record<string, SchemaDefinition>;
|
|
124
|
+
body?: SchemaDefinition;
|
|
125
|
+
};
|
|
126
|
+
response: {
|
|
127
|
+
statusCode: number;
|
|
128
|
+
headers?: Record<string, SchemaDefinition>;
|
|
129
|
+
body?: SchemaDefinition;
|
|
130
|
+
};
|
|
131
|
+
examples?: {
|
|
132
|
+
request?: any;
|
|
133
|
+
response?: any;
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface SchemaDefinition {
|
|
138
|
+
type: string;
|
|
139
|
+
properties?: Record<string, SchemaDefinition>;
|
|
140
|
+
items?: SchemaDefinition;
|
|
141
|
+
required?: string[];
|
|
142
|
+
enum?: any[];
|
|
143
|
+
format?: string;
|
|
144
|
+
pattern?: string;
|
|
145
|
+
minimum?: number;
|
|
146
|
+
maximum?: number;
|
|
147
|
+
minLength?: number;
|
|
148
|
+
maxLength?: number;
|
|
149
|
+
nullable?: boolean;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Contract Validator
|
|
154
|
+
*/
|
|
155
|
+
export class ContractValidator {
|
|
156
|
+
private ajv: Ajv;
|
|
157
|
+
private validators: Map<string, ValidateFunction> = new Map();
|
|
158
|
+
|
|
159
|
+
constructor() {
|
|
160
|
+
this.ajv = new Ajv({ allErrors: true, strict: false });
|
|
161
|
+
addFormats(this.ajv);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Register a contract
|
|
166
|
+
*/
|
|
167
|
+
registerContract(contract: Contract): void {
|
|
168
|
+
for (const endpoint of contract.endpoints) {
|
|
169
|
+
const key = this.getEndpointKey(endpoint);
|
|
170
|
+
|
|
171
|
+
if (endpoint.request?.body) {
|
|
172
|
+
const requestKey = \`\${key}:request\`;
|
|
173
|
+
this.validators.set(
|
|
174
|
+
requestKey,
|
|
175
|
+
this.ajv.compile(this.toJsonSchema(endpoint.request.body)),
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (endpoint.response.body) {
|
|
180
|
+
const responseKey = \`\${key}:response:\${endpoint.response.statusCode}\`;
|
|
181
|
+
this.validators.set(
|
|
182
|
+
responseKey,
|
|
183
|
+
this.ajv.compile(this.toJsonSchema(endpoint.response.body)),
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Validate request against contract
|
|
191
|
+
*/
|
|
192
|
+
validateRequest(
|
|
193
|
+
method: string,
|
|
194
|
+
path: string,
|
|
195
|
+
body: any,
|
|
196
|
+
): ContractValidationResult {
|
|
197
|
+
const key = \`\${method.toUpperCase()}:\${path}:request\`;
|
|
198
|
+
return this.validate(key, body);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Validate response against contract
|
|
203
|
+
*/
|
|
204
|
+
validateResponse(
|
|
205
|
+
method: string,
|
|
206
|
+
path: string,
|
|
207
|
+
statusCode: number,
|
|
208
|
+
body: any,
|
|
209
|
+
): ContractValidationResult {
|
|
210
|
+
const key = \`\${method.toUpperCase()}:\${path}:response:\${statusCode}\`;
|
|
211
|
+
return this.validate(key, body);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Validate data against a key
|
|
216
|
+
*/
|
|
217
|
+
private validate(key: string, data: any): ContractValidationResult {
|
|
218
|
+
const validator = this.validators.get(key);
|
|
219
|
+
|
|
220
|
+
if (!validator) {
|
|
221
|
+
return {
|
|
222
|
+
valid: false,
|
|
223
|
+
errors: [{ path: '', message: \`No contract found for: \${key}\` }],
|
|
224
|
+
warnings: [],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const valid = validator(data);
|
|
229
|
+
|
|
230
|
+
if (valid) {
|
|
231
|
+
return { valid: true, errors: [], warnings: [] };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
valid: false,
|
|
236
|
+
errors: this.formatErrors(validator.errors || []),
|
|
237
|
+
warnings: [],
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Validate schema directly
|
|
243
|
+
*/
|
|
244
|
+
validateSchema(schema: SchemaDefinition, data: any): ContractValidationResult {
|
|
245
|
+
const validate = this.ajv.compile(this.toJsonSchema(schema));
|
|
246
|
+
const valid = validate(data);
|
|
247
|
+
|
|
248
|
+
if (valid) {
|
|
249
|
+
return { valid: true, errors: [], warnings: [] };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
valid: false,
|
|
254
|
+
errors: this.formatErrors(validate.errors || []),
|
|
255
|
+
warnings: [],
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private getEndpointKey(endpoint: EndpointContract): string {
|
|
260
|
+
return \`\${endpoint.method.toUpperCase()}:\${endpoint.path}\`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private toJsonSchema(schema: SchemaDefinition): any {
|
|
264
|
+
return {
|
|
265
|
+
type: schema.type,
|
|
266
|
+
properties: schema.properties
|
|
267
|
+
? Object.fromEntries(
|
|
268
|
+
Object.entries(schema.properties).map(([k, v]) => [
|
|
269
|
+
k,
|
|
270
|
+
this.toJsonSchema(v),
|
|
271
|
+
]),
|
|
272
|
+
)
|
|
273
|
+
: undefined,
|
|
274
|
+
items: schema.items ? this.toJsonSchema(schema.items) : undefined,
|
|
275
|
+
required: schema.required,
|
|
276
|
+
enum: schema.enum,
|
|
277
|
+
format: schema.format,
|
|
278
|
+
pattern: schema.pattern,
|
|
279
|
+
minimum: schema.minimum,
|
|
280
|
+
maximum: schema.maximum,
|
|
281
|
+
minLength: schema.minLength,
|
|
282
|
+
maxLength: schema.maxLength,
|
|
283
|
+
nullable: schema.nullable,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private formatErrors(errors: ErrorObject[]): ContractError[] {
|
|
288
|
+
return errors.map(error => ({
|
|
289
|
+
path: error.instancePath || '/',
|
|
290
|
+
message: error.message || 'Validation failed',
|
|
291
|
+
expected: error.params,
|
|
292
|
+
}));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Create contract validator
|
|
298
|
+
*/
|
|
299
|
+
export function createContractValidator(): ContractValidator {
|
|
300
|
+
return new ContractValidator();
|
|
301
|
+
}
|
|
302
|
+
`;
|
|
303
|
+
}
|
|
304
|
+
function generateSchemaValidator() {
|
|
305
|
+
return `/**
|
|
306
|
+
* Schema Validator
|
|
307
|
+
* Validates data against JSON schemas
|
|
308
|
+
*/
|
|
309
|
+
|
|
310
|
+
import Ajv, { ValidateFunction } from 'ajv';
|
|
311
|
+
import addFormats from 'ajv-formats';
|
|
312
|
+
|
|
313
|
+
export interface Schema {
|
|
314
|
+
$id?: string;
|
|
315
|
+
type: string;
|
|
316
|
+
properties?: Record<string, Schema | SchemaRef>;
|
|
317
|
+
items?: Schema | SchemaRef;
|
|
318
|
+
required?: string[];
|
|
319
|
+
additionalProperties?: boolean | Schema;
|
|
320
|
+
enum?: any[];
|
|
321
|
+
const?: any;
|
|
322
|
+
format?: string;
|
|
323
|
+
pattern?: string;
|
|
324
|
+
minimum?: number;
|
|
325
|
+
maximum?: number;
|
|
326
|
+
minLength?: number;
|
|
327
|
+
maxLength?: number;
|
|
328
|
+
minItems?: number;
|
|
329
|
+
maxItems?: number;
|
|
330
|
+
uniqueItems?: boolean;
|
|
331
|
+
oneOf?: (Schema | SchemaRef)[];
|
|
332
|
+
anyOf?: (Schema | SchemaRef)[];
|
|
333
|
+
allOf?: (Schema | SchemaRef)[];
|
|
334
|
+
not?: Schema | SchemaRef;
|
|
335
|
+
if?: Schema | SchemaRef;
|
|
336
|
+
then?: Schema | SchemaRef;
|
|
337
|
+
else?: Schema | SchemaRef;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export interface SchemaRef {
|
|
341
|
+
$ref: string;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export interface ValidationResult {
|
|
345
|
+
valid: boolean;
|
|
346
|
+
errors: ValidationError[];
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export interface ValidationError {
|
|
350
|
+
path: string;
|
|
351
|
+
message: string;
|
|
352
|
+
keyword: string;
|
|
353
|
+
params: Record<string, any>;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Schema Validator
|
|
358
|
+
*/
|
|
359
|
+
export class SchemaValidator {
|
|
360
|
+
private ajv: Ajv;
|
|
361
|
+
private schemas: Map<string, ValidateFunction> = new Map();
|
|
362
|
+
|
|
363
|
+
constructor() {
|
|
364
|
+
this.ajv = new Ajv({
|
|
365
|
+
allErrors: true,
|
|
366
|
+
strict: false,
|
|
367
|
+
validateFormats: true,
|
|
368
|
+
});
|
|
369
|
+
addFormats(this.ajv);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Register a schema
|
|
374
|
+
*/
|
|
375
|
+
register(id: string, schema: Schema): void {
|
|
376
|
+
this.ajv.addSchema({ ...schema, $id: id });
|
|
377
|
+
this.schemas.set(id, this.ajv.compile({ $ref: id }));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Validate data against a registered schema
|
|
382
|
+
*/
|
|
383
|
+
validate(schemaId: string, data: any): ValidationResult {
|
|
384
|
+
const validator = this.schemas.get(schemaId);
|
|
385
|
+
|
|
386
|
+
if (!validator) {
|
|
387
|
+
return {
|
|
388
|
+
valid: false,
|
|
389
|
+
errors: [
|
|
390
|
+
{
|
|
391
|
+
path: '',
|
|
392
|
+
message: \`Schema not found: \${schemaId}\`,
|
|
393
|
+
keyword: 'schema',
|
|
394
|
+
params: { schemaId },
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const valid = validator(data);
|
|
401
|
+
|
|
402
|
+
if (valid) {
|
|
403
|
+
return { valid: true, errors: [] };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
valid: false,
|
|
408
|
+
errors: (validator.errors || []).map(error => ({
|
|
409
|
+
path: error.instancePath || '/',
|
|
410
|
+
message: error.message || 'Validation failed',
|
|
411
|
+
keyword: error.keyword,
|
|
412
|
+
params: error.params,
|
|
413
|
+
})),
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Validate data against an inline schema
|
|
419
|
+
*/
|
|
420
|
+
validateInline(schema: Schema, data: any): ValidationResult {
|
|
421
|
+
const validate = this.ajv.compile(schema);
|
|
422
|
+
const valid = validate(data);
|
|
423
|
+
|
|
424
|
+
if (valid) {
|
|
425
|
+
return { valid: true, errors: [] };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
valid: false,
|
|
430
|
+
errors: (validate.errors || []).map(error => ({
|
|
431
|
+
path: error.instancePath || '/',
|
|
432
|
+
message: error.message || 'Validation failed',
|
|
433
|
+
keyword: error.keyword,
|
|
434
|
+
params: error.params,
|
|
435
|
+
})),
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Check if schema is registered
|
|
441
|
+
*/
|
|
442
|
+
has(schemaId: string): boolean {
|
|
443
|
+
return this.schemas.has(schemaId);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Remove a schema
|
|
448
|
+
*/
|
|
449
|
+
remove(schemaId: string): void {
|
|
450
|
+
this.ajv.removeSchema(schemaId);
|
|
451
|
+
this.schemas.delete(schemaId);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Schema builder for fluent API
|
|
457
|
+
*/
|
|
458
|
+
export class SchemaBuilder {
|
|
459
|
+
private schema: Schema = { type: 'object' };
|
|
460
|
+
|
|
461
|
+
type(type: string): this {
|
|
462
|
+
this.schema.type = type;
|
|
463
|
+
return this;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
property(name: string, schema: Schema | SchemaRef): this {
|
|
467
|
+
if (!this.schema.properties) {
|
|
468
|
+
this.schema.properties = {};
|
|
469
|
+
}
|
|
470
|
+
this.schema.properties[name] = schema;
|
|
471
|
+
return this;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
required(...fields: string[]): this {
|
|
475
|
+
this.schema.required = fields;
|
|
476
|
+
return this;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
items(schema: Schema | SchemaRef): this {
|
|
480
|
+
this.schema.items = schema;
|
|
481
|
+
return this;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
enum(...values: any[]): this {
|
|
485
|
+
this.schema.enum = values;
|
|
486
|
+
return this;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
format(format: string): this {
|
|
490
|
+
this.schema.format = format;
|
|
491
|
+
return this;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
pattern(pattern: string): this {
|
|
495
|
+
this.schema.pattern = pattern;
|
|
496
|
+
return this;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
min(value: number): this {
|
|
500
|
+
this.schema.minimum = value;
|
|
501
|
+
return this;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
max(value: number): this {
|
|
505
|
+
this.schema.maximum = value;
|
|
506
|
+
return this;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
build(): Schema {
|
|
510
|
+
return { ...this.schema };
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Create schema builder
|
|
516
|
+
*/
|
|
517
|
+
export function schema(): SchemaBuilder {
|
|
518
|
+
return new SchemaBuilder();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Common schema templates
|
|
523
|
+
*/
|
|
524
|
+
export const CommonSchemas = {
|
|
525
|
+
uuid: { type: 'string', format: 'uuid' } as Schema,
|
|
526
|
+
email: { type: 'string', format: 'email' } as Schema,
|
|
527
|
+
date: { type: 'string', format: 'date' } as Schema,
|
|
528
|
+
dateTime: { type: 'string', format: 'date-time' } as Schema,
|
|
529
|
+
uri: { type: 'string', format: 'uri' } as Schema,
|
|
530
|
+
positiveInteger: { type: 'integer', minimum: 1 } as Schema,
|
|
531
|
+
nonNegativeInteger: { type: 'integer', minimum: 0 } as Schema,
|
|
532
|
+
|
|
533
|
+
pagination: {
|
|
534
|
+
type: 'object',
|
|
535
|
+
properties: {
|
|
536
|
+
page: { type: 'integer', minimum: 1 },
|
|
537
|
+
pageSize: { type: 'integer', minimum: 1, maximum: 100 },
|
|
538
|
+
total: { type: 'integer', minimum: 0 },
|
|
539
|
+
totalPages: { type: 'integer', minimum: 0 },
|
|
540
|
+
},
|
|
541
|
+
required: ['page', 'pageSize', 'total', 'totalPages'],
|
|
542
|
+
} as Schema,
|
|
543
|
+
|
|
544
|
+
error: {
|
|
545
|
+
type: 'object',
|
|
546
|
+
properties: {
|
|
547
|
+
statusCode: { type: 'integer' },
|
|
548
|
+
message: { type: 'string' },
|
|
549
|
+
error: { type: 'string' },
|
|
550
|
+
},
|
|
551
|
+
required: ['statusCode', 'message'],
|
|
552
|
+
} as Schema,
|
|
553
|
+
};
|
|
554
|
+
`;
|
|
555
|
+
}
|
|
556
|
+
function generateConsumerTestHelpers() {
|
|
557
|
+
return `/**
|
|
558
|
+
* Consumer Test Helpers
|
|
559
|
+
* Helpers for consumer-driven contract testing
|
|
560
|
+
*/
|
|
561
|
+
|
|
562
|
+
import { ContractValidator, Contract, EndpointContract } from './contract.validator';
|
|
563
|
+
|
|
564
|
+
export interface Interaction {
|
|
565
|
+
description: string;
|
|
566
|
+
request: {
|
|
567
|
+
method: string;
|
|
568
|
+
path: string;
|
|
569
|
+
headers?: Record<string, string>;
|
|
570
|
+
query?: Record<string, string>;
|
|
571
|
+
body?: any;
|
|
572
|
+
};
|
|
573
|
+
response: {
|
|
574
|
+
statusCode: number;
|
|
575
|
+
headers?: Record<string, string>;
|
|
576
|
+
body?: any;
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export interface Pact {
|
|
581
|
+
consumer: string;
|
|
582
|
+
provider: string;
|
|
583
|
+
interactions: Interaction[];
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Consumer Contract Builder
|
|
588
|
+
* Creates pact-style consumer contracts
|
|
589
|
+
*/
|
|
590
|
+
export class ConsumerContractBuilder {
|
|
591
|
+
private pact: Pact;
|
|
592
|
+
private currentInteraction: Partial<Interaction> | null = null;
|
|
593
|
+
|
|
594
|
+
constructor(consumer: string, provider: string) {
|
|
595
|
+
this.pact = {
|
|
596
|
+
consumer,
|
|
597
|
+
provider,
|
|
598
|
+
interactions: [],
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Start new interaction
|
|
604
|
+
*/
|
|
605
|
+
given(description: string): this {
|
|
606
|
+
if (this.currentInteraction) {
|
|
607
|
+
this.commitInteraction();
|
|
608
|
+
}
|
|
609
|
+
this.currentInteraction = { description };
|
|
610
|
+
return this;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Define request
|
|
615
|
+
*/
|
|
616
|
+
uponReceiving(method: string, path: string): this {
|
|
617
|
+
if (!this.currentInteraction) {
|
|
618
|
+
throw new Error('Call given() first');
|
|
619
|
+
}
|
|
620
|
+
this.currentInteraction.request = { method, path };
|
|
621
|
+
return this;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Add request headers
|
|
626
|
+
*/
|
|
627
|
+
withHeaders(headers: Record<string, string>): this {
|
|
628
|
+
if (!this.currentInteraction?.request) {
|
|
629
|
+
throw new Error('Call uponReceiving() first');
|
|
630
|
+
}
|
|
631
|
+
this.currentInteraction.request.headers = headers;
|
|
632
|
+
return this;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Add request query
|
|
637
|
+
*/
|
|
638
|
+
withQuery(query: Record<string, string>): this {
|
|
639
|
+
if (!this.currentInteraction?.request) {
|
|
640
|
+
throw new Error('Call uponReceiving() first');
|
|
641
|
+
}
|
|
642
|
+
this.currentInteraction.request.query = query;
|
|
643
|
+
return this;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Add request body
|
|
648
|
+
*/
|
|
649
|
+
withBody(body: any): this {
|
|
650
|
+
if (!this.currentInteraction?.request) {
|
|
651
|
+
throw new Error('Call uponReceiving() first');
|
|
652
|
+
}
|
|
653
|
+
this.currentInteraction.request.body = body;
|
|
654
|
+
return this;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Define expected response
|
|
659
|
+
*/
|
|
660
|
+
willRespondWith(statusCode: number): this {
|
|
661
|
+
if (!this.currentInteraction) {
|
|
662
|
+
throw new Error('Call given() first');
|
|
663
|
+
}
|
|
664
|
+
this.currentInteraction.response = { statusCode };
|
|
665
|
+
return this;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Add response headers
|
|
670
|
+
*/
|
|
671
|
+
withResponseHeaders(headers: Record<string, string>): this {
|
|
672
|
+
if (!this.currentInteraction?.response) {
|
|
673
|
+
throw new Error('Call willRespondWith() first');
|
|
674
|
+
}
|
|
675
|
+
this.currentInteraction.response.headers = headers;
|
|
676
|
+
return this;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Add response body
|
|
681
|
+
*/
|
|
682
|
+
withResponseBody(body: any): this {
|
|
683
|
+
if (!this.currentInteraction?.response) {
|
|
684
|
+
throw new Error('Call willRespondWith() first');
|
|
685
|
+
}
|
|
686
|
+
this.currentInteraction.response.body = body;
|
|
687
|
+
return this;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Build the pact
|
|
692
|
+
*/
|
|
693
|
+
build(): Pact {
|
|
694
|
+
if (this.currentInteraction) {
|
|
695
|
+
this.commitInteraction();
|
|
696
|
+
}
|
|
697
|
+
return this.pact;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
private commitInteraction(): void {
|
|
701
|
+
if (
|
|
702
|
+
this.currentInteraction?.description &&
|
|
703
|
+
this.currentInteraction?.request &&
|
|
704
|
+
this.currentInteraction?.response
|
|
705
|
+
) {
|
|
706
|
+
this.pact.interactions.push(this.currentInteraction as Interaction);
|
|
707
|
+
}
|
|
708
|
+
this.currentInteraction = null;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Consumer test runner
|
|
714
|
+
*/
|
|
715
|
+
export class ConsumerTestRunner {
|
|
716
|
+
private validator: ContractValidator;
|
|
717
|
+
|
|
718
|
+
constructor() {
|
|
719
|
+
this.validator = new ContractValidator();
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Run consumer tests against a mock provider
|
|
724
|
+
*/
|
|
725
|
+
async runTests(
|
|
726
|
+
pact: Pact,
|
|
727
|
+
mockProvider: (interaction: Interaction) => Promise<any>,
|
|
728
|
+
): Promise<TestResult[]> {
|
|
729
|
+
const results: TestResult[] = [];
|
|
730
|
+
|
|
731
|
+
for (const interaction of pact.interactions) {
|
|
732
|
+
const result = await this.runInteraction(interaction, mockProvider);
|
|
733
|
+
results.push(result);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return results;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
private async runInteraction(
|
|
740
|
+
interaction: Interaction,
|
|
741
|
+
mockProvider: (interaction: Interaction) => Promise<any>,
|
|
742
|
+
): Promise<TestResult> {
|
|
743
|
+
try {
|
|
744
|
+
const response = await mockProvider(interaction);
|
|
745
|
+
|
|
746
|
+
// Validate response matches expected
|
|
747
|
+
const matches = this.matchResponse(interaction.response, response);
|
|
748
|
+
|
|
749
|
+
return {
|
|
750
|
+
description: interaction.description,
|
|
751
|
+
passed: matches.passed,
|
|
752
|
+
errors: matches.errors,
|
|
753
|
+
};
|
|
754
|
+
} catch (error) {
|
|
755
|
+
return {
|
|
756
|
+
description: interaction.description,
|
|
757
|
+
passed: false,
|
|
758
|
+
errors: [(error as Error).message],
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
private matchResponse(
|
|
764
|
+
expected: Interaction['response'],
|
|
765
|
+
actual: any,
|
|
766
|
+
): { passed: boolean; errors: string[] } {
|
|
767
|
+
const errors: string[] = [];
|
|
768
|
+
|
|
769
|
+
if (actual.statusCode !== expected.statusCode) {
|
|
770
|
+
errors.push(
|
|
771
|
+
\`Status code mismatch: expected \${expected.statusCode}, got \${actual.statusCode}\`,
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Deep compare bodies if provided
|
|
776
|
+
if (expected.body) {
|
|
777
|
+
const bodyMatch = this.deepMatch(expected.body, actual.body);
|
|
778
|
+
if (!bodyMatch.matches) {
|
|
779
|
+
errors.push(\`Body mismatch: \${bodyMatch.path}\`);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
return {
|
|
784
|
+
passed: errors.length === 0,
|
|
785
|
+
errors,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
private deepMatch(
|
|
790
|
+
expected: any,
|
|
791
|
+
actual: any,
|
|
792
|
+
path: string = '',
|
|
793
|
+
): { matches: boolean; path: string } {
|
|
794
|
+
if (typeof expected !== typeof actual) {
|
|
795
|
+
return { matches: false, path: path || 'root' };
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (expected === null || actual === null) {
|
|
799
|
+
return { matches: expected === actual, path };
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (typeof expected !== 'object') {
|
|
803
|
+
return { matches: expected === actual, path };
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (Array.isArray(expected)) {
|
|
807
|
+
if (!Array.isArray(actual) || expected.length !== actual.length) {
|
|
808
|
+
return { matches: false, path };
|
|
809
|
+
}
|
|
810
|
+
for (let i = 0; i < expected.length; i++) {
|
|
811
|
+
const result = this.deepMatch(expected[i], actual[i], \`\${path}[\${i}]\`);
|
|
812
|
+
if (!result.matches) return result;
|
|
813
|
+
}
|
|
814
|
+
return { matches: true, path };
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
for (const key of Object.keys(expected)) {
|
|
818
|
+
const result = this.deepMatch(
|
|
819
|
+
expected[key],
|
|
820
|
+
actual[key],
|
|
821
|
+
path ? \`\${path}.\${key}\` : key,
|
|
822
|
+
);
|
|
823
|
+
if (!result.matches) return result;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
return { matches: true, path };
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
interface TestResult {
|
|
831
|
+
description: string;
|
|
832
|
+
passed: boolean;
|
|
833
|
+
errors: string[];
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Create consumer contract builder
|
|
838
|
+
*/
|
|
839
|
+
export function consumerContract(consumer: string, provider: string): ConsumerContractBuilder {
|
|
840
|
+
return new ConsumerContractBuilder(consumer, provider);
|
|
841
|
+
}
|
|
842
|
+
`;
|
|
843
|
+
}
|
|
844
|
+
function generateProviderTestHelpers() {
|
|
845
|
+
return `/**
|
|
846
|
+
* Provider Test Helpers
|
|
847
|
+
* Helpers for provider-side contract verification
|
|
848
|
+
*/
|
|
849
|
+
|
|
850
|
+
import { INestApplication } from '@nestjs/common';
|
|
851
|
+
import * as request from 'supertest';
|
|
852
|
+
import { Pact, Interaction } from './consumer-test.helpers';
|
|
853
|
+
import { ContractValidator } from './contract.validator';
|
|
854
|
+
|
|
855
|
+
export interface ProviderState {
|
|
856
|
+
name: string;
|
|
857
|
+
setup: () => Promise<void>;
|
|
858
|
+
teardown?: () => Promise<void>;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
export interface VerificationResult {
|
|
862
|
+
interaction: string;
|
|
863
|
+
passed: boolean;
|
|
864
|
+
errors: string[];
|
|
865
|
+
duration: number;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Provider Verifier
|
|
870
|
+
* Verifies provider against consumer contracts
|
|
871
|
+
*/
|
|
872
|
+
export class ProviderVerifier {
|
|
873
|
+
private states: Map<string, ProviderState> = new Map();
|
|
874
|
+
private validator: ContractValidator;
|
|
875
|
+
|
|
876
|
+
constructor(private readonly app: INestApplication) {
|
|
877
|
+
this.validator = new ContractValidator();
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Register provider state
|
|
882
|
+
*/
|
|
883
|
+
registerState(state: ProviderState): this {
|
|
884
|
+
this.states.set(state.name, state);
|
|
885
|
+
return this;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Verify all interactions in a pact
|
|
890
|
+
*/
|
|
891
|
+
async verify(pact: Pact): Promise<VerificationResult[]> {
|
|
892
|
+
const results: VerificationResult[] = [];
|
|
893
|
+
|
|
894
|
+
for (const interaction of pact.interactions) {
|
|
895
|
+
const result = await this.verifyInteraction(interaction);
|
|
896
|
+
results.push(result);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
return results;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Verify single interaction
|
|
904
|
+
*/
|
|
905
|
+
async verifyInteraction(interaction: Interaction): Promise<VerificationResult> {
|
|
906
|
+
const startTime = Date.now();
|
|
907
|
+
const errors: string[] = [];
|
|
908
|
+
|
|
909
|
+
try {
|
|
910
|
+
// Setup provider state if exists
|
|
911
|
+
const state = this.states.get(interaction.description);
|
|
912
|
+
if (state) {
|
|
913
|
+
await state.setup();
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Make request
|
|
917
|
+
const response = await this.makeRequest(interaction);
|
|
918
|
+
|
|
919
|
+
// Verify response
|
|
920
|
+
if (response.status !== interaction.response.statusCode) {
|
|
921
|
+
errors.push(
|
|
922
|
+
\`Status code mismatch: expected \${interaction.response.statusCode}, got \${response.status}\`,
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Verify response body
|
|
927
|
+
if (interaction.response.body) {
|
|
928
|
+
const bodyErrors = this.verifyBody(interaction.response.body, response.body);
|
|
929
|
+
errors.push(...bodyErrors);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Verify response headers
|
|
933
|
+
if (interaction.response.headers) {
|
|
934
|
+
const headerErrors = this.verifyHeaders(interaction.response.headers, response.headers);
|
|
935
|
+
errors.push(...headerErrors);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Teardown provider state
|
|
939
|
+
if (state?.teardown) {
|
|
940
|
+
await state.teardown();
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
return {
|
|
944
|
+
interaction: interaction.description,
|
|
945
|
+
passed: errors.length === 0,
|
|
946
|
+
errors,
|
|
947
|
+
duration: Date.now() - startTime,
|
|
948
|
+
};
|
|
949
|
+
} catch (error) {
|
|
950
|
+
return {
|
|
951
|
+
interaction: interaction.description,
|
|
952
|
+
passed: false,
|
|
953
|
+
errors: [(error as Error).message],
|
|
954
|
+
duration: Date.now() - startTime,
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
private async makeRequest(interaction: Interaction): Promise<any> {
|
|
960
|
+
const req = interaction.request;
|
|
961
|
+
let testRequest = request(this.app.getHttpServer());
|
|
962
|
+
|
|
963
|
+
switch (req.method.toUpperCase()) {
|
|
964
|
+
case 'GET':
|
|
965
|
+
testRequest = testRequest.get(req.path);
|
|
966
|
+
break;
|
|
967
|
+
case 'POST':
|
|
968
|
+
testRequest = testRequest.post(req.path);
|
|
969
|
+
break;
|
|
970
|
+
case 'PUT':
|
|
971
|
+
testRequest = testRequest.put(req.path);
|
|
972
|
+
break;
|
|
973
|
+
case 'PATCH':
|
|
974
|
+
testRequest = testRequest.patch(req.path);
|
|
975
|
+
break;
|
|
976
|
+
case 'DELETE':
|
|
977
|
+
testRequest = testRequest.delete(req.path);
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
if (req.headers) {
|
|
982
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
983
|
+
testRequest = testRequest.set(key, value);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
if (req.query) {
|
|
988
|
+
testRequest = testRequest.query(req.query);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
if (req.body) {
|
|
992
|
+
testRequest = testRequest.send(req.body);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
return testRequest;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
private verifyBody(expected: any, actual: any): string[] {
|
|
999
|
+
const errors: string[] = [];
|
|
1000
|
+
this.compareObjects(expected, actual, '', errors);
|
|
1001
|
+
return errors;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
private verifyHeaders(expected: Record<string, string>, actual: any): string[] {
|
|
1005
|
+
const errors: string[] = [];
|
|
1006
|
+
|
|
1007
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
1008
|
+
const actualValue = actual[key.toLowerCase()];
|
|
1009
|
+
if (actualValue !== value) {
|
|
1010
|
+
errors.push(\`Header '\${key}' mismatch: expected '\${value}', got '\${actualValue}'\`);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
return errors;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
private compareObjects(expected: any, actual: any, path: string, errors: string[]): void {
|
|
1018
|
+
if (expected === null || expected === undefined) {
|
|
1019
|
+
if (actual !== null && actual !== undefined) {
|
|
1020
|
+
errors.push(\`\${path || 'root'}: expected null/undefined, got \${typeof actual}\`);
|
|
1021
|
+
}
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
if (typeof expected !== typeof actual) {
|
|
1026
|
+
errors.push(\`\${path || 'root'}: type mismatch - expected \${typeof expected}, got \${typeof actual}\`);
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
if (Array.isArray(expected)) {
|
|
1031
|
+
if (!Array.isArray(actual)) {
|
|
1032
|
+
errors.push(\`\${path || 'root'}: expected array, got \${typeof actual}\`);
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
for (let i = 0; i < expected.length; i++) {
|
|
1037
|
+
this.compareObjects(expected[i], actual[i], \`\${path}[\${i}]\`, errors);
|
|
1038
|
+
}
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
if (typeof expected === 'object') {
|
|
1043
|
+
for (const key of Object.keys(expected)) {
|
|
1044
|
+
this.compareObjects(
|
|
1045
|
+
expected[key],
|
|
1046
|
+
actual?.[key],
|
|
1047
|
+
path ? \`\${path}.\${key}\` : key,
|
|
1048
|
+
errors,
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
if (expected !== actual) {
|
|
1055
|
+
errors.push(\`\${path || 'root'}: value mismatch - expected \${expected}, got \${actual}\`);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* Create provider verifier
|
|
1062
|
+
*/
|
|
1063
|
+
export function createVerifier(app: INestApplication): ProviderVerifier {
|
|
1064
|
+
return new ProviderVerifier(app);
|
|
1065
|
+
}
|
|
1066
|
+
`;
|
|
1067
|
+
}
|
|
1068
|
+
function generateContractStore() {
|
|
1069
|
+
return `/**
|
|
1070
|
+
* Contract Store
|
|
1071
|
+
* Centralized storage for API contracts
|
|
1072
|
+
*/
|
|
1073
|
+
|
|
1074
|
+
import * as fs from 'fs';
|
|
1075
|
+
import * as path from 'path';
|
|
1076
|
+
import { Contract } from './contract.validator';
|
|
1077
|
+
import { Pact } from './consumer-test.helpers';
|
|
1078
|
+
|
|
1079
|
+
export interface StoredContract {
|
|
1080
|
+
id: string;
|
|
1081
|
+
contract: Contract | Pact;
|
|
1082
|
+
version: string;
|
|
1083
|
+
createdAt: Date;
|
|
1084
|
+
updatedAt: Date;
|
|
1085
|
+
metadata?: Record<string, any>;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* Contract Store
|
|
1090
|
+
*/
|
|
1091
|
+
export class ContractStore {
|
|
1092
|
+
private contracts: Map<string, StoredContract> = new Map();
|
|
1093
|
+
private basePath: string;
|
|
1094
|
+
|
|
1095
|
+
constructor(basePath?: string) {
|
|
1096
|
+
this.basePath = basePath || path.join(process.cwd(), 'contracts');
|
|
1097
|
+
this.ensureDirectory();
|
|
1098
|
+
this.loadContracts();
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* Store a contract
|
|
1103
|
+
*/
|
|
1104
|
+
store(id: string, contract: Contract | Pact, version: string, metadata?: Record<string, any>): void {
|
|
1105
|
+
const stored: StoredContract = {
|
|
1106
|
+
id,
|
|
1107
|
+
contract,
|
|
1108
|
+
version,
|
|
1109
|
+
createdAt: new Date(),
|
|
1110
|
+
updatedAt: new Date(),
|
|
1111
|
+
metadata,
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
this.contracts.set(id, stored);
|
|
1115
|
+
this.persist(id, stored);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* Get a contract
|
|
1120
|
+
*/
|
|
1121
|
+
get(id: string): StoredContract | undefined {
|
|
1122
|
+
return this.contracts.get(id);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* Get contract by provider and consumer
|
|
1127
|
+
*/
|
|
1128
|
+
getByProviderConsumer(provider: string, consumer: string): StoredContract[] {
|
|
1129
|
+
return Array.from(this.contracts.values()).filter(stored => {
|
|
1130
|
+
const contract = stored.contract;
|
|
1131
|
+
if ('provider' in contract) {
|
|
1132
|
+
return contract.provider === provider &&
|
|
1133
|
+
('consumer' in contract ? contract.consumer === consumer : true);
|
|
1134
|
+
}
|
|
1135
|
+
return false;
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* List all contracts
|
|
1141
|
+
*/
|
|
1142
|
+
list(): StoredContract[] {
|
|
1143
|
+
return Array.from(this.contracts.values());
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
/**
|
|
1147
|
+
* Remove a contract
|
|
1148
|
+
*/
|
|
1149
|
+
remove(id: string): boolean {
|
|
1150
|
+
const existed = this.contracts.delete(id);
|
|
1151
|
+
if (existed) {
|
|
1152
|
+
this.deleteFile(id);
|
|
1153
|
+
}
|
|
1154
|
+
return existed;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/**
|
|
1158
|
+
* Get contract versions
|
|
1159
|
+
*/
|
|
1160
|
+
getVersions(providerId: string): string[] {
|
|
1161
|
+
return Array.from(this.contracts.values())
|
|
1162
|
+
.filter(stored => stored.id.startsWith(providerId))
|
|
1163
|
+
.map(stored => stored.version)
|
|
1164
|
+
.filter((v, i, a) => a.indexOf(v) === i)
|
|
1165
|
+
.sort();
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* Compare two contract versions
|
|
1170
|
+
*/
|
|
1171
|
+
compareVersions(id: string, version1: string, version2: string): ContractDiff | null {
|
|
1172
|
+
const c1 = this.contracts.get(\`\${id}:\${version1}\`);
|
|
1173
|
+
const c2 = this.contracts.get(\`\${id}:\${version2}\`);
|
|
1174
|
+
|
|
1175
|
+
if (!c1 || !c2) return null;
|
|
1176
|
+
|
|
1177
|
+
return this.diffContracts(c1.contract, c2.contract);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
private ensureDirectory(): void {
|
|
1181
|
+
if (!fs.existsSync(this.basePath)) {
|
|
1182
|
+
fs.mkdirSync(this.basePath, { recursive: true });
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
private loadContracts(): void {
|
|
1187
|
+
if (!fs.existsSync(this.basePath)) return;
|
|
1188
|
+
|
|
1189
|
+
const files = fs.readdirSync(this.basePath).filter(f => f.endsWith('.json'));
|
|
1190
|
+
|
|
1191
|
+
for (const file of files) {
|
|
1192
|
+
try {
|
|
1193
|
+
const content = fs.readFileSync(path.join(this.basePath, file), 'utf-8');
|
|
1194
|
+
const stored = JSON.parse(content) as StoredContract;
|
|
1195
|
+
stored.createdAt = new Date(stored.createdAt);
|
|
1196
|
+
stored.updatedAt = new Date(stored.updatedAt);
|
|
1197
|
+
this.contracts.set(stored.id, stored);
|
|
1198
|
+
} catch (error) {
|
|
1199
|
+
console.warn(\`Failed to load contract: \${file}\`);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
private persist(id: string, stored: StoredContract): void {
|
|
1205
|
+
const filePath = path.join(this.basePath, \`\${id.replace(/[:/]/g, '_')}.json\`);
|
|
1206
|
+
fs.writeFileSync(filePath, JSON.stringify(stored, null, 2));
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
private deleteFile(id: string): void {
|
|
1210
|
+
const filePath = path.join(this.basePath, \`\${id.replace(/[:/]/g, '_')}.json\`);
|
|
1211
|
+
if (fs.existsSync(filePath)) {
|
|
1212
|
+
fs.unlinkSync(filePath);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
private diffContracts(c1: Contract | Pact, c2: Contract | Pact): ContractDiff {
|
|
1217
|
+
const diff: ContractDiff = {
|
|
1218
|
+
added: [],
|
|
1219
|
+
removed: [],
|
|
1220
|
+
modified: [],
|
|
1221
|
+
};
|
|
1222
|
+
|
|
1223
|
+
// Simple diff based on endpoints/interactions
|
|
1224
|
+
const e1 = this.getEndpoints(c1);
|
|
1225
|
+
const e2 = this.getEndpoints(c2);
|
|
1226
|
+
|
|
1227
|
+
const keys1 = new Set(e1.map(e => e.key));
|
|
1228
|
+
const keys2 = new Set(e2.map(e => e.key));
|
|
1229
|
+
|
|
1230
|
+
for (const e of e2) {
|
|
1231
|
+
if (!keys1.has(e.key)) {
|
|
1232
|
+
diff.added.push(e.key);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
for (const e of e1) {
|
|
1237
|
+
if (!keys2.has(e.key)) {
|
|
1238
|
+
diff.removed.push(e.key);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
return diff;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
private getEndpoints(contract: Contract | Pact): { key: string; data: any }[] {
|
|
1246
|
+
if ('endpoints' in contract) {
|
|
1247
|
+
return contract.endpoints.map(e => ({
|
|
1248
|
+
key: \`\${e.method}:\${e.path}\`,
|
|
1249
|
+
data: e,
|
|
1250
|
+
}));
|
|
1251
|
+
}
|
|
1252
|
+
if ('interactions' in contract) {
|
|
1253
|
+
return contract.interactions.map(i => ({
|
|
1254
|
+
key: \`\${i.request.method}:\${i.request.path}\`,
|
|
1255
|
+
data: i,
|
|
1256
|
+
}));
|
|
1257
|
+
}
|
|
1258
|
+
return [];
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
interface ContractDiff {
|
|
1263
|
+
added: string[];
|
|
1264
|
+
removed: string[];
|
|
1265
|
+
modified: string[];
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* Create contract store
|
|
1270
|
+
*/
|
|
1271
|
+
export function createContractStore(basePath?: string): ContractStore {
|
|
1272
|
+
return new ContractStore(basePath);
|
|
1273
|
+
}
|
|
1274
|
+
`;
|
|
1275
|
+
}
|
|
1276
|
+
function generateSampleContractTest() {
|
|
1277
|
+
return `/**
|
|
1278
|
+
* Sample Contract Test
|
|
1279
|
+
* Example of consumer-driven contract testing
|
|
1280
|
+
*/
|
|
1281
|
+
|
|
1282
|
+
import { consumerContract, ConsumerTestRunner } from '../src/shared/contracts/consumer-test.helpers';
|
|
1283
|
+
import { createVerifier } from '../src/shared/contracts/provider-test.helpers';
|
|
1284
|
+
import { ContractValidator } from '../src/shared/contracts/contract.validator';
|
|
1285
|
+
|
|
1286
|
+
describe('User API Contract', () => {
|
|
1287
|
+
describe('Consumer Contract', () => {
|
|
1288
|
+
it('should define user creation contract', () => {
|
|
1289
|
+
const pact = consumerContract('web-app', 'user-service')
|
|
1290
|
+
.given('user creation')
|
|
1291
|
+
.uponReceiving('POST', '/api/users')
|
|
1292
|
+
.withHeaders({ 'Content-Type': 'application/json' })
|
|
1293
|
+
.withBody({
|
|
1294
|
+
email: 'test@example.com',
|
|
1295
|
+
name: 'Test User',
|
|
1296
|
+
})
|
|
1297
|
+
.willRespondWith(201)
|
|
1298
|
+
.withResponseBody({
|
|
1299
|
+
id: expect.any(String),
|
|
1300
|
+
email: 'test@example.com',
|
|
1301
|
+
name: 'Test User',
|
|
1302
|
+
createdAt: expect.any(String),
|
|
1303
|
+
})
|
|
1304
|
+
.given('get user by id')
|
|
1305
|
+
.uponReceiving('GET', '/api/users/123')
|
|
1306
|
+
.willRespondWith(200)
|
|
1307
|
+
.withResponseBody({
|
|
1308
|
+
id: '123',
|
|
1309
|
+
email: 'test@example.com',
|
|
1310
|
+
name: 'Test User',
|
|
1311
|
+
})
|
|
1312
|
+
.build();
|
|
1313
|
+
|
|
1314
|
+
expect(pact.consumer).toBe('web-app');
|
|
1315
|
+
expect(pact.provider).toBe('user-service');
|
|
1316
|
+
expect(pact.interactions).toHaveLength(2);
|
|
1317
|
+
});
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
describe('Schema Validation', () => {
|
|
1321
|
+
it('should validate user response schema', () => {
|
|
1322
|
+
const validator = new ContractValidator();
|
|
1323
|
+
|
|
1324
|
+
validator.registerContract({
|
|
1325
|
+
name: 'user-api',
|
|
1326
|
+
version: '1.0.0',
|
|
1327
|
+
provider: 'user-service',
|
|
1328
|
+
endpoints: [
|
|
1329
|
+
{
|
|
1330
|
+
method: 'GET',
|
|
1331
|
+
path: '/api/users/:id',
|
|
1332
|
+
response: {
|
|
1333
|
+
statusCode: 200,
|
|
1334
|
+
body: {
|
|
1335
|
+
type: 'object',
|
|
1336
|
+
properties: {
|
|
1337
|
+
id: { type: 'string' },
|
|
1338
|
+
email: { type: 'string', format: 'email' },
|
|
1339
|
+
name: { type: 'string', minLength: 1 },
|
|
1340
|
+
},
|
|
1341
|
+
required: ['id', 'email', 'name'],
|
|
1342
|
+
},
|
|
1343
|
+
},
|
|
1344
|
+
},
|
|
1345
|
+
],
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
const validResult = validator.validateResponse('GET', '/api/users/:id', 200, {
|
|
1349
|
+
id: '123',
|
|
1350
|
+
email: 'test@example.com',
|
|
1351
|
+
name: 'Test User',
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
expect(validResult.valid).toBe(true);
|
|
1355
|
+
|
|
1356
|
+
const invalidResult = validator.validateResponse('GET', '/api/users/:id', 200, {
|
|
1357
|
+
id: '123',
|
|
1358
|
+
email: 'invalid-email',
|
|
1359
|
+
name: '',
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
expect(invalidResult.valid).toBe(false);
|
|
1363
|
+
});
|
|
1364
|
+
});
|
|
1365
|
+
});
|
|
1366
|
+
`;
|
|
1367
|
+
}
|
|
1368
|
+
//# sourceMappingURL=api-contracts.js.map
|