fork of hey-api/openapi-ts because I need some additional things

Convert bundle tests to use snapshot testing

Following the pattern from Zod v4 tests, convert bundle tests from value comparisons to snapshot testing for better visibility of bundled specs.

Changes:
- Added __snapshots__ directory with JSON snapshot files for each test
- Updated bundle.test.ts to use toMatchFileSnapshot matcher
- All 5 tests now write bundled output to temp directory and compare with snapshots
- Snapshots provide complete visibility of bundled spec structure
- Removed verbose assertion code, replaced with simple snapshot comparisons

Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com>

+351 -154
+1
packages/json-schema-ref-parser/.gitignore
··· 1 + .gen
+11
packages/json-schema-ref-parser/src/__tests__/__snapshots__/circular-ref-with-description.json
··· 1 + { 2 + "schemas": { 3 + "Foo": { 4 + "$ref": "#/schemas/Bar" 5 + }, 6 + "Bar": { 7 + "description": "ok", 8 + "$ref": "#/schemas/Foo" 9 + } 10 + } 11 + }
+64
packages/json-schema-ref-parser/src/__tests__/__snapshots__/cross-file-ref-main.json
··· 1 + { 2 + "openapi": "3.0.0", 3 + "info": { 4 + "title": "Cross-file Reference Test", 5 + "version": "1.0.0" 6 + }, 7 + "paths": { 8 + "/resource-a": { 9 + "get": { 10 + "responses": { 11 + "200": { 12 + "description": "Returns SchemaA", 13 + "content": { 14 + "application/json": { 15 + "schema": { 16 + "$ref": "#/components/schemas/cross-file-ref-file1_SchemaA" 17 + } 18 + } 19 + } 20 + } 21 + } 22 + } 23 + }, 24 + "/resource-b": { 25 + "get": { 26 + "responses": { 27 + "200": { 28 + "description": "Returns SchemaB", 29 + "content": { 30 + "application/json": { 31 + "schema": { 32 + "$ref": "#/components/schemas/cross-file-ref-file2_SchemaB" 33 + } 34 + } 35 + } 36 + } 37 + } 38 + } 39 + } 40 + }, 41 + "components": { 42 + "schemas": { 43 + "cross-file-ref-file1_SchemaA": { 44 + "type": "object", 45 + "properties": { 46 + "typeField": { 47 + "$ref": "#/components/schemas/cross-file-ref-file2_SchemaB" 48 + }, 49 + "name": { 50 + "type": "string" 51 + } 52 + } 53 + }, 54 + "cross-file-ref-file2_SchemaB": { 55 + "type": "string", 56 + "enum": [ 57 + "TypeA", 58 + "TypeB", 59 + "TypeC" 60 + ] 61 + } 62 + } 63 + } 64 + }
+78
packages/json-schema-ref-parser/src/__tests__/__snapshots__/main-with-external-siblings.json
··· 1 + { 2 + "openapi": "3.0.0", 3 + "info": { 4 + "title": "Test API", 5 + "version": "1.0.0" 6 + }, 7 + "paths": { 8 + "/resolution": { 9 + "get": { 10 + "summary": "Get resolution step", 11 + "responses": { 12 + "200": { 13 + "description": "Success", 14 + "content": { 15 + "application/json": { 16 + "schema": { 17 + "$ref": "#/components/schemas/external-with-siblings_ResolutionStep" 18 + } 19 + } 20 + } 21 + } 22 + } 23 + } 24 + }, 25 + "/action": { 26 + "get": { 27 + "summary": "Get action info", 28 + "responses": { 29 + "200": { 30 + "description": "Success", 31 + "content": { 32 + "application/json": { 33 + "schema": { 34 + "$ref": "#/components/schemas/external-with-siblings_ActionInfo" 35 + } 36 + } 37 + } 38 + } 39 + } 40 + } 41 + } 42 + }, 43 + "components": { 44 + "schemas": { 45 + "external-with-siblings_ActionInfo": { 46 + "type": "object", 47 + "properties": { 48 + "ActionId": { 49 + "type": "string" 50 + } 51 + } 52 + }, 53 + "external-with-siblings_ResolutionStep": { 54 + "type": "object", 55 + "properties": { 56 + "ResolutionType": { 57 + "oneOf": [ 58 + { 59 + "$ref": "#/components/schemas/external-with-siblings_ResolutionType" 60 + } 61 + ] 62 + }, 63 + "ActionName": { 64 + "type": "string" 65 + } 66 + } 67 + }, 68 + "external-with-siblings_ResolutionType": { 69 + "type": "string", 70 + "enum": [ 71 + "ContactVendor", 72 + "ResetToDefaults", 73 + "RetryOperation" 74 + ] 75 + } 76 + } 77 + } 78 + }
+48
packages/json-schema-ref-parser/src/__tests__/__snapshots__/multiple-refs.json
··· 1 + { 2 + "paths": { 3 + "/test1/{pathId}": { 4 + "get": { 5 + "summary": "First endpoint using the same pathId schema", 6 + "parameters": [ 7 + { 8 + "$ref": "#/components/parameters/path-parameter_pathId" 9 + } 10 + ], 11 + "responses": { 12 + "200": { 13 + "description": "Test 1 response" 14 + } 15 + } 16 + } 17 + }, 18 + "/test2/{pathId}": { 19 + "get": { 20 + "summary": "Second endpoint using the same pathId schema", 21 + "parameters": [ 22 + { 23 + "$ref": "#/components/parameters/path-parameter_pathId" 24 + } 25 + ], 26 + "responses": { 27 + "200": { 28 + "description": "Test 2 response" 29 + } 30 + } 31 + } 32 + } 33 + }, 34 + "components": { 35 + "parameters": { 36 + "path-parameter_pathId": { 37 + "name": "pathId", 38 + "in": "path", 39 + "required": true, 40 + "schema": { 41 + "type": "string", 42 + "format": "uuid", 43 + "description": "Unique identifier for the path" 44 + } 45 + } 46 + } 47 + } 48 + }
+87
packages/json-schema-ref-parser/src/__tests__/__snapshots__/redfish-like.json
··· 1 + { 2 + "openapi": "3.0.0", 3 + "info": { 4 + "title": "Redfish-like API", 5 + "version": "1.0.0", 6 + "description": "Test API simulating Redfish structure with versioned schemas" 7 + }, 8 + "paths": { 9 + "/redfish/v1/Systems": { 10 + "get": { 11 + "summary": "Get Systems", 12 + "responses": { 13 + "200": { 14 + "description": "Success", 15 + "content": { 16 + "application/json": { 17 + "schema": { 18 + "$ref": "#/components/schemas/ResolutionStep_v1_0_1_ResolutionStep_v1_0_1_ResolutionStep" 19 + } 20 + } 21 + } 22 + }, 23 + "default": { 24 + "description": "Error" 25 + } 26 + } 27 + } 28 + }, 29 + "/redfish/v1/Actions": { 30 + "post": { 31 + "summary": "Submit Action", 32 + "responses": { 33 + "200": { 34 + "description": "Success", 35 + "content": { 36 + "application/json": { 37 + "schema": { 38 + "$ref": "#/components/schemas/ResolutionStep_v1_0_1_ResolutionStep_v1_0_1_ActionParameters" 39 + } 40 + } 41 + } 42 + } 43 + } 44 + } 45 + } 46 + }, 47 + "components": { 48 + "schemas": { 49 + "ResolutionStep_v1_0_1_ResolutionStep_v1_0_1_ActionParameters": { 50 + "type": "object", 51 + "properties": { 52 + "ActionId": { 53 + "type": "string" 54 + }, 55 + "ActionType": { 56 + "$ref": "#/components/schemas/ResolutionStep_v1_0_1_ResolutionStep_v1_0_1_ResolutionType" 57 + } 58 + } 59 + }, 60 + "ResolutionStep_v1_0_1_ResolutionStep_v1_0_1_ResolutionStep": { 61 + "type": "object", 62 + "properties": { 63 + "ResolutionType": { 64 + "oneOf": [ 65 + { 66 + "$ref": "#/components/schemas/ResolutionStep_v1_0_1_ResolutionStep_v1_0_1_ResolutionType" 67 + } 68 + ] 69 + }, 70 + "ActionName": { 71 + "type": "string", 72 + "description": "Name of the action" 73 + } 74 + } 75 + }, 76 + "ResolutionStep_v1_0_1_ResolutionStep_v1_0_1_ResolutionType": { 77 + "type": "string", 78 + "enum": [ 79 + "ContactVendor", 80 + "ResetToDefaults", 81 + "RetryOperation" 82 + ], 83 + "description": "Types of resolution actions" 84 + } 85 + } 86 + } 87 + }
+62 -154
packages/json-schema-ref-parser/src/__tests__/bundle.test.ts
··· 1 + import fs from 'node:fs'; 1 2 import path from 'node:path'; 3 + import { fileURLToPath } from 'node:url'; 2 4 3 5 import { $RefParser } from '..'; 4 6 import { getSpecsPath } from './utils'; 5 7 8 + const __filename = fileURLToPath(import.meta.url); 9 + const __dirname = path.dirname(__filename); 10 + 11 + const getSnapshotsPath = () => path.join(__dirname, '__snapshots__'); 12 + const getTempSnapshotsPath = () => path.join(__dirname, '.gen', 'snapshots'); 13 + 6 14 describe('bundle', () => { 7 15 it('handles circular reference with description', async () => { 8 16 const refParser = new $RefParser(); ··· 12 20 'circular-ref-with-description.json', 13 21 ); 14 22 const schema = await refParser.bundle({ pathOrUrlOrSchema }); 15 - expect(schema).toEqual({ 16 - schemas: { 17 - Bar: { 18 - $ref: '#/schemas/Foo', 19 - description: 'ok', 20 - }, 21 - Foo: { 22 - $ref: '#/schemas/Bar', 23 - }, 24 - }, 25 - }); 23 + 24 + const outputPath = path.join(getTempSnapshotsPath(), 'circular-ref-with-description.json'); 25 + const snapshotPath = path.join(getSnapshotsPath(), 'circular-ref-with-description.json'); 26 + 27 + // Ensure directory exists 28 + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); 29 + 30 + // Write the bundled result 31 + const content = JSON.stringify(schema, null, 2); 32 + fs.writeFileSync(outputPath, content); 33 + 34 + // Compare with snapshot 35 + await expect(content).toMatchFileSnapshot(snapshotPath); 26 36 }); 27 37 28 38 it('bundles multiple references to the same file correctly', async () => { ··· 32 42 'json-schema-ref-parser', 33 43 'multiple-refs.json', 34 44 ); 35 - const schema = (await refParser.bundle({ pathOrUrlOrSchema })) as any; 45 + const schema = await refParser.bundle({ pathOrUrlOrSchema }); 36 46 37 - // Both parameters should now be $ref to the same internal definition 38 - const firstParam = schema.paths['/test1/{pathId}'].get.parameters[0]; 39 - const secondParam = schema.paths['/test2/{pathId}'].get.parameters[0]; 47 + const outputPath = path.join(getTempSnapshotsPath(), 'multiple-refs.json'); 48 + const snapshotPath = path.join(getSnapshotsPath(), 'multiple-refs.json'); 40 49 41 - // The $ref should match the output structure in file_context_0 42 - expect(firstParam.$ref).toBe('#/components/parameters/path-parameter_pathId'); 43 - expect(secondParam.$ref).toBe('#/components/parameters/path-parameter_pathId'); 50 + // Ensure directory exists 51 + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); 44 52 45 - // The referenced parameter should exist and match the expected structure 46 - expect(schema.components).toBeDefined(); 47 - expect(schema.components.parameters).toBeDefined(); 48 - expect(schema.components.parameters['path-parameter_pathId']).toEqual({ 49 - in: 'path', 50 - name: 'pathId', 51 - required: true, 52 - schema: { 53 - description: 'Unique identifier for the path', 54 - format: 'uuid', 55 - type: 'string', 56 - }, 57 - }); 53 + // Write the bundled result 54 + const content = JSON.stringify(schema, null, 2); 55 + fs.writeFileSync(outputPath, content); 56 + 57 + // Compare with snapshot 58 + await expect(content).toMatchFileSnapshot(snapshotPath); 58 59 }); 59 60 60 61 it('hoists sibling schemas from external files', async () => { ··· 64 65 'json-schema-ref-parser', 65 66 'main-with-external-siblings.json', 66 67 ); 67 - const schema = (await refParser.bundle({ pathOrUrlOrSchema })) as any; 68 + const schema = await refParser.bundle({ pathOrUrlOrSchema }); 68 69 69 - // Main schema should reference the hoisted schemas 70 - const resolutionStepSchema = 71 - schema.paths['/resolution'].get.responses['200'].content['application/json'].schema; 72 - expect(resolutionStepSchema.$ref).toBe( 73 - '#/components/schemas/external-with-siblings_ResolutionStep', 74 - ); 75 - 76 - const actionInfoSchema = 77 - schema.paths['/action'].get.responses['200'].content['application/json'].schema; 78 - expect(actionInfoSchema.$ref).toBe('#/components/schemas/external-with-siblings_ActionInfo'); 79 - 80 - // All schemas from the external file should be hoisted 81 - expect(schema.components).toBeDefined(); 82 - expect(schema.components.schemas).toBeDefined(); 70 + const outputPath = path.join(getTempSnapshotsPath(), 'main-with-external-siblings.json'); 71 + const snapshotPath = path.join(getSnapshotsPath(), 'main-with-external-siblings.json'); 83 72 84 - // ResolutionStep should be hoisted 85 - expect(schema.components.schemas['external-with-siblings_ResolutionStep']).toBeDefined(); 86 - expect( 87 - schema.components.schemas['external-with-siblings_ResolutionStep'].properties.ResolutionType 88 - .oneOf[0].$ref, 89 - ).toBe('#/components/schemas/external-with-siblings_ResolutionType'); 73 + // Ensure directory exists 74 + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); 90 75 91 - // ResolutionType (sibling schema) should also be hoisted 92 - expect(schema.components.schemas['external-with-siblings_ResolutionType']).toBeDefined(); 93 - expect(schema.components.schemas['external-with-siblings_ResolutionType']).toEqual({ 94 - enum: ['ContactVendor', 'ResetToDefaults', 'RetryOperation'], 95 - type: 'string', 96 - }); 76 + // Write the bundled result 77 + const content = JSON.stringify(schema, null, 2); 78 + fs.writeFileSync(outputPath, content); 97 79 98 - // ActionInfo (another sibling schema) should also be hoisted 99 - expect(schema.components.schemas['external-with-siblings_ActionInfo']).toBeDefined(); 100 - expect(schema.components.schemas['external-with-siblings_ActionInfo']).toEqual({ 101 - properties: { 102 - ActionId: { 103 - type: 'string', 104 - }, 105 - }, 106 - type: 'object', 107 - }); 80 + // Compare with snapshot 81 + await expect(content).toMatchFileSnapshot(snapshotPath); 108 82 }); 109 83 110 84 it('hoists sibling schemas from YAML files with versioned names (Redfish-like)', async () => { ··· 114 88 'json-schema-ref-parser', 115 89 'redfish-like.yaml', 116 90 ); 117 - const schema = (await refParser.bundle({ pathOrUrlOrSchema })) as any; 118 - 119 - // Verify the main schema references are hoisted 120 - const systemsSchema = 121 - schema.paths['/redfish/v1/Systems'].get.responses['200'].content['application/json'].schema; 122 - expect(systemsSchema.$ref).toContain('ResolutionStep'); 91 + const schema = await refParser.bundle({ pathOrUrlOrSchema }); 123 92 124 - const actionsSchema = 125 - schema.paths['/redfish/v1/Actions'].post.responses['200'].content['application/json'].schema; 126 - expect(actionsSchema.$ref).toContain('ActionParameters'); 127 - 128 - // All three schemas from the external YAML should be hoisted 129 - expect(schema.components).toBeDefined(); 130 - expect(schema.components.schemas).toBeDefined(); 131 - 132 - const schemaKeys = Object.keys(schema.components.schemas); 133 - 134 - // ResolutionStep (directly referenced) 135 - const resolutionStepKey = schemaKeys.find( 136 - (k) => k.includes('ResolutionStep') && !k.includes('Type') && !k.includes('ActionParameters'), 137 - ); 138 - expect(resolutionStepKey).toBeDefined(); 139 - 140 - // ResolutionType (sibling, referenced by ResolutionStep and ActionParameters) 141 - const resolutionTypeKey = schemaKeys.find((k) => k.includes('ResolutionType')); 142 - expect(resolutionTypeKey).toBeDefined(); 143 - expect(schema.components.schemas[resolutionTypeKey!]).toEqual({ 144 - description: 'Types of resolution actions', 145 - enum: ['ContactVendor', 'ResetToDefaults', 'RetryOperation'], 146 - type: 'string', 147 - }); 93 + const outputPath = path.join(getTempSnapshotsPath(), 'redfish-like.json'); 94 + const snapshotPath = path.join(getSnapshotsPath(), 'redfish-like.json'); 148 95 149 - // ActionParameters (directly referenced) 150 - const actionParamsKey = schemaKeys.find((k) => k.includes('ActionParameters')); 151 - expect(actionParamsKey).toBeDefined(); 96 + // Ensure directory exists 97 + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); 152 98 153 - // Verify that internal $refs in hoisted schemas point to hoisted locations 154 - const resolutionStep = schema.components.schemas[resolutionStepKey!]; 155 - expect(resolutionStep.properties.ResolutionType.oneOf[0].$ref).toContain('ResolutionType'); 156 - expect(resolutionStep.properties.ResolutionType.oneOf[0].$ref).toMatch( 157 - /^#\/components\/schemas\//, 158 - ); 99 + // Write the bundled result 100 + const content = JSON.stringify(schema, null, 2); 101 + fs.writeFileSync(outputPath, content); 159 102 160 - const actionParams = schema.components.schemas[actionParamsKey!]; 161 - expect(actionParams.properties.ActionType.$ref).toContain('ResolutionType'); 162 - expect(actionParams.properties.ActionType.$ref).toMatch(/^#\/components\/schemas\//); 103 + // Compare with snapshot 104 + await expect(content).toMatchFileSnapshot(snapshotPath); 163 105 }); 164 106 165 107 it('fixes cross-file references (schemas in different external files)', async () => { ··· 169 111 'json-schema-ref-parser', 170 112 'cross-file-ref-main.json', 171 113 ); 172 - const schema = (await refParser.bundle({ pathOrUrlOrSchema })) as any; 173 - 174 - // Both schemas should be hoisted 175 - expect(schema.components).toBeDefined(); 176 - expect(schema.components.schemas).toBeDefined(); 177 - 178 - const schemaKeys = Object.keys(schema.components.schemas); 179 - expect(schemaKeys.length).toBe(2); 114 + const schema = await refParser.bundle({ pathOrUrlOrSchema }); 180 115 181 - // Find the hoisted schemas 182 - const schemaAKey = schemaKeys.find((k) => k.includes('SchemaA')); 183 - const schemaBKey = schemaKeys.find((k) => k.includes('SchemaB')); 116 + const outputPath = path.join(getTempSnapshotsPath(), 'cross-file-ref-main.json'); 117 + const snapshotPath = path.join(getSnapshotsPath(), 'cross-file-ref-main.json'); 184 118 185 - expect(schemaAKey).toBeDefined(); 186 - expect(schemaBKey).toBeDefined(); 187 - 188 - // SchemaA should have a reference to SchemaB 189 - const schemaA = schema.components.schemas[schemaAKey!]; 190 - expect(schemaA.properties.typeField.$ref).toBe(`#/components/schemas/${schemaBKey}`); 191 - 192 - // SchemaB should be the enum type 193 - const schemaB = schema.components.schemas[schemaBKey!]; 194 - expect(schemaB).toEqual({ 195 - enum: ['TypeA', 'TypeB', 'TypeC'], 196 - type: 'string', 197 - }); 119 + // Ensure directory exists 120 + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); 198 121 199 - // Verify no dangling refs exist 200 - const findDanglingRefs = (obj: any, schemas: any): string[] => { 201 - const dangling: string[] = []; 202 - const check = (o: any) => { 203 - if (!o || typeof o !== 'object') return; 204 - if (o.$ref && typeof o.$ref === 'string' && o.$ref.startsWith('#/components/schemas/')) { 205 - const schemaName = o.$ref.replace('#/components/schemas/', ''); 206 - if (!schemas[schemaName]) { 207 - dangling.push(o.$ref); 208 - } 209 - } 210 - for (const value of Object.values(o)) { 211 - check(value); 212 - } 213 - }; 214 - check(obj); 215 - return dangling; 216 - }; 122 + // Write the bundled result 123 + const content = JSON.stringify(schema, null, 2); 124 + fs.writeFileSync(outputPath, content); 217 125 218 - const danglingRefs = findDanglingRefs(schema, schema.components.schemas); 219 - expect(danglingRefs).toEqual([]); 126 + // Compare with snapshot 127 + await expect(content).toMatchFileSnapshot(snapshotPath); 220 128 }); 221 129 });