- 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>
237 lines
8.2 KiB
TypeScript
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);
|
|
});
|