apx-toolkit
Version:
Automatically discover APIs and generate complete integration packages: code in 12 languages, TypeScript types, test suites, SDK packages, API documentation, mock servers, performance reports, and contract tests. Saves 2-4 weeks of work in seconds.
239 lines (209 loc) • 6.31 kB
text/typescript
/**
* API Contract Testing Generator
*
* Generates contract tests to prevent breaking changes
*/
import type { DiscoveredAPI } from '../types.js';
import * as fs from 'fs/promises';
import * as path from 'path';
export interface ContractDefinition {
endpoint: string;
method: string;
requestSchema: any;
responseSchema: any;
examples: {
request: any;
response: any;
}[];
}
export interface ContractTestOutput {
pactContracts: string;
schemathesisTests: string;
contractJson: string;
ciConfig: string;
}
/**
* Generate contract definitions from discovered APIs
*/
export function generateContractDefinitions(apis: DiscoveredAPI[]): ContractDefinition[] {
return apis.map(api => ({
endpoint: api.url,
method: api.method,
requestSchema: inferSchema(api.requestExample),
responseSchema: inferSchema(api.responseExample || api.data),
examples: [
{
request: api.requestExample || {},
response: api.responseExample || api.data || {},
},
],
}));
}
/**
* Generate Pact contract files
*/
export function generatePactContracts(contracts: ContractDefinition[]): string {
return `// Auto-generated Pact contracts by APX Toolkit
const { Pact } = require('@pact-foundation/pact');
const path = require('path');
const provider = new Pact({
consumer: 'APX-Generated-Client',
provider: 'API-Provider',
log: path.resolve(process.cwd(), 'logs', 'pact.log'),
dir: path.resolve(process.cwd(), 'pacts'),
logLevel: 'INFO',
});
describe('API Contract Tests', () => {
${contracts.map((contract, index) => `
describe('${contract.method} ${contract.endpoint}', () => {
it('should match the contract', async () => {
await provider.addInteraction({
state: 'default',
uponReceiving: 'a request for ${contract.endpoint}',
withRequest: {
method: '${contract.method}',
path: '${new URL(contract.endpoint).pathname}',
headers: {
'Content-Type': 'application/json',
},
},
willRespondWith: {
status: 200,
headers: {
'Content-Type': 'application/json',
},
body: ${JSON.stringify(contract.examples[0].response, null, 12)},
},
});
// Verify contract
await provider.verify();
});
});`).join('\n')}
});
`;
}
/**
* Generate Schemathesis tests
*/
export function generateSchemathesisTests(contracts: ContractDefinition[]): string {
return `
import schemathesis
from schemathesis import case
BASE_URL = "https://api.example.com"
${contracts.map((contract, index) => `
@schemathesis.test
def test_${index}_${contract.method.toLowerCase()}_${extractEndpointName(contract.endpoint)}(case):
"""Test contract for ${contract.method} ${contract.endpoint}"""
response = case.call(BASE_URL + "${new URL(contract.endpoint).pathname}")
assert response.status_code == 200
assert response.headers.get("Content-Type") == "application/json"
`).join('\n')}
`;
}
function extractEndpointName(url: string): string {
const urlObj = new URL(url);
const pathParts = urlObj.pathname.split('/').filter(p => p);
return pathParts[pathParts.length - 1] || 'root';
}
/**
* Infer JSON schema from data
*/
function inferSchema(data: any): any {
if (!data) return { type: 'object' };
if (Array.isArray(data)) {
return {
type: 'array',
items: data.length > 0 ? inferSchema(data[0]) : { type: 'object' },
};
}
if (typeof data === 'object') {
const properties: Record<string, any> = {};
const required: string[] = [];
for (const [key, value] of Object.entries(data)) {
properties[key] = inferSchema(value);
if (value !== null && value !== undefined) {
required.push(key);
}
}
return {
type: 'object',
properties,
required,
};
}
return { type: typeof data };
}
/**
* Generate contract test output
*/
export function generateContractTests(apis: DiscoveredAPI[]): ContractTestOutput {
const contracts = generateContractDefinitions(apis);
return {
pactContracts: generatePactContracts(contracts),
schemathesisTests: generateSchemathesisTests(contracts),
contractJson: JSON.stringify(contracts, null, 2),
ciConfig: generateCIConfig(),
};
}
/**
* Generate CI/CD configuration
*/
function generateCIConfig(): string {
return `
name: Contract Tests
on:
pull_request:
paths:
- 'contracts/**'
schedule:
- cron: '0 0 * * *'
jobs:
contract-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm install
- run: npm run test:contracts
- name: Publish Pact
uses: pact-foundation/pact-publish-action@v1
with:
pact_files: 'pacts/*.json'
pact_broker: \${{ secrets.PACT_BROKER_URL }}
pact_broker_token: \${{ secrets.PACT_BROKER_TOKEN }}
`;
}
/**
* Save contract tests
*/
export async function saveContractTests(
output: ContractTestOutput,
outputPath: string
): Promise<void> {
await fs.mkdir(outputPath, { recursive: true });
await fs.mkdir(path.join(outputPath, 'contracts'), { recursive: true });
// Save Pact contracts
await fs.writeFile(
path.join(outputPath, 'contracts', 'pact-contracts.test.js'),
output.pactContracts
);
// Save Schemathesis tests
await fs.writeFile(
path.join(outputPath, 'contracts', 'schemathesis_tests.py'),
output.schemathesisTests
);
// Save contract JSON
await fs.writeFile(
path.join(outputPath, 'contracts', 'contracts.json'),
output.contractJson
);
// Save CI config
await fs.writeFile(
path.join(outputPath, '.github', 'workflows', 'contract-tests.yml'),
output.ciConfig
);
}