diff --git a/packages/downgrader/tests/v3.1-to-v3.0.test.ts b/packages/downgrader/tests/v3.1-to-v3.0.test.ts index 9b9d80a..629c267 100644 --- a/packages/downgrader/tests/v3.1-to-v3.0.test.ts +++ b/packages/downgrader/tests/v3.1-to-v3.0.test.ts @@ -495,6 +495,7 @@ describe('downgradeSchemaV31ToV30', () => { ]) expect(downgradeSchemaV31ToV30({ type: 'null', enum: [null, 'a'] })).toEqual(onlyNull) expect(downgradeSchemaV31ToV30({ enum: [null] })).toEqual(onlyNull) + expect(downgradeSchemaV31ToV30({ enum: [null, 'a'] })).toEqual({ enum: [null, 'a'] }) // Only null may match the type, and the enum rules it out. expect(downgradeSchemaV31ToV30({ type: 'null', enum: ['a'] })).toEqual({ enum: ['a'], @@ -624,6 +625,11 @@ describe('downgradeSchemaV31ToV30', () => { nullable: true, enum: [null], }) + // As Zod emits `z.literal(5)`: the written type stays, though 5 is an integer. + expect(downgradeSchemaV31ToV30({ type: 'number', const: 5 })).toEqual({ + type: 'number', + enum: [5], + }) }) it('types a const with no type after its value', () => { @@ -686,6 +692,7 @@ describe('downgradeSchemaV31ToV30', () => { it('turns examples into example', () => { expect(downgradeSchemaV31ToV30({ examples: ['a', 'b'] })).toEqual({ example: 'a' }) expect(downgradeSchemaV31ToV30({ example: 'x', examples: ['a'] })).toEqual({ example: 'x' }) + expect(downgradeSchemaV31ToV30({ examples: [] })).toStrictEqual({}) }) it('turns tuples into arrays whose items match any item schema', () => { @@ -784,6 +791,10 @@ describe('downgradeSchemaV31ToV30', () => { type: 'string', format: 'binary', }) + expect(downgradeSchemaV31ToV30({ type: 'string', contentEncoding: 'binary' })).toEqual({ + type: 'string', + format: 'binary', + }) expect( downgradeSchemaV31ToV30({ type: 'string', @@ -820,6 +831,11 @@ describe('downgradeSchemaV31ToV30', () => { ], }), ).toEqual({}) + expect( + downgradeSchemaV31ToV30({ + anyOf: [{ type: 'string', contentMediaType: 'image/png' }, { type: 'integer' }], + }), + ).toEqual({ anyOf: [{ type: 'string', format: 'binary' }, { type: 'integer' }] }) }) describe('$ref', () => { @@ -1145,6 +1161,44 @@ describe('downgradeSchemaV31ToV30', () => { ).toEqual({ anyOf: [{ minimum: 0 }], allOf: [{ anyOf: [{ items: {} }, {}] }] }) }) + it('includes propertyNames beyond a string type, and unevaluatedProperties beside an in-place applicator', () => { + // As zod emits z.record with a key pattern. + expect( + downgradeSchemaV31ToV30({ + not: { type: 'object', propertyNames: { type: 'string', pattern: '^a' } }, + }), + ).toEqual({}) + expect( + downgradeSchemaV31ToV30({ + not: { allOf: [{ properties: { a: {} } }], unevaluatedProperties: false }, + }), + ).toEqual({}) + }) + + it('excludes propertyNames: true, and unevaluatedProperties beside additionalProperties', () => { + expect(downgradeSchemaV31ToV30({ not: { type: 'object', propertyNames: true } })).toEqual({ + not: { type: 'object' }, + }) + expect( + downgradeSchemaV31ToV30({ + not: { additionalProperties: { type: 'string' }, unevaluatedProperties: false }, + }), + ).toEqual({ not: { additionalProperties: { type: 'string' } } }) + }) + + it('carries over to a schema around it, through allOf, anyOf, properties, not, and oneOf', () => { + const lost = { type: 'object', propertyNames: { pattern: '^a' } } as const + expect(downgradeSchemaV31ToV30({ not: { allOf: [lost, { required: ['a'] }] } })).toEqual({}) + expect(downgradeSchemaV31ToV30({ not: { anyOf: [lost, { type: 'string' }] } })).toEqual({}) + expect( + downgradeSchemaV31ToV30({ not: { properties: { a: lost, b: { type: 'string' } } } }), + ).toEqual({}) + expect(downgradeSchemaV31ToV30({ not: { not: lost } })).toEqual({}) + expect( + downgradeSchemaV31ToV30({ not: { anyOf: [{ minimum: 0 }], oneOf: [lost, {}] } }), + ).toEqual({}) + }) + it('excludes keywords that restrict nothing, and tuples converted exactly', () => { // As zod emits a discriminated union whose member holds a record or a tuple. const record: OpenAPIV3_1.SchemaObject = { @@ -1483,6 +1537,9 @@ describe('unusual input', () => { ], }), ).toEqual({ oneOf: [{ type: 'object' }, { type: 'string' }] }) + expect( + downgradeSchemaV31ToV30({ oneOf: [{ type: 'object', if: undefined }, { type: 'string' }] }), + ).toEqual({ oneOf: [{ type: 'object' }, { type: 'string' }] }) }) it('tolerates malformed input without throwing', () => { diff --git a/packages/downgrader/tests/v3.2-to-v3.1.test.ts b/packages/downgrader/tests/v3.2-to-v3.1.test.ts index 2021069..34ac525 100644 --- a/packages/downgrader/tests/v3.2-to-v3.1.test.ts +++ b/packages/downgrader/tests/v3.2-to-v3.1.test.ts @@ -359,12 +359,14 @@ describe('downgradeSpecV32ToV31', () => { 200: { summary: 'OK', description: 'Everything went fine' }, 201: { summary: 'Created' }, 204: {}, + 404: { $ref: '#/components/responses/NotFound' }, }, }) expect(out?.responses).toEqual({ 200: { description: 'Everything went fine' }, 201: { description: 'Created' }, 204: { description: '' }, + 404: { $ref: '#/components/responses/NotFound' }, }) }) diff --git a/packages/downgrader/tests/validate.ts b/packages/downgrader/tests/validate.ts index 510c345..3d1d717 100644 --- a/packages/downgrader/tests/validate.ts +++ b/packages/downgrader/tests/validate.ts @@ -92,6 +92,8 @@ export async function expectValidDowngrade( const before = structuredClone(doc) const output = downgrade(doc) await expectValidAs(output, to) + // It holds only what JSON keeps, with no key holding `undefined`. + expect(output).toStrictEqual(JSON.parse(JSON.stringify(output))) expectNoNewDanglingRefs(doc, output) expect(doc).toEqual(before) return output