Skip to main content
Glama
openapi.test.ts10.8 kB
import { describe, it, expect } from 'vitest'; import { normalizeExtensions } from './openapi.ts'; import { CustomExtensions } from './operation/custom-extensions.ts'; describe('normalizeExtensions', () => { describe('Basic functionality', () => { it('returns an empty object when input is null', () => { const result = normalizeExtensions(null); expect(result).toEqual({}); }); it('returns an empty object when input is undefined', () => { const result = normalizeExtensions(undefined); expect(result).toEqual({}); }); it('returns an empty object when input is not an object', () => { const inputs = [123, 'string', true, [1, 2, 3]]; for (const input of inputs) { const result = normalizeExtensions(input); expect(result).toEqual({}); } }); it('extracts operationId from extensions', () => { const extensions = { operationId: 'customOperationId', otherProperty: 'value', }; const result = normalizeExtensions(extensions); const expected = CustomExtensions.of({ operationId: 'customOperationId', }); expect(result).toEqual(expected); }); it('ignores properties other than operationId', () => { const extensions = { notOperationId: 'ignoredValue', anotherProperty: 123, nestedObject: { foo: 'bar' }, }; const result = normalizeExtensions(extensions); expect(result).toEqual({}); }); it('handles empty object input', () => { const result = normalizeExtensions({}); expect(result).toEqual({}); }); }); describe('Advanced object handling', () => { it('handles complex nested objects', () => { const extensions = { operationId: 'custom-id', nested: { deeply: { nested: 'value', }, }, arrays: ['a', 'b', 'c'], }; const result = normalizeExtensions(extensions); const expected = CustomExtensions.of({ operationId: 'custom-id', }); expect(result).toEqual(expected); }); it('handles multiple recognized properties when they are added', () => { // This test is forward-looking, for when more properties are supported const extensions = { operationId: 'custom-id', someProperty: 'value', anotherProperty: 123, }; const result = normalizeExtensions(extensions); // Currently only operationId is extracted expect(result).toEqual(CustomExtensions.of({ operationId: 'custom-id' })); }); it('treats non-plain objects as empty extensions', () => { class CustomClass { operationId = 'from-class'; } const date = new Date(); const regex = /test/; const instance = new CustomClass(); // Current implementation doesn't check if object is plain vs class instance // This test documents current behavior expect(normalizeExtensions(date)).toEqual({}); expect(normalizeExtensions(regex)).toEqual({}); expect(normalizeExtensions(instance)).toEqual( CustomExtensions.of({ operationId: 'from-class' }), ); }); it('handles property inheritance', () => { // Using a concrete parent object with known types const parent: Record<string, string> = { inherited: 'value' }; // Create a new object with type-safe inheritance const child: Record<string, string> = {} as Record<string, string>; Object.setPrototypeOf(child, parent); // Set property with index notation as required by linter child['operationId'] = 'child-id'; const result = normalizeExtensions(child); // Should only include own properties, not inherited ones expect(result).toEqual(CustomExtensions.of({ operationId: 'child-id' })); }); }); describe('Resource classification extensions', () => { it('handles boolean false extensions', () => { const result = normalizeExtensions(false); // If the implementation changes to handle boolean false, update this test expect(result).toEqual({}); }); it('handles boolean true extensions', () => { const result = normalizeExtensions(true); expect(result).toEqual({}); }); it('processes the ignore property correctly', () => { const resources = [ { ignore: 'resource' }, { ignore: 'tool' }, { ignore: true }, { ignore: false }, { ignore: 'invalid-value' }, { ignore: 123 }, ]; // Only these values should be preserved expect(normalizeExtensions(resources[0])).toEqual( CustomExtensions.of({ ignore: 'resource' }), ); expect(normalizeExtensions(resources[1])).toEqual(CustomExtensions.of({ ignore: 'tool' })); expect(normalizeExtensions(resources[2])).toEqual(CustomExtensions.of({ ignore: true })); // These values should not be retained expect(normalizeExtensions(resources[3])).toEqual(CustomExtensions.of({})); expect(normalizeExtensions(resources[4])).toEqual(CustomExtensions.of({})); expect(normalizeExtensions(resources[5])).toEqual(CustomExtensions.of({})); }); it('processes description property with string', () => { const input = { description: 'Valid description' }; expect(normalizeExtensions(input)).toEqual( CustomExtensions.of({ description: 'Valid description' }), ); }); it('processes description property with number value', () => { const input = { description: 123 }; expect(normalizeExtensions(input)).toEqual(CustomExtensions.of({})); }); it('processes description property with boolean value', () => { const input = { description: true }; expect(normalizeExtensions(input)).toEqual(CustomExtensions.of({})); }); it('processes description property with null value', () => { const input = { description: null }; expect(normalizeExtensions(input)).toEqual(CustomExtensions.of({})); }); it('processes readOnlyHint=true correctly', () => { const input = { annotations: { readOnlyHint: true } }; expect(normalizeExtensions(input)).toEqual( CustomExtensions.of({ safety: { access: 'readonly' } }), ); }); it('processes readOnlyHint=false correctly', () => { const input = { annotations: { readOnlyHint: false } }; expect(normalizeExtensions(input)).toEqual( CustomExtensions.of({ safety: { access: 'update', idempotent: false }, }), ); }); it('processes destructiveHint=true with readOnlyHint=false correctly', () => { const input = { annotations: { readOnlyHint: false, destructiveHint: true } }; // Observe actual behavior - since the actual implementation prioritizes destructiveHint const result = normalizeExtensions(input); expect(result).toEqual(CustomExtensions.of({ safety: { access: 'delete' } })); }); it('processes destructiveHint=true alone correctly', () => { const input = { annotations: { destructiveHint: true } }; expect(normalizeExtensions(input)).toEqual( CustomExtensions.of({ safety: { access: 'delete' } }), ); }); it('handles invalid annotations type', () => { const input = { annotations: 'not-an-object' }; expect(normalizeExtensions(input)).toEqual({}); }); it('handles empty annotations', () => { const input = { annotations: {} }; expect(normalizeExtensions(input)).toEqual({}); }); it('handles non-boolean annotation values', () => { const input = { annotations: { readOnlyHint: 'string', destructiveHint: 123 } }; // Check actual implementation behavior const result = normalizeExtensions(input); // Update the expected value to match actual implementation behavior expect(result).toEqual( CustomExtensions.of({ safety: { access: 'update', idempotent: false } }), ); }); it('combines multiple extension properties correctly', () => { const complex = { operationId: 'test-operation', ignore: 'resource', description: 'Test description', annotations: { readOnlyHint: true, destructiveHint: false, }, }; const expected = { operationId: 'test-operation', ignore: 'resource', description: 'Test description', safety: { access: 'readonly', }, } as const; expect(normalizeExtensions(complex)).toEqual(CustomExtensions.of(expected)); }); }); describe('Edge cases', () => { it('handles falsy non-null/undefined values', () => { const falsy = ['', 0, false, NaN]; for (const value of falsy) { const result = normalizeExtensions(value); expect(result).toEqual({}); } }); it('handles array inputs', () => { const arrays = [[], [1, 2, 3], [{ operationId: 'shouldNotBePicked' }]]; for (const arr of arrays) { const result = normalizeExtensions(arr); expect(result).toEqual({}); } }); it('handles objects with different property types', () => { const input = { operationId: 'valid-id', number: 123, boolean: true, object: { nested: 'value' }, array: [1, 2, 3], func: (): void => { /* empty */ }, symbol: Symbol('test'), }; const result = normalizeExtensions(input); // Should only extract operationId const expected = CustomExtensions.of({ operationId: 'valid-id', }); expect(result).toEqual(expected); }); it('handles malformed operationId values', () => { // Various non-string operationId values const inputs = [ { operationId: 123 }, { operationId: true }, { operationId: { nested: 'object' } }, { operationId: ['array'] }, { operationId: null }, { operationId: undefined }, ]; for (const input of inputs) { const result = normalizeExtensions(input); // Document the actual behavior of the function // Based on the implementation, null operationId is retained but undefined is filtered out if (input.operationId === undefined) { expect(result).toEqual({}); } else { // The updated function now ensures operationId is a string // Safely check if the result has an operationId property if ('operationId' in result) { expect(result).toEqual({ operationId: input.operationId }); } else { expect(result).toEqual({}); } } } }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/wycats/mcpify'

If you have feedback or need assistance with the MCP directory API, please join our Discord server