UNPKG

@furystack/rest

Version:
325 lines (302 loc) 10.7 kB
import { describe, expect, it } from 'vitest' import type { OpenApiDocument } from './openapi-document.js' import { resolveOpenApiRefs } from './openapi-resolve-refs.js' describe('resolveOpenApiRefs', () => { describe('Schema $ref resolution', () => { it('Should resolve $ref to components/schemas', () => { const doc: OpenApiDocument = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/users': { get: { responses: { '200': { description: 'OK', content: { 'application/json': { schema: { $ref: '#/components/schemas/User' }, }, }, }, }, }, }, }, components: { schemas: { User: { type: 'object', properties: { id: { type: 'string' }, name: { type: 'string' } } }, }, }, } const resolved = resolveOpenApiRefs(doc) const { schema } = ( resolved.paths?.['/users']?.get?.responses?.['200'] as { content: Record<string, { schema: unknown }> } ).content['application/json'] expect(schema).toEqual({ type: 'object', properties: { id: { type: 'string' }, name: { type: 'string' } } }) }) it('Should resolve nested $ref chains', () => { const doc: OpenApiDocument = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/items': { get: { responses: { '200': { description: 'OK', content: { 'application/json': { schema: { type: 'object', properties: { category: { $ref: '#/components/schemas/Category' }, }, }, }, }, }, }, }, }, }, components: { schemas: { Category: { type: 'object', properties: { id: { type: 'integer' }, name: { type: 'string' } } }, }, }, } const resolved = resolveOpenApiRefs(doc) const schema = ( resolved.paths?.['/items']?.get?.responses?.['200'] as { content: Record<string, { schema: unknown }> } ).content['application/json'].schema as Record<string, unknown> const props = (schema.properties as Record<string, unknown>).category as Record<string, unknown> expect(props.type).toBe('object') expect(props.properties).toEqual({ id: { type: 'integer' }, name: { type: 'string' } }) }) }) describe('Parameter $ref resolution', () => { it('Should resolve $ref parameters', () => { const doc: OpenApiDocument = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/items': { get: { parameters: [{ $ref: '#/components/parameters/LimitParam' }], responses: { '200': { description: 'OK' } }, }, }, }, components: { parameters: { LimitParam: { name: 'limit', in: 'query', schema: { type: 'integer' } }, }, }, } const resolved = resolveOpenApiRefs(doc) const params = resolved.paths?.['/items']?.get?.parameters as Array<Record<string, unknown>> expect(params[0].name).toBe('limit') expect(params[0].in).toBe('query') }) }) describe('Response $ref resolution', () => { it('Should resolve $ref responses', () => { const doc: OpenApiDocument = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/items': { get: { responses: { '400': { $ref: '#/components/responses/BadRequest' }, }, }, }, }, components: { responses: { BadRequest: { description: 'Bad request', content: { 'application/json': { schema: { type: 'object', properties: { message: { type: 'string' } } }, }, }, }, }, }, } const resolved = resolveOpenApiRefs(doc) const resp = resolved.paths?.['/items']?.get?.responses?.['400'] as Record<string, unknown> expect(resp.description).toBe('Bad request') }) }) describe('RequestBody $ref resolution', () => { it('Should resolve $ref in requestBody schema', () => { const doc: OpenApiDocument = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/items': { post: { requestBody: { content: { 'application/json': { schema: { $ref: '#/components/schemas/Item' }, }, }, }, responses: { '201': { description: 'Created' } }, }, }, }, components: { schemas: { Item: { type: 'object', properties: { name: { type: 'string' } } }, }, }, } const resolved = resolveOpenApiRefs(doc) const body = resolved.paths?.['/items']?.post?.requestBody as Record<string, unknown> const schema = ((body.content as Record<string, unknown>)['application/json'] as Record<string, unknown>) .schema as Record<string, unknown> expect(schema.type).toBe('object') }) }) describe('Edge cases', () => { it('Should handle circular $ref by breaking the cycle with empty object', () => { const doc: OpenApiDocument = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, components: { schemas: { Node: { type: 'object', properties: { value: { type: 'string' }, child: { $ref: '#/components/schemas/Node' }, }, }, }, }, } const resolved = resolveOpenApiRefs(doc) const nodeSchema = resolved.components?.schemas?.Node as Record<string, unknown> expect(nodeSchema.type).toBe('object') const props = nodeSchema.properties as Record<string, Record<string, unknown>> expect(props.value).toEqual({ type: 'string' }) // The circular child ref is resolved, but the nested self-ref within it breaks the cycle expect(props.child.type).toBe('object') const childProps = props.child.properties as Record<string, Record<string, unknown>> expect(childProps.value).toEqual({ type: 'string' }) expect(childProps.child).toEqual({}) }) it('Should not modify the original document', () => { const doc: OpenApiDocument = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/items': { get: { responses: { '200': { description: 'OK', content: { 'application/json': { schema: { $ref: '#/components/schemas/Item' } } }, }, }, }, }, }, components: { schemas: { Item: { type: 'object' } } }, } resolveOpenApiRefs(doc) const schema = doc.paths?.['/items']?.get?.responses?.['200'] as Record<string, unknown> expect((schema.content as Record<string, { schema: unknown }>)['application/json'].schema).toEqual({ $ref: '#/components/schemas/Item', }) }) it('Should leave external $ref as-is', () => { const doc: OpenApiDocument = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/items': { get: { responses: { '200': { description: 'OK', content: { 'application/json': { schema: { $ref: 'external.json#/Schema' } } }, }, }, }, }, }, } const resolved = resolveOpenApiRefs(doc) const { schema } = ( resolved.paths?.['/items']?.get?.responses?.['200'] as { content: Record<string, { schema: unknown }> } ).content['application/json'] expect(schema).toEqual({ $ref: 'external.json#/Schema' }) }) it('Should leave unresolvable $ref as-is', () => { const doc: OpenApiDocument = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/items': { get: { responses: { '200': { description: 'OK', content: { 'application/json': { schema: { $ref: '#/components/schemas/Missing' } } }, }, }, }, }, }, } const resolved = resolveOpenApiRefs(doc) const { schema } = ( resolved.paths?.['/items']?.get?.responses?.['200'] as { content: Record<string, { schema: unknown }> } ).content['application/json'] expect(schema).toEqual({ $ref: '#/components/schemas/Missing' }) }) it('Should handle documents with no $ref', () => { const doc: OpenApiDocument = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/health': { get: { responses: { '200': { description: 'OK' } } } }, }, } const resolved = resolveOpenApiRefs(doc) expect(resolved.paths?.['/health']?.get?.responses?.['200']).toEqual({ description: 'OK' }) }) it('Should resolve arrays of items with $ref', () => { const doc: OpenApiDocument = { openapi: '3.1.0', info: { title: 'Test', version: '1.0.0' }, paths: { '/items': { get: { parameters: [{ $ref: '#/components/parameters/A' }, { $ref: '#/components/parameters/B' }], responses: { '200': { description: 'OK' } }, }, }, }, components: { parameters: { A: { name: 'a', in: 'query', schema: { type: 'string' } }, B: { name: 'b', in: 'query', schema: { type: 'integer' } }, }, }, } const resolved = resolveOpenApiRefs(doc) const params = resolved.paths?.['/items']?.get?.parameters as Array<Record<string, unknown>> expect(params).toHaveLength(2) expect(params[0].name).toBe('a') expect(params[1].name).toBe('b') }) }) })