UNPKG

@furystack/rest

Version:
610 lines 26.8 kB
import { describe, expect, it } from 'vitest'; import { convertOpenApiPathToFuryStack, openApiToSchema } from './openapi-to-schema.js'; describe('convertOpenApiPathToFuryStack', () => { it('Should convert single {param} to :param', () => { expect(convertOpenApiPathToFuryStack('/users/{id}')).toBe('/users/:id'); }); it('Should convert multiple params', () => { expect(convertOpenApiPathToFuryStack('/users/{userId}/posts/{postId}')).toBe('/users/:userId/posts/:postId'); }); it('Should pass through paths without params', () => { expect(convertOpenApiPathToFuryStack('/users')).toBe('/users'); }); it('Should handle root path', () => { expect(convertOpenApiPathToFuryStack('/')).toBe('/'); }); it('Should handle param at the start', () => { expect(convertOpenApiPathToFuryStack('/{version}')).toBe('/:version'); }); }); describe('openApiToSchema', () => { describe('Document info', () => { it('Should extract title, version, and description', () => { const doc = { openapi: '3.1.0', info: { title: 'My API', version: '2.0.0', description: 'A test API' }, }; const schema = openApiToSchema(doc); expect(schema.name).toBe('My API'); expect(schema.version).toBe('2.0.0'); expect(schema.description).toBe('A test API'); }); it('Should default description to empty string when absent', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, }; const schema = openApiToSchema(doc); expect(schema.description).toBe(''); }); }); describe('HTTP methods', () => { it('Should extract GET endpoints', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/items': { get: { responses: { '200': { description: 'OK' } } } } }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.GET).toBeDefined(); expect(schema.endpoints.GET['/items']).toBeDefined(); }); it('Should extract POST endpoints', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/items': { post: { responses: { '201': { description: 'Created' } } } } }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.POST).toBeDefined(); expect(schema.endpoints.POST['/items']).toBeDefined(); }); it('Should extract PUT endpoints', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/items/{id}': { put: { responses: { '200': { description: 'OK' } } } } }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.PUT).toBeDefined(); expect(schema.endpoints.PUT['/items/:id']).toBeDefined(); }); it('Should extract DELETE endpoints', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/items/{id}': { delete: { responses: { '200': { description: 'OK' } } } } }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.DELETE).toBeDefined(); expect(schema.endpoints.DELETE['/items/:id']).toBeDefined(); }); it('Should extract PATCH endpoints', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/items/{id}': { patch: { responses: { '200': { description: 'OK' } } } } }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.PATCH).toBeDefined(); expect(schema.endpoints.PATCH['/items/:id']).toBeDefined(); }); it('Should extract HEAD endpoints', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/items': { head: { responses: { '200': { description: 'OK' } } } } }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.HEAD).toBeDefined(); expect(schema.endpoints.HEAD['/items']).toBeDefined(); }); it('Should extract OPTIONS endpoints', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/items': { options: { responses: { '200': { description: 'OK' } } } } }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.OPTIONS).toBeDefined(); expect(schema.endpoints.OPTIONS['/items']).toBeDefined(); }); it('Should extract TRACE endpoints', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/items': { trace: { responses: { '200': { description: 'OK' } } } } }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.TRACE).toBeDefined(); expect(schema.endpoints.TRACE['/items']).toBeDefined(); }); it('Should handle multiple HTTP methods on the same path', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/items': { get: { responses: { '200': { description: 'OK' } } }, post: { responses: { '201': { description: 'Created' } } }, delete: { responses: { '200': { description: 'Deleted' } } }, }, }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.GET['/items']).toBeDefined(); expect(schema.endpoints.POST['/items']).toBeDefined(); expect(schema.endpoints.DELETE['/items']).toBeDefined(); }); }); describe('Path parameters', () => { it('Should convert single {param} to :param', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/users/{id}': { get: { responses: { '200': { description: 'OK' } } } }, }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.GET['/users/:id']).toBeDefined(); expect(schema.endpoints.GET['/users/:id'].path).toBe('/users/:id'); }); it('Should convert multiple {params} to :params', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/users/{userId}/posts/{postId}': { get: { responses: { '200': { description: 'OK' } } } }, }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.GET['/users/:userId/posts/:postId']).toBeDefined(); expect(schema.endpoints.GET['/users/:userId/posts/:postId'].path).toBe('/users/:userId/posts/:postId'); }); }); describe('Response schema extraction', () => { it('Should extract schema from 200 response', () => { const responseSchema = { type: 'object', properties: { id: { type: 'string' } } }; const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/items': { get: { responses: { '200': { description: 'OK', content: { 'application/json': { schema: responseSchema } } }, }, }, }, }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.GET['/items'].schema).toEqual(responseSchema); }); it('Should fall back to 201 response schema', () => { const responseSchema = { type: 'object', properties: { id: { type: 'string' } } }; const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/items': { post: { responses: { '201': { description: 'Created', content: { 'application/json': { schema: responseSchema } } }, }, }, }, }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.POST['/items'].schema).toEqual(responseSchema); }); it('Should return empty object for responses without content', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/health': { get: { responses: { '200': { description: 'OK' } } } }, }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.GET['/health'].schema).toEqual({}); }); it('Should skip $ref response objects', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/items': { get: { responses: { '200': { $ref: '#/components/responses/Success' }, }, }, }, }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.GET['/items'].schema).toEqual({}); }); }); describe('Schema naming', () => { it('Should use operationId as schemaName when available', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/users': { get: { operationId: 'listUsers', responses: { '200': { description: 'OK' } } } }, }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.GET['/users'].schemaName).toBe('listUsers'); }); it('Should generate schemaName from method and path when no operationId', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/users/{id}': { get: { responses: { '200': { description: 'OK' } } } }, }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.GET['/users/:id'].schemaName).toBe('get_users_id'); }); }); describe('Authentication detection', () => { it('Should mark as not authenticated when no security defined', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/public': { get: { responses: { '200': { description: 'OK' } } } }, }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.GET['/public'].isAuthenticated).toBe(false); }); it('Should mark as not authenticated with empty operation security', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/public': { get: { security: [], responses: { '200': { description: 'OK' } } } }, }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.GET['/public'].isAuthenticated).toBe(false); }); it('Should mark as authenticated from operation-level security', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/private': { get: { security: [{ bearerAuth: [] }], responses: { '200': { description: 'OK' } } }, }, }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.GET['/private'].isAuthenticated).toBe(true); }); it('Should mark as authenticated from document-level security', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, security: [{ apiKey: [] }], paths: { '/items': { get: { responses: { '200': { description: 'OK' } } } }, }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.GET['/items'].isAuthenticated).toBe(true); }); it('Should prefer operation-level security over document-level', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, security: [{ apiKey: [] }], paths: { '/public': { get: { security: [], responses: { '200': { description: 'OK' } } }, }, }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.GET['/public'].isAuthenticated).toBe(false); }); }); describe('Security scheme name extraction', () => { it('Should extract operation-level security scheme names', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/private': { get: { security: [{ bearerAuth: [] }], responses: { '200': { description: 'OK' } } }, }, }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.GET['/private'].securitySchemes).toEqual(['bearerAuth']); }); it('Should extract multiple security scheme names', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/private': { get: { security: [{ bearerAuth: [] }, { apiKey: [] }], responses: { '200': { description: 'OK' } }, }, }, }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.GET['/private'].securitySchemes).toEqual(['bearerAuth', 'apiKey']); }); it('Should inherit document-level security scheme names', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, security: [{ apiKey: [] }], paths: { '/items': { get: { responses: { '200': { description: 'OK' } } } }, }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.GET['/items'].securitySchemes).toEqual(['apiKey']); }); it('Should not set securitySchemes when security is empty', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/public': { get: { security: [], responses: { '200': { description: 'OK' } } } }, }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.GET['/public'].securitySchemes).toBeUndefined(); }); it('Should not set securitySchemes when no security defined', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/public': { get: { responses: { '200': { description: 'OK' } } } }, }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.GET['/public'].securitySchemes).toBeUndefined(); }); it('Should prefer operation-level scheme names over document-level', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, security: [{ apiKey: [] }], paths: { '/custom': { get: { security: [{ bearerAuth: [] }], responses: { '200': { description: 'OK' } } }, }, }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.GET['/custom'].securitySchemes).toEqual(['bearerAuth']); }); }); describe('Edge cases', () => { it('Should handle documents with no paths', () => { const doc = { openapi: '3.1.0', info: { title: 'Empty', version: '0.0.1' }, }; const schema = openApiToSchema(doc); expect(schema.endpoints).toEqual({}); }); it('Should skip $ref path items', () => { const paths = { '/ref-path': { $ref: '#/components/pathItems/SomePath' }, '/real-path': { get: { responses: { '200': { description: 'OK' } } } }, }; const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: paths, }; const schema = openApiToSchema(doc); expect(schema.endpoints.GET['/real-path']).toBeDefined(); expect(schema.endpoints.GET['/ref-path']).toBeUndefined(); }); it('Should handle path items with summary/description but no operations', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/no-ops': { summary: 'A path with no operations' }, }, }; const schema = openApiToSchema(doc); expect(Object.keys(schema.endpoints)).toEqual([]); }); }); describe('Document-level metadata extraction', () => { it('Should extract all info metadata fields', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0', summary: 'A test API', termsOfService: 'https://example.com/terms', contact: { name: 'Support', email: 'support@example.com' }, license: { name: 'MIT' }, }, }; const schema = openApiToSchema(doc); expect(schema.metadata?.summary).toBe('A test API'); expect(schema.metadata?.termsOfService).toBe('https://example.com/terms'); expect(schema.metadata?.contact).toEqual({ name: 'Support', email: 'support@example.com' }); expect(schema.metadata?.license).toEqual({ name: 'MIT' }); }); it('Should extract servers with variables', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, servers: [ { url: 'https://{env}.example.com', variables: { env: { default: 'prod', enum: ['prod', 'staging'] } }, }, ], }; const schema = openApiToSchema(doc); expect(schema.metadata?.servers).toHaveLength(1); expect(schema.metadata?.servers?.[0].url).toBe('https://{env}.example.com'); expect(schema.metadata?.servers?.[0].variables?.env.default).toBe('prod'); }); it('Should extract tags', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, tags: [ { name: 'pets', description: 'Pet operations' }, { name: 'store', externalDocs: { url: 'https://example.com/docs' } }, ], }; const schema = openApiToSchema(doc); expect(schema.metadata?.tags).toHaveLength(2); expect(schema.metadata?.tags?.[0].name).toBe('pets'); }); it('Should extract externalDocs', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, externalDocs: { url: 'https://example.com/docs', description: 'Full docs' }, }; const schema = openApiToSchema(doc); expect(schema.metadata?.externalDocs).toEqual({ url: 'https://example.com/docs', description: 'Full docs' }); }); it('Should extract security schemes', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, components: { securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, apiKey: { type: 'apiKey', in: 'header', name: 'X-API-Key' }, }, }, }; const schema = openApiToSchema(doc); expect(schema.metadata?.securitySchemes?.bearerAuth).toEqual({ type: 'http', scheme: 'bearer', bearerFormat: 'JWT', }); expect(schema.metadata?.securitySchemes?.apiKey).toBeDefined(); }); it('Should skip $ref security schemes', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, components: { securitySchemes: { external: { $ref: '#/components/securitySchemes/Other' }, local: { type: 'apiKey', in: 'header', name: 'X-Key' }, }, }, }; const schema = openApiToSchema(doc); expect(schema.metadata?.securitySchemes?.local).toBeDefined(); expect(schema.metadata?.securitySchemes?.external).toBeUndefined(); }); it('Should not set securitySchemes when all are $ref', () => { const schemes = { external: { $ref: '#/other' }, }; const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, components: { securitySchemes: schemes, }, }; const schema = openApiToSchema(doc); expect(schema.metadata?.securitySchemes).toBeUndefined(); }); it('Should return undefined metadata when no metadata present', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, }; const schema = openApiToSchema(doc); expect(schema.metadata).toBeUndefined(); }); }); describe('Operation-level metadata extraction', () => { it('Should extract tags from operations', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/pets': { get: { tags: ['pets'], responses: { '200': { description: 'OK' } } }, }, }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.GET['/pets'].tags).toEqual(['pets']); }); it('Should extract deprecated flag from operations', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/old': { get: { deprecated: true, responses: { '200': { description: 'OK' } } }, }, }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.GET['/old'].deprecated).toBe(true); }); it('Should extract summary and description from operations', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/pets': { get: { summary: 'List pets', description: 'Returns all pets', responses: { '200': { description: 'OK' } }, }, }, }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.GET['/pets'].summary).toBe('List pets'); expect(schema.endpoints.GET['/pets'].description).toBe('Returns all pets'); }); it('Should not set metadata fields when absent', () => { const doc = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/items': { get: { responses: { '200': { description: 'OK' } } }, }, }, }; const schema = openApiToSchema(doc); expect(schema.endpoints.GET['/items'].tags).toBeUndefined(); expect(schema.endpoints.GET['/items'].deprecated).toBeUndefined(); expect(schema.endpoints.GET['/items'].summary).toBeUndefined(); expect(schema.endpoints.GET['/items'].description).toBeUndefined(); }); }); }); //# sourceMappingURL=openapi-to-schema.spec.js.map