fork of hey-api/openapi-ts because I need some additional things
at feat/use-query-options-param 311 lines 12 kB view raw
1import fs from 'node:fs'; 2import path from 'node:path'; 3import { fileURLToPath } from 'node:url'; 4 5import { $RefParser } from '..'; 6import { getSpecsPath } from './utils'; 7 8const __filename = fileURLToPath(import.meta.url); 9const __dirname = path.dirname(__filename); 10 11const getSnapshotsPath = () => path.join(__dirname, '__snapshots__'); 12const getTempSnapshotsPath = () => path.join(__dirname, '.gen', 'snapshots'); 13 14/** 15 * Helper function to compare a bundled schema with a snapshot file. 16 * Handles writing the schema to a temp file and comparing with the snapshot. 17 * 18 * @param schema - The bundled schema to compare 19 * @param snapshotName - The name of the snapshot file (e.g., 'circular-ref-with-description.json') 20 */ 21const expectBundledSchemaToMatchSnapshot = async (schema: unknown, snapshotName: string) => { 22 const outputPath = path.join(getTempSnapshotsPath(), snapshotName); 23 const snapshotPath = path.join(getSnapshotsPath(), snapshotName); 24 25 // Ensure directory exists 26 fs.mkdirSync(path.dirname(outputPath), { recursive: true }); 27 28 // Write the bundled result 29 const content = JSON.stringify(schema, null, 2); 30 fs.writeFileSync(outputPath, content); 31 32 // Compare with snapshot 33 await expect(content).toMatchFileSnapshot(snapshotPath); 34}; 35 36describe('bundle', () => { 37 it('handles circular reference with description', async () => { 38 const refParser = new $RefParser(); 39 const pathOrUrlOrSchema = path.join( 40 getSpecsPath(), 41 'json-schema-ref-parser', 42 'circular-ref-with-description.json', 43 ); 44 const schema = await refParser.bundle({ pathOrUrlOrSchema }); 45 46 await expectBundledSchemaToMatchSnapshot(schema, 'circular-ref-with-description.json'); 47 }); 48 49 it('bundles multiple references to the same file correctly', async () => { 50 const refParser = new $RefParser(); 51 const pathOrUrlOrSchema = path.join( 52 getSpecsPath(), 53 'json-schema-ref-parser', 54 'multiple-refs.json', 55 ); 56 const schema = await refParser.bundle({ pathOrUrlOrSchema }); 57 58 await expectBundledSchemaToMatchSnapshot(schema, 'multiple-refs.json'); 59 }); 60 61 it('hoists sibling schemas from external files', async () => { 62 const refParser = new $RefParser(); 63 const pathOrUrlOrSchema = path.join( 64 getSpecsPath(), 65 'json-schema-ref-parser', 66 'main-with-external-siblings.json', 67 ); 68 const schema = await refParser.bundle({ pathOrUrlOrSchema }); 69 70 await expectBundledSchemaToMatchSnapshot(schema, 'main-with-external-siblings.json'); 71 }); 72 73 it('hoists sibling schemas from YAML files with versioned names (Redfish-like)', async () => { 74 const refParser = new $RefParser(); 75 const pathOrUrlOrSchema = path.join( 76 getSpecsPath(), 77 'json-schema-ref-parser', 78 'redfish-like.yaml', 79 ); 80 const schema = await refParser.bundle({ pathOrUrlOrSchema }); 81 82 await expectBundledSchemaToMatchSnapshot(schema, 'redfish-like.json'); 83 }); 84 85 describe('sibling schema resolution', () => { 86 const specsDir = path.join(getSpecsPath(), 'json-schema-ref-parser'); 87 88 const findSchemaByValue = ( 89 schemas: Record<string, any>, 90 predicate: (value: any) => boolean, 91 ): [string, any] | undefined => { 92 for (const [name, value] of Object.entries(schemas)) { 93 if (predicate(value)) { 94 return [name, value]; 95 } 96 } 97 return undefined; 98 }; 99 100 it('hoists sibling schemas through a bare $ref wrapper chain', async () => { 101 const refParser = new $RefParser(); 102 const pathOrUrlOrSchema = path.join(specsDir, 'sibling-schema-root.json'); 103 const schema = (await refParser.bundle({ pathOrUrlOrSchema })) as any; 104 105 expect(schema.components).toBeDefined(); 106 expect(schema.components.schemas).toBeDefined(); 107 108 const schemas = schema.components.schemas; 109 110 const mainSchema = findSchemaByValue( 111 schemas, 112 (v) => v.type === 'object' && v.properties?.name, 113 ); 114 expect(mainSchema).toBeDefined(); 115 const [mainName, mainValue] = mainSchema!; 116 expect(mainValue.type).toBe('object'); 117 expect(mainValue.properties.name).toEqual({ type: 'string' }); 118 119 const enumSchema = findSchemaByValue( 120 schemas, 121 (v) => Array.isArray(v.enum) && v.enum.includes('active'), 122 ); 123 expect(enumSchema).toBeDefined(); 124 const [enumName, enumValue] = enumSchema!; 125 expect(enumValue.type).toBe('string'); 126 expect(enumValue.enum).toEqual(['active', 'inactive', 'pending']); 127 128 // The main schema's status property should reference the hoisted enum 129 expect(mainValue.properties.status.$ref).toBe(`#/components/schemas/${enumName}`); 130 131 // The root path's schema ref should point to the hoisted main schema 132 const rootRef = schema.paths['/test'].get.responses['200'].content['application/json'].schema; 133 expect(rootRef.$ref).toBe(`#/components/schemas/${mainName}`); 134 }); 135 136 it('hoists sibling schemas through an extended $ref wrapper chain', async () => { 137 const refParser = new $RefParser(); 138 const pathOrUrlOrSchema = path.join(specsDir, 'sibling-schema-extended-root.json'); 139 const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); 140 141 try { 142 const schema = (await refParser.bundle({ pathOrUrlOrSchema })) as any; 143 144 expect(schema.components).toBeDefined(); 145 expect(schema.components.schemas).toBeDefined(); 146 147 const schemas = schema.components.schemas; 148 149 // The main schema should be hoisted (with the extra description merged in) 150 const mainSchema = findSchemaByValue( 151 schemas, 152 (v) => 153 v.description === 'Wrapper that extends the versioned schema' || 154 (v.type === 'object' && v.properties?.name), 155 ); 156 expect(mainSchema).toBeDefined(); 157 158 // The sibling enum must also be hoisted (this was the bug — it was lost before the fix) 159 const enumSchema = findSchemaByValue( 160 schemas, 161 (v) => Array.isArray(v.enum) && v.enum.includes('active'), 162 ); 163 expect(enumSchema).toBeDefined(); 164 const [, enumValue] = enumSchema!; 165 expect(enumValue.type).toBe('string'); 166 expect(enumValue.enum).toEqual(['active', 'inactive', 'pending']); 167 168 // No "Skipping unresolvable $ref" warnings should have been emitted 169 const unresolvableWarnings = warnSpy.mock.calls.filter( 170 (args) => typeof args[0] === 'string' && args[0].includes('Skipping unresolvable $ref'), 171 ); 172 expect(unresolvableWarnings).toHaveLength(0); 173 } finally { 174 warnSpy.mockRestore(); 175 } 176 }); 177 178 it('hoists sibling schemas from a direct reference (no wrapper)', async () => { 179 const refParser = new $RefParser(); 180 const pathOrUrlOrSchema = path.join(specsDir, 'sibling-schema-direct-root.json'); 181 const schema = (await refParser.bundle({ pathOrUrlOrSchema })) as any; 182 183 expect(schema.components).toBeDefined(); 184 expect(schema.components.schemas).toBeDefined(); 185 186 const schemas = schema.components.schemas; 187 188 const mainSchema = findSchemaByValue( 189 schemas, 190 (v) => v.type === 'object' && v.properties?.name, 191 ); 192 expect(mainSchema).toBeDefined(); 193 194 const enumSchema = findSchemaByValue( 195 schemas, 196 (v) => Array.isArray(v.enum) && v.enum.includes('active'), 197 ); 198 expect(enumSchema).toBeDefined(); 199 const [enumName, enumValue] = enumSchema!; 200 expect(enumValue.enum).toEqual(['active', 'inactive', 'pending']); 201 202 const [, mainValue] = mainSchema!; 203 expect(mainValue.properties.status.$ref).toBe(`#/components/schemas/${enumName}`); 204 }); 205 206 it('hoists multiple sibling schemas through an extended wrapper', async () => { 207 const refParser = new $RefParser(); 208 const pathOrUrlOrSchema = path.join(specsDir, 'sibling-schema-multi-root.json'); 209 const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); 210 211 try { 212 const schema = (await refParser.bundle({ pathOrUrlOrSchema })) as any; 213 214 expect(schema.components).toBeDefined(); 215 expect(schema.components.schemas).toBeDefined(); 216 217 const schemas = schema.components.schemas; 218 219 const mainSchema = findSchemaByValue( 220 schemas, 221 (v) => v.type === 'object' && v.properties?.health, 222 ); 223 expect(mainSchema).toBeDefined(); 224 225 const statusEnum = findSchemaByValue( 226 schemas, 227 (v) => Array.isArray(v.enum) && v.enum.includes('enabled'), 228 ); 229 expect(statusEnum).toBeDefined(); 230 expect(statusEnum![1].enum).toEqual(['enabled', 'disabled', 'standby']); 231 232 const healthEnum = findSchemaByValue( 233 schemas, 234 (v) => Array.isArray(v.enum) && v.enum.includes('ok'), 235 ); 236 expect(healthEnum).toBeDefined(); 237 expect(healthEnum![1].enum).toEqual(['ok', 'warning', 'critical']); 238 239 const [, mainValue] = mainSchema!; 240 expect(mainValue.properties.status.$ref).toBe(`#/components/schemas/${statusEnum![0]}`); 241 expect(mainValue.properties.health.$ref).toBe(`#/components/schemas/${healthEnum![0]}`); 242 243 const unresolvableWarnings = warnSpy.mock.calls.filter( 244 (args) => typeof args[0] === 'string' && args[0].includes('Skipping unresolvable $ref'), 245 ); 246 expect(unresolvableWarnings).toHaveLength(0); 247 } finally { 248 warnSpy.mockRestore(); 249 } 250 }); 251 252 it('handles multiple external files with same-named sibling schemas', async () => { 253 const refParser = new $RefParser(); 254 const pathOrUrlOrSchema = path.join(specsDir, 'sibling-schema-collision-root.json'); 255 const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); 256 257 try { 258 const schema = (await refParser.bundle({ pathOrUrlOrSchema })) as any; 259 260 expect(schema.components).toBeDefined(); 261 expect(schema.components.schemas).toBeDefined(); 262 263 const schemas = schema.components.schemas; 264 const schemaNames = Object.keys(schemas); 265 266 const mainSchemaKey = schemaNames.find((name) => name.includes('MainSchema')); 267 const otherSchemaKey = schemaNames.find((name) => name.includes('OtherSchema')); 268 269 expect(mainSchemaKey).toBeDefined(); 270 expect(otherSchemaKey).toBeDefined(); 271 272 const statusSchemas = schemaNames.filter((name) => name.includes('Status')); 273 expect(statusSchemas.length).toBeGreaterThanOrEqual(2); 274 275 const statusValues = statusSchemas.map((name) => schemas[name]); 276 const stringStatus = statusValues.find((v: any) => v.type === 'string'); 277 const integerStatus = statusValues.find((v: any) => v.type === 'integer'); 278 279 expect(stringStatus).toBeDefined(); 280 expect(integerStatus).toBeDefined(); 281 expect(stringStatus!.enum).toEqual(['active', 'inactive']); 282 expect(integerStatus!.enum).toEqual([0, 1, 2]); 283 284 const mainSchemaValue = schemas[mainSchemaKey!]; 285 const mainStatusRef = mainSchemaValue.properties.status.$ref; 286 expect(mainStatusRef).toMatch(/^#\/components\/schemas\/.*Status/); 287 288 const referencedStatus = schemas[mainStatusRef.replace('#/components/schemas/', '')]; 289 expect(referencedStatus).toBeDefined(); 290 expect(referencedStatus.type).toBe('string'); 291 expect(referencedStatus.enum).toEqual(['active', 'inactive']); 292 293 const otherSchemaValue = schemas[otherSchemaKey!]; 294 const otherStatusRef = otherSchemaValue.properties.code.$ref; 295 expect(otherStatusRef).toMatch(/^#\/components\/schemas\/.*Status/); 296 297 const referencedOtherStatus = schemas[otherStatusRef.replace('#/components/schemas/', '')]; 298 expect(referencedOtherStatus).toBeDefined(); 299 expect(referencedOtherStatus.type).toBe('integer'); 300 expect(referencedOtherStatus.enum).toEqual([0, 1, 2]); 301 302 const unresolvableWarnings = warnSpy.mock.calls.filter( 303 (args) => typeof args[0] === 'string' && args[0].includes('Skipping unresolvable $ref'), 304 ); 305 expect(unresolvableWarnings).toHaveLength(0); 306 } finally { 307 warnSpy.mockRestore(); 308 } 309 }); 310 }); 311});