/** * 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 { 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(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); });