swagger-tools/scripts/self-testing/openapi-31-test.ts
rimskij 58c91615b9 feat: add OpenAPI 3.1 TypeScript generation support
- Handle type arrays: type: ['string', 'null'] → string | null
- Handle const keyword: const: "active" → 'active' literal type
- Handle nullable (OpenAPI 3.0 backward compatibility)
- Extract and display webhook count in metadata
- Add security escaping for string literals and JSDoc comments
- Add OpenAPI 3.1 test fixture and 12 unit tests

Fixes #365

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 21:20:13 +01:00

237 lines
8.2 KiB
TypeScript

/**
* OpenAPI 3.1 Support Tests
*
* Tests for OpenAPI 3.1 specific features:
* - Type as array: type: ['string', 'null']
* - Const keyword: const: "value"
* - Webhook metadata extraction
* - Backward compatibility with OpenAPI 3.0 nullable
*/
import { parseSpec } from '../../src/lib/parser.js';
import { generateToolHandler } from '../../src/tools/generate.js';
import * as path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
interface TestResult {
name: string;
passed: boolean;
error?: string;
}
const results: TestResult[] = [];
function test(name: string, fn: () => Promise<void> | void): void {
const wrapped = async () => {
try {
await fn();
results.push({ name, passed: true });
console.log(`${name}`);
} catch (err) {
results.push({ name, passed: false, error: (err as Error).message });
console.log(`${name}`);
console.log(` Error: ${(err as Error).message}`);
}
};
wrapped();
}
function assertEqual<T>(actual: T, expected: T, message?: string): void {
if (actual !== expected) {
throw new Error(message || `Expected ${expected}, got ${actual}`);
}
}
function assertContains(str: string, substring: string, message?: string): void {
if (!str.includes(substring)) {
throw new Error(message || `Expected string to contain "${substring}"\nActual: ${str}`);
}
}
function assertNotContains(str: string, substring: string, message?: string): void {
if (str.includes(substring)) {
throw new Error(message || `Expected string NOT to contain "${substring}"\nActual: ${str}`);
}
}
async function runTests() {
console.log('\n=== OpenAPI 3.1 Support Tests ===\n');
const fixturePath = path.join(__dirname, '../../test/fixtures/openapi-31.yaml');
// ==========================================
// Parsing and Metadata Tests
// ==========================================
console.log('Parsing and Metadata:');
test('Parse OpenAPI 3.1 spec successfully', async () => {
const result = await parseSpec(fixturePath, { noCache: true });
assertEqual(result.metadata.version, 'OpenAPI 3.1.0');
assertEqual(result.metadata.title, 'OpenAPI 3.1 Test Spec');
});
test('Extract webhook count from 3.1 spec', async () => {
const result = await parseSpec(fixturePath, { noCache: true });
assertEqual(result.metadata.webhookCount, 2, 'Should have 2 webhooks');
});
test('3.0 spec has undefined webhookCount', async () => {
// Use petstore as a 3.0 spec (or any 3.0/2.0 spec)
const result = await parseSpec('https://petstore.swagger.io/v2/swagger.json', { noCache: true });
assertEqual(result.metadata.webhookCount, undefined, 'Swagger 2.0 should have undefined webhookCount');
});
// ==========================================
// Type Generation Tests - Array Types
// ==========================================
console.log('\nType Generation - Array Types:');
test('Generate type for type: [string, null]', async () => {
const result = await generateToolHandler({
path: fixturePath,
schemas: ['NullableString'],
noCache: true,
});
const content = result.content[0];
if (content.type !== 'text') throw new Error('Expected text content');
assertContains(content.text, 'string | null', 'Should generate string | null');
});
test('Generate type for type: [string, number]', async () => {
const result = await generateToolHandler({
path: fixturePath,
schemas: ['StringOrNumber'],
noCache: true,
});
const content = result.content[0];
if (content.type !== 'text') throw new Error('Expected text content');
assertContains(content.text, 'string | number', 'Should generate string | number');
});
test('Generate type for type: [integer, null]', async () => {
const result = await generateToolHandler({
path: fixturePath,
schemas: ['NullableInteger'],
noCache: true,
});
const content = result.content[0];
if (content.type !== 'text') throw new Error('Expected text content');
assertContains(content.text, 'number | null', 'Should generate number | null');
});
// ==========================================
// Type Generation Tests - Const Keyword
// ==========================================
console.log('\nType Generation - Const Keyword:');
test('Generate literal type for const: "active"', async () => {
const result = await generateToolHandler({
path: fixturePath,
schemas: ['StatusActive'],
noCache: true,
});
const content = result.content[0];
if (content.type !== 'text') throw new Error('Expected text content');
assertContains(content.text, "'active'", 'Should generate literal type');
});
test('Generate literal type for const: 200', async () => {
const result = await generateToolHandler({
path: fixturePath,
schemas: ['StatusCode'],
noCache: true,
});
const content = result.content[0];
if (content.type !== 'text') throw new Error('Expected text content');
assertContains(content.text, '200', 'Should generate number literal');
});
// ==========================================
// Type Generation Tests - Object with 3.1 Features
// ==========================================
console.log('\nType Generation - Objects with 3.1 Features:');
test('Generate User interface with nullable properties', async () => {
const result = await generateToolHandler({
path: fixturePath,
schemas: ['User'],
noCache: true,
});
const content = result.content[0];
if (content.type !== 'text') throw new Error('Expected text content');
// Check that name can be string | null
assertContains(content.text, 'name?:', 'Should have name property');
// Check const status property
assertContains(content.text, 'status:', 'Should have status property');
});
// ==========================================
// Backward Compatibility Tests
// ==========================================
console.log('\nBackward Compatibility:');
test('Generate type with OpenAPI 3.0 nullable: true', async () => {
const result = await generateToolHandler({
path: fixturePath,
schemas: ['LegacyUser'],
noCache: true,
});
const content = result.content[0];
if (content.type !== 'text') throw new Error('Expected text content');
assertContains(content.text, 'string | null', 'Should handle 3.0 nullable');
});
test('Parse Swagger 2.0 spec (petstore) still works', async () => {
const result = await parseSpec('https://petstore.swagger.io/v2/swagger.json', { noCache: true });
assertEqual(result.metadata.version.includes('Swagger'), true, 'Should parse as Swagger');
assertEqual(result.metadata.pathCount > 0, true, 'Should have paths');
});
// ==========================================
// Full Generation Test
// ==========================================
console.log('\nFull Generation:');
test('Generate all schemas from 3.1 spec', async () => {
const result = await generateToolHandler({
path: fixturePath,
noCache: true,
});
const content = result.content[0];
if (content.type !== 'text') throw new Error('Expected text content');
// Verify multiple schemas generated
assertContains(content.text, 'NullableString', 'Should have NullableString');
assertContains(content.text, 'User', 'Should have User');
assertContains(content.text, 'StatusActive', 'Should have StatusActive');
assertContains(content.text, 'LegacyUser', 'Should have LegacyUser');
});
// Wait for all async tests to complete
await new Promise(resolve => setTimeout(resolve, 3000));
// Print summary
console.log('\n=== Test Summary ===');
const passed = results.filter(r => r.passed).length;
const failed = results.filter(r => !r.passed).length;
console.log(`Passed: ${passed}`);
console.log(`Failed: ${failed}`);
console.log(`Total: ${results.length}`);
if (failed > 0) {
console.log('\nFailed tests:');
results.filter(r => !r.passed).forEach(r => {
console.log(` - ${r.name}: ${r.error}`);
});
process.exit(1);
}
console.log('\n✓ All tests passed!\n');
}
runTests().catch(err => {
console.error('Test suite failed:', err);
process.exit(1);
});