diff --git a/package.json b/package.json index 40a513eac5..a50b406f32 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "openapi-workspaces", "license": "MIT", "private": true, - "version": "0.53.15", + "version": "0.53.16", "workspaces": [ "projects/json-pointer-helpers", "projects/openapi-io", diff --git a/projects/fastify-capture/package.json b/projects/fastify-capture/package.json index 6cd08e687f..0da12a3364 100644 --- a/projects/fastify-capture/package.json +++ b/projects/fastify-capture/package.json @@ -2,7 +2,7 @@ "name": "@useoptic/fastify-capture", "license": "MIT", "packageManager": "yarn@4.0.2", - "version": "0.53.15", + "version": "0.53.16", "main": "build/index.js", "types": "build/index.d.ts", "files": [ diff --git a/projects/json-pointer-helpers/package.json b/projects/json-pointer-helpers/package.json index e0f4099b28..834edacc6f 100644 --- a/projects/json-pointer-helpers/package.json +++ b/projects/json-pointer-helpers/package.json @@ -2,7 +2,7 @@ "name": "@useoptic/json-pointer-helpers", "license": "MIT", "packageManager": "yarn@4.0.2", - "version": "0.53.15", + "version": "0.53.16", "main": "build/index.js", "types": "build/index.d.ts", "files": [ diff --git a/projects/openapi-io/package.json b/projects/openapi-io/package.json index 6198d20723..a059686233 100644 --- a/projects/openapi-io/package.json +++ b/projects/openapi-io/package.json @@ -2,7 +2,7 @@ "name": "@useoptic/openapi-io", "license": "MIT", "packageManager": "yarn@4.0.2", - "version": "0.53.15", + "version": "0.53.16", "main": "build/index.js", "types": "build/index.d.ts", "files": [ diff --git a/projects/openapi-utilities/package.json b/projects/openapi-utilities/package.json index 73f5b9a08f..b00d8afff3 100644 --- a/projects/openapi-utilities/package.json +++ b/projects/openapi-utilities/package.json @@ -2,7 +2,7 @@ "name": "@useoptic/openapi-utilities", "license": "MIT", "packageManager": "yarn@4.0.2", - "version": "0.53.15", + "version": "0.53.16", "main": "build/index.js", "types": "build/index.d.ts", "files": [ diff --git a/projects/optic/package.json b/projects/optic/package.json index 8a3fd5992b..c43ebdd3cb 100644 --- a/projects/optic/package.json +++ b/projects/optic/package.json @@ -2,7 +2,7 @@ "name": "@useoptic/optic", "license": "MIT", "packageManager": "yarn@4.0.2", - "version": "0.53.15", + "version": "0.53.16", "main": "build/index.js", "types": "build/index.d.ts", "files": [ diff --git a/projects/optic/src/commands/capture/patches/__tests__/__snapshots__/patches.test.ts.snap b/projects/optic/src/commands/capture/patches/__tests__/__snapshots__/patches.test.ts.snap index b33baca0b5..77617f6c1d 100644 --- a/projects/optic/src/commands/capture/patches/__tests__/__snapshots__/patches.test.ts.snap +++ b/projects/optic/src/commands/capture/patches/__tests__/__snapshots__/patches.test.ts.snap @@ -1,31 +1,119 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`generateEndpointSpecPatches OAS version 3.0.1 allOf with extra keys in interaction 1`] = ` +exports[`generateEndpointSpecPatches OAS version 3.0.1 3.0.x exclusiveMaximum and exclusiveMinimum booleans 1`] = ` [ { - "description": "update response body: add property name", + "bodyPath": "/paths/~1api~1animals/post/responses/200/content/application~1json", + "example": 20, + "interaction": { + "request": { + "body": null, + "headers": [], + "host": "localhost:3030", + "method": "post", + "path": "/api/animals", + "query": [], + }, + "response": { + "body": { + "body": "{"data":[{"no":10},{"no":20}]}", + "contentType": "application/json", + "size": 30, + }, + "headers": [], + "statusCode": "200", + }, + }, + "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/properties/data/items/properties/no/maximum", + "schemaPath": "/properties/data/items/properties/no/maximum", + "unpatchable": true, + "validationError": { + "instancePath": "/data/1/no", + "keyword": "maximum", + "message": "must be <= 19", + "params": { + "comparison": "<=", + "limit": 19, + }, + "schemaPath": "#/properties/data/items/properties/no/maximum", + }, + }, +] +`; + +exports[`generateEndpointSpecPatches OAS version 3.0.1 3.0.x exclusiveMaximum and exclusiveMinimum booleans 2`] = ` +{ + "info": {}, + "openapi": "3.0.1", + "paths": { + "/api/animals": { + "post": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "items": { + "properties": { + "no": { + "exclusiveMaximum": true, + "exclusiveMinumum": true, + "maximum": 20, + "minumum": 10, + "type": "number", + }, + }, + "required": [ + "no", + ], + "type": "object", + }, + "type": "array", + }, + }, + "type": "object", + }, + }, + }, + }, + }, + }, + }, + }, +} +`; + +exports[`generateEndpointSpecPatches OAS version 3.0.1 allOf allOf in a property 1`] = ` +[ + { + "description": "update response body: add property as", "diff": { - "description": "'name' is not documented", - "example": "me", - "instancePath": "/name", - "key": "name", + "description": "'as' is not documented", + "example": 12, + "instancePath": "/data/asda/as", + "key": "as", "keyword": "additionalProperties", "kind": "AdditionalProperty", - "parentObjectPath": "/allOf/0/properties", - "propertyExamplePath": "/name", - "propertyPath": "/allOf/0/properties/name", + "parentObjectPath": "/properties/data/allOf/1/properties/asda/properties", + "propertyExamplePath": "/data/asda/as", + "propertyPath": "/properties/data/allOf/1/properties/asda/properties/as", }, "groupedOperations": [ { + "extra": "same", "op": "add", - "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/allOf/0/required/-", - "value": "name", + "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/properties/data/allOf/1/properties/asda/required", + "value": [ + "as", + ], }, { "op": "add", - "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/allOf/0/properties/name", + "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/properties/data/allOf/1/properties/asda/properties/as", "value": { - "type": "string", + "type": "number", }, }, ], @@ -44,9 +132,9 @@ exports[`generateEndpointSpecPatches OAS version 3.0.1 allOf with extra keys in }, "response": { "body": { - "body": "{"status":"ok","name":"me","age":50}", + "body": "{"data":{"status":"ok","name":"me","age":50,"asda":{"as":12}}}", "contentType": "application/json", - "size": 36, + "size": 62, }, "headers": [], "statusCode": "200", @@ -59,23 +147,23 @@ exports[`generateEndpointSpecPatches OAS version 3.0.1 allOf with extra keys in "diff": { "description": "'age' is not documented", "example": 50, - "instancePath": "/age", + "instancePath": "/data/age", "key": "age", "keyword": "additionalProperties", "kind": "AdditionalProperty", - "parentObjectPath": "/allOf/0/properties", - "propertyExamplePath": "/age", - "propertyPath": "/allOf/0/properties/age", + "parentObjectPath": "/properties/data/allOf/0/properties", + "propertyExamplePath": "/data/age", + "propertyPath": "/properties/data/allOf/0/properties/age", }, "groupedOperations": [ { "op": "add", - "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/allOf/0/required/-", + "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/properties/data/allOf/0/required/-", "value": "age", }, { "op": "add", - "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/allOf/0/properties/age", + "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/properties/data/allOf/0/properties/age", "value": { "type": "number", }, @@ -96,9 +184,9 @@ exports[`generateEndpointSpecPatches OAS version 3.0.1 allOf with extra keys in }, "response": { "body": { - "body": "{"status":"ok","name":"me","age":50}", + "body": "{"data":{"status":"ok","name":"me","age":50,"asda":{"as":12}}}", "contentType": "application/json", - "size": 36, + "size": 62, }, "headers": [], "statusCode": "200", @@ -106,33 +194,151 @@ exports[`generateEndpointSpecPatches OAS version 3.0.1 allOf with extra keys in }, "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema", }, +] +`; + +exports[`generateEndpointSpecPatches OAS version 3.0.1 allOf allOf in a property 2`] = ` +{ + "info": {}, + "openapi": "3.0.1", + "paths": { + "/api/animals": { + "post": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "allOf": [ + { + "properties": { + "age": { + "type": "number", + }, + "status": { + "type": "string", + }, + }, + "required": [ + "status", + "age", + ], + "type": "object", + }, + { + "properties": { + "asda": { + "properties": { + "as": { + "type": "number", + }, + }, + "required": [ + "as", + ], + "type": "object", + }, + "name": { + "type": "string", + }, + }, + "type": "object", + }, + ], + }, + }, + "type": "object", + }, + }, + }, + }, + }, + }, + }, + }, +} +`; + +exports[`generateEndpointSpecPatches OAS version 3.0.1 allOf matching interaction 1`] = `[]`; + +exports[`generateEndpointSpecPatches OAS version 3.0.1 allOf matching interaction 2`] = ` +{ + "info": {}, + "openapi": "3.0.1", + "paths": { + "/api/animals": { + "post": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "properties": { + "age": { + "type": "number", + }, + "status": { + "type": "string", + }, + }, + "required": [ + "status", + ], + "type": "object", + }, + { + "properties": { + "name": { + "type": "string", + }, + }, + "type": "object", + }, + ], + }, + }, + }, + }, + }, + }, + }, + }, +} +`; + +exports[`generateEndpointSpecPatches OAS version 3.0.1 allOf with extra keys in interaction 1`] = ` +[ { - "description": "update response body: add property status", + "description": "update response body: add property as", "diff": { - "description": "'status' is not documented", - "example": "ok", - "instancePath": "/status", - "key": "status", + "description": "'as' is not documented", + "example": 12, + "instancePath": "/asda/as", + "key": "as", "keyword": "additionalProperties", "kind": "AdditionalProperty", - "parentObjectPath": "/allOf/1/properties", - "propertyExamplePath": "/status", - "propertyPath": "/allOf/1/properties/status", + "parentObjectPath": "/allOf/1/properties/asda/properties", + "propertyExamplePath": "/asda/as", + "propertyPath": "/allOf/1/properties/asda/properties/as", }, "groupedOperations": [ { "extra": "same", "op": "add", - "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/allOf/1/required", + "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/allOf/1/properties/asda/required", "value": [ - "status", + "as", ], }, { "op": "add", - "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/allOf/1/properties/status", + "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/allOf/1/properties/asda/properties/as", "value": { - "type": "string", + "type": "number", }, }, ], @@ -151,9 +357,9 @@ exports[`generateEndpointSpecPatches OAS version 3.0.1 allOf with extra keys in }, "response": { "body": { - "body": "{"status":"ok","name":"me","age":50}", + "body": "{"status":"ok","name":"me","age":50,"asda":{"as":12}}", "contentType": "application/json", - "size": 36, + "size": 53, }, "headers": [], "statusCode": "200", @@ -170,19 +376,19 @@ exports[`generateEndpointSpecPatches OAS version 3.0.1 allOf with extra keys in "key": "age", "keyword": "additionalProperties", "kind": "AdditionalProperty", - "parentObjectPath": "/allOf/1/properties", + "parentObjectPath": "/allOf/0/properties", "propertyExamplePath": "/age", - "propertyPath": "/allOf/1/properties/age", + "propertyPath": "/allOf/0/properties/age", }, "groupedOperations": [ { "op": "add", - "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/allOf/1/required/-", + "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/allOf/0/required/-", "value": "age", }, { "op": "add", - "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/allOf/1/properties/age", + "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/allOf/0/properties/age", "value": { "type": "number", }, @@ -203,9 +409,9 @@ exports[`generateEndpointSpecPatches OAS version 3.0.1 allOf with extra keys in }, "response": { "body": { - "body": "{"status":"ok","name":"me","age":50}", + "body": "{"status":"ok","name":"me","age":50,"asda":{"as":12}}", "contentType": "application/json", - "size": 36, + "size": 53, }, "headers": [], "statusCode": "200", @@ -234,36 +440,33 @@ exports[`generateEndpointSpecPatches OAS version 3.0.1 allOf with extra keys in "age": { "type": "number", }, - "name": { - "type": "string", - }, "status": { "type": "string", }, }, "required": [ "status", - "name", "age", ], "type": "object", }, { "properties": { - "age": { - "type": "number", + "asda": { + "properties": { + "as": { + "type": "number", + }, + }, + "required": [ + "as", + ], + "type": "object", }, "name": { "type": "string", }, - "status": { - "type": "string", - }, }, - "required": [ - "status", - "age", - ], "type": "object", }, ], @@ -323,58 +526,6 @@ exports[`generateEndpointSpecPatches OAS version 3.0.1 allOf with missing keys i }, "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema", }, - { - "description": "update response body: add property status", - "diff": { - "description": "'status' is not documented", - "example": "ok", - "instancePath": "/status", - "key": "status", - "keyword": "additionalProperties", - "kind": "AdditionalProperty", - "parentObjectPath": "/allOf/1/properties", - "propertyExamplePath": "/status", - "propertyPath": "/allOf/1/properties/status", - }, - "groupedOperations": [ - { - "op": "add", - "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/allOf/1/required/-", - "value": "status", - }, - { - "op": "add", - "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/allOf/1/properties/status", - "value": { - "type": "string", - }, - }, - ], - "impact": [ - "Addition", - "BackwardsCompatible", - ], - "interaction": { - "request": { - "body": null, - "headers": [], - "host": "localhost:3030", - "method": "post", - "path": "/api/animals", - "query": [], - }, - "response": { - "body": { - "body": "{"status":"ok"}", - "contentType": "application/json", - "size": 15, - }, - "headers": [], - "statusCode": "200", - }, - }, - "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema", - }, ] `; @@ -407,13 +558,8 @@ exports[`generateEndpointSpecPatches OAS version 3.0.1 allOf with missing keys i "name": { "type": "string", }, - "status": { - "type": "string", - }, }, - "required": [ - "status", - ], + "required": [], "type": "object", }, ], @@ -6602,32 +6748,35 @@ exports[`generateEndpointSpecPatches OAS version 3.0.1 undocumented response bod } `; -exports[`generateEndpointSpecPatches OAS version 3.1.0 allOf with extra keys in interaction 1`] = ` +exports[`generateEndpointSpecPatches OAS version 3.1.0 allOf allOf in a property 1`] = ` [ { - "description": "update response body: add property name", + "description": "update response body: add property as", "diff": { - "description": "'name' is not documented", - "example": "me", - "instancePath": "/name", - "key": "name", + "description": "'as' is not documented", + "example": 12, + "instancePath": "/data/asda/as", + "key": "as", "keyword": "additionalProperties", "kind": "AdditionalProperty", - "parentObjectPath": "/allOf/0/properties", - "propertyExamplePath": "/name", - "propertyPath": "/allOf/0/properties/name", + "parentObjectPath": "/properties/data/allOf/1/properties/asda/properties", + "propertyExamplePath": "/data/asda/as", + "propertyPath": "/properties/data/allOf/1/properties/asda/properties/as", }, "groupedOperations": [ { + "extra": "same", "op": "add", - "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/allOf/0/required/-", - "value": "name", + "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/properties/data/allOf/1/properties/asda/required", + "value": [ + "as", + ], }, { "op": "add", - "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/allOf/0/properties/name", + "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/properties/data/allOf/1/properties/asda/properties/as", "value": { - "type": "string", + "type": "number", }, }, ], @@ -6646,9 +6795,9 @@ exports[`generateEndpointSpecPatches OAS version 3.1.0 allOf with extra keys in }, "response": { "body": { - "body": "{"status":"ok","name":"me","age":50}", + "body": "{"data":{"status":"ok","name":"me","age":50,"asda":{"as":12}}}", "contentType": "application/json", - "size": 36, + "size": 62, }, "headers": [], "statusCode": "200", @@ -6661,23 +6810,23 @@ exports[`generateEndpointSpecPatches OAS version 3.1.0 allOf with extra keys in "diff": { "description": "'age' is not documented", "example": 50, - "instancePath": "/age", + "instancePath": "/data/age", "key": "age", "keyword": "additionalProperties", "kind": "AdditionalProperty", - "parentObjectPath": "/allOf/0/properties", - "propertyExamplePath": "/age", - "propertyPath": "/allOf/0/properties/age", + "parentObjectPath": "/properties/data/allOf/0/properties", + "propertyExamplePath": "/data/age", + "propertyPath": "/properties/data/allOf/0/properties/age", }, "groupedOperations": [ { "op": "add", - "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/allOf/0/required/-", + "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/properties/data/allOf/0/required/-", "value": "age", }, { "op": "add", - "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/allOf/0/properties/age", + "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/properties/data/allOf/0/properties/age", "value": { "type": "number", }, @@ -6698,9 +6847,9 @@ exports[`generateEndpointSpecPatches OAS version 3.1.0 allOf with extra keys in }, "response": { "body": { - "body": "{"status":"ok","name":"me","age":50}", + "body": "{"data":{"status":"ok","name":"me","age":50,"asda":{"as":12}}}", "contentType": "application/json", - "size": 36, + "size": 62, }, "headers": [], "statusCode": "200", @@ -6708,33 +6857,151 @@ exports[`generateEndpointSpecPatches OAS version 3.1.0 allOf with extra keys in }, "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema", }, +] +`; + +exports[`generateEndpointSpecPatches OAS version 3.1.0 allOf allOf in a property 2`] = ` +{ + "info": {}, + "openapi": "3.1.0", + "paths": { + "/api/animals": { + "post": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "allOf": [ + { + "properties": { + "age": { + "type": "number", + }, + "status": { + "type": "string", + }, + }, + "required": [ + "status", + "age", + ], + "type": "object", + }, + { + "properties": { + "asda": { + "properties": { + "as": { + "type": "number", + }, + }, + "required": [ + "as", + ], + "type": "object", + }, + "name": { + "type": "string", + }, + }, + "type": "object", + }, + ], + }, + }, + "type": "object", + }, + }, + }, + }, + }, + }, + }, + }, +} +`; + +exports[`generateEndpointSpecPatches OAS version 3.1.0 allOf matching interaction 1`] = `[]`; + +exports[`generateEndpointSpecPatches OAS version 3.1.0 allOf matching interaction 2`] = ` +{ + "info": {}, + "openapi": "3.1.0", + "paths": { + "/api/animals": { + "post": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "properties": { + "age": { + "type": "number", + }, + "status": { + "type": "string", + }, + }, + "required": [ + "status", + ], + "type": "object", + }, + { + "properties": { + "name": { + "type": "string", + }, + }, + "type": "object", + }, + ], + }, + }, + }, + }, + }, + }, + }, + }, +} +`; + +exports[`generateEndpointSpecPatches OAS version 3.1.0 allOf with extra keys in interaction 1`] = ` +[ { - "description": "update response body: add property status", + "description": "update response body: add property as", "diff": { - "description": "'status' is not documented", - "example": "ok", - "instancePath": "/status", - "key": "status", + "description": "'as' is not documented", + "example": 12, + "instancePath": "/asda/as", + "key": "as", "keyword": "additionalProperties", "kind": "AdditionalProperty", - "parentObjectPath": "/allOf/1/properties", - "propertyExamplePath": "/status", - "propertyPath": "/allOf/1/properties/status", + "parentObjectPath": "/allOf/1/properties/asda/properties", + "propertyExamplePath": "/asda/as", + "propertyPath": "/allOf/1/properties/asda/properties/as", }, "groupedOperations": [ { "extra": "same", "op": "add", - "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/allOf/1/required", + "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/allOf/1/properties/asda/required", "value": [ - "status", + "as", ], }, { "op": "add", - "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/allOf/1/properties/status", + "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/allOf/1/properties/asda/properties/as", "value": { - "type": "string", + "type": "number", }, }, ], @@ -6753,9 +7020,9 @@ exports[`generateEndpointSpecPatches OAS version 3.1.0 allOf with extra keys in }, "response": { "body": { - "body": "{"status":"ok","name":"me","age":50}", + "body": "{"status":"ok","name":"me","age":50,"asda":{"as":12}}", "contentType": "application/json", - "size": 36, + "size": 53, }, "headers": [], "statusCode": "200", @@ -6772,19 +7039,19 @@ exports[`generateEndpointSpecPatches OAS version 3.1.0 allOf with extra keys in "key": "age", "keyword": "additionalProperties", "kind": "AdditionalProperty", - "parentObjectPath": "/allOf/1/properties", + "parentObjectPath": "/allOf/0/properties", "propertyExamplePath": "/age", - "propertyPath": "/allOf/1/properties/age", + "propertyPath": "/allOf/0/properties/age", }, "groupedOperations": [ { "op": "add", - "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/allOf/1/required/-", + "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/allOf/0/required/-", "value": "age", }, { "op": "add", - "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/allOf/1/properties/age", + "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/allOf/0/properties/age", "value": { "type": "number", }, @@ -6805,9 +7072,9 @@ exports[`generateEndpointSpecPatches OAS version 3.1.0 allOf with extra keys in }, "response": { "body": { - "body": "{"status":"ok","name":"me","age":50}", + "body": "{"status":"ok","name":"me","age":50,"asda":{"as":12}}", "contentType": "application/json", - "size": 36, + "size": 53, }, "headers": [], "statusCode": "200", @@ -6836,36 +7103,33 @@ exports[`generateEndpointSpecPatches OAS version 3.1.0 allOf with extra keys in "age": { "type": "number", }, - "name": { - "type": "string", - }, "status": { "type": "string", }, }, "required": [ "status", - "name", "age", ], "type": "object", }, { "properties": { - "age": { - "type": "number", + "asda": { + "properties": { + "as": { + "type": "number", + }, + }, + "required": [ + "as", + ], + "type": "object", }, "name": { "type": "string", }, - "status": { - "type": "string", - }, }, - "required": [ - "status", - "age", - ], "type": "object", }, ], @@ -6925,58 +7189,6 @@ exports[`generateEndpointSpecPatches OAS version 3.1.0 allOf with missing keys i }, "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema", }, - { - "description": "update response body: add property status", - "diff": { - "description": "'status' is not documented", - "example": "ok", - "instancePath": "/status", - "key": "status", - "keyword": "additionalProperties", - "kind": "AdditionalProperty", - "parentObjectPath": "/allOf/1/properties", - "propertyExamplePath": "/status", - "propertyPath": "/allOf/1/properties/status", - }, - "groupedOperations": [ - { - "op": "add", - "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/allOf/1/required/-", - "value": "status", - }, - { - "op": "add", - "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema/allOf/1/properties/status", - "value": { - "type": "string", - }, - }, - ], - "impact": [ - "Addition", - "BackwardsCompatible", - ], - "interaction": { - "request": { - "body": null, - "headers": [], - "host": "localhost:3030", - "method": "post", - "path": "/api/animals", - "query": [], - }, - "response": { - "body": { - "body": "{"status":"ok"}", - "contentType": "application/json", - "size": 15, - }, - "headers": [], - "statusCode": "200", - }, - }, - "path": "/paths/~1api~1animals/post/responses/200/content/application~1json/schema", - }, ] `; @@ -7009,13 +7221,8 @@ exports[`generateEndpointSpecPatches OAS version 3.1.0 allOf with missing keys i "name": { "type": "string", }, - "status": { - "type": "string", - }, }, - "required": [ - "status", - ], + "required": [], "type": "object", }, ], diff --git a/projects/optic/src/commands/capture/patches/__tests__/patches.test.ts b/projects/optic/src/commands/capture/patches/__tests__/patches.test.ts index c25e93c319..b7ff0acfbf 100644 --- a/projects/optic/src/commands/capture/patches/__tests__/patches.test.ts +++ b/projects/optic/src/commands/capture/patches/__tests__/patches.test.ts @@ -632,8 +632,64 @@ describe('generateEndpointSpecPatches', () => { expect(specHolder.spec).toMatchSnapshot(); }); + test('3.0.x exclusiveMaximum and exclusiveMinimum booleans', async () => { + if (version !== '3.0.1') return; + specHolder.spec.paths['/api/animals'].post.responses = { + '200': { + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + data: { + type: 'array', + items: { + type: 'object', + properties: { + no: { + type: 'number', + maximum: 20, + exclusiveMaximum: true, + minumum: 10, + exclusiveMinumum: true, + }, + }, + required: ['no'], + }, + }, + }, + }, + }, + }, + }, + }; + + const interaction = makeInteraction( + { method: OpenAPIV3.HttpMethods.POST, path: '/api/animals' }, + { + responseBody: { + data: [{ no: 10 }, { no: 20 }], + }, + } + ); + + const patches = await AT.collect( + generateEndpointSpecPatches( + GenerateInteractions([interaction]), + specHolder, + { + method: 'post', + path: '/api/animals', + } + ) + ); + + expect(patches).toMatchSnapshot(); + expect(specHolder.spec).toMatchSnapshot(); + }); + describe('allOf', () => { - test('with extra keys in interaction', async () => { + test('matching interaction', async () => { specHolder.spec.paths['/api/animals'].post.responses = { '200': { content: { @@ -646,6 +702,9 @@ describe('generateEndpointSpecPatches', () => { status: { type: 'string', }, + age: { + type: 'number', + }, }, required: ['status'], }, @@ -686,6 +745,132 @@ describe('generateEndpointSpecPatches', () => { expect(specHolder.spec).toMatchSnapshot(); }); + test('with extra keys in interaction', async () => { + specHolder.spec.paths['/api/animals'].post.responses = { + '200': { + content: { + 'application/json': { + schema: { + allOf: [ + { + type: 'object', + properties: { + status: { + type: 'string', + }, + }, + required: ['status'], + }, + { + type: 'object', + properties: { + name: { + type: 'string', + }, + asda: { + type: 'object', + properties: {}, + }, + }, + }, + ], + }, + }, + }, + }, + }; + + const interaction = makeInteraction( + { method: OpenAPIV3.HttpMethods.POST, path: '/api/animals' }, + { + responseBody: { + status: 'ok', + name: 'me', + age: 50, + asda: { as: 12 }, + }, + } + ); + + const patches = await AT.collect( + generateEndpointSpecPatches( + GenerateInteractions([interaction]), + specHolder, + { + method: 'post', + path: '/api/animals', + } + ) + ); + + expect(patches).toMatchSnapshot(); + expect(specHolder.spec).toMatchSnapshot(); + }); + + test('allOf in a property', async () => { + specHolder.spec.paths['/api/animals'].post.responses = { + '200': { + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + data: { + allOf: [ + { + type: 'object', + properties: { + status: { + type: 'string', + }, + }, + required: ['status'], + }, + { + type: 'object', + properties: { + name: { + type: 'string', + }, + asda: { + type: 'object', + properties: {}, + }, + }, + }, + ], + }, + }, + }, + }, + }, + }, + }; + + const interaction = makeInteraction( + { method: OpenAPIV3.HttpMethods.POST, path: '/api/animals' }, + { + responseBody: { + data: { status: 'ok', name: 'me', age: 50, asda: { as: 12 } }, + }, + } + ); + + const patches = await AT.collect( + generateEndpointSpecPatches( + GenerateInteractions([interaction]), + specHolder, + { + method: 'post', + path: '/api/animals', + } + ) + ); + + expect(patches).toMatchSnapshot(); + expect(specHolder.spec).toMatchSnapshot(); + }); + test('with missing keys in interaction', async () => { specHolder.spec.paths['/api/animals'].post.responses = { '200': { diff --git a/projects/optic/src/commands/capture/patches/patchers/shapes/diff.ts b/projects/optic/src/commands/capture/patches/patchers/shapes/diff.ts index 5a8abad8ff..95b620f153 100644 --- a/projects/optic/src/commands/capture/patches/patchers/shapes/diff.ts +++ b/projects/optic/src/commands/capture/patches/patchers/shapes/diff.ts @@ -1,5 +1,6 @@ import jsonSchemaTraverse from 'json-schema-traverse'; -import Ajv, { ErrorObject, ValidateFunction } from 'ajv'; +import { ErrorObject, ValidateFunction } from 'ajv'; +import Ajv from 'ajv/dist/2019'; import { jsonPointerHelpers } from '@useoptic/json-pointer-helpers'; import { Ono } from '@jsdevtools/ono'; import { Result, Ok, Err } from 'ts-results'; @@ -13,6 +14,7 @@ import { requiredKeywordDiffs } from './handlers/required'; import { typeKeywordDiffs } from './handlers/type'; import { enumKeywordDiffs } from './handlers/enum'; import { CapturedInteraction } from '../../../sources/captured-interactions'; +import { unevaluatedPropertiesDiffs } from './handlers/unevaluatedProperties'; export function diffBodyBySchema( body: Body, @@ -41,12 +43,22 @@ function* diffVisitors( { specJsonPath, interaction, + schema, }: { specJsonPath: string; interaction: CapturedInteraction; + schema: SchemaObject; } ): IterableIterator { switch (validationError.keyword) { + case JsonSchemaKnownKeyword.unevaluatedProperties: + yield* unevaluatedPropertiesDiffs(validationError, example, { + specJsonPath, + interaction, + schema, + }); + break; + case JsonSchemaKnownKeyword.additionalProperties: yield* additionalPropertiesDiffs(validationError, example); break; @@ -147,6 +159,7 @@ export class ShapeDiffTraverser { private bodyValue?: any; private specJsonPath: string; private interaction: CapturedInteraction; + private schema?: SchemaObject; constructor({ specJsonPath, @@ -171,6 +184,7 @@ export class ShapeDiffTraverser { schema: SchemaObject ): Result { this.bodyValue = bodyValue; + this.schema = schema; try { this.validate = this.validator.compile(prepareSchemaForDiff(schema)); } catch (err) { @@ -195,7 +209,6 @@ export class ShapeDiffTraverser { let oneOfs: Map = new Map(); let oneOfBranchType: [string, ErrorObject][] = []; let oneOfBranchOther: [string, ErrorObject][] = []; - for (let validationError of validationErrors) { if (validationError.keyword === JsonSchemaKnownKeyword.oneOf) { let schemaPath = validationError.schemaPath.substring(1); // valid json pointer @@ -236,6 +249,7 @@ export class ShapeDiffTraverser { yield* diffVisitors(validationError, this.bodyValue, { specJsonPath: this.specJsonPath, interaction: this.interaction, + schema: this.schema!, }); } } @@ -245,6 +259,7 @@ export class ShapeDiffTraverser { yield* diffVisitors(otherBranchError, this.bodyValue, { specJsonPath: this.specJsonPath, interaction: this.interaction, + schema: this.schema!, }); // once a nested error has been visited, we consider this a branch type match @@ -259,6 +274,7 @@ export class ShapeDiffTraverser { yield* diffVisitors(oneOfError, this.bodyValue, { specJsonPath: this.specJsonPath, interaction: this.interaction, + schema: this.schema!, }); } } @@ -267,6 +283,7 @@ export class ShapeDiffTraverser { export enum JsonSchemaKnownKeyword { required = 'required', additionalProperties = 'additionalProperties', + unevaluatedProperties = 'unevaluatedProperties', type = 'type', oneOf = 'oneOf', enum = 'enum', @@ -284,18 +301,23 @@ function prepareSchemaForDiff(input: SchemaObject): SchemaObject { const schema: SchemaObject = JSON.parse(JSON.stringify(input)); jsonSchemaTraverse(schema as OpenAPIV3.SchemaObject, { allKeys: true, - cb: (schema) => { + cb: (schema, jsonPtr) => { /* Some developers don't set all the JSON Schema properties because it's quite verbose, effectively underspecifing their schemas. Optic tries to apply sensible defaults, which are easy to override by writing your schemas properly */ + const parts = jsonPointerHelpers.decode(jsonPtr); if ( OAS3.isObjectType(schema.type) && - !schema.hasOwnProperty('additionalProperties') + !schema.hasOwnProperty('additionalProperties') && + parts[parts.length - 2] !== 'allOf' // excludes direct children of allOF ) { schema['additionalProperties'] = false; } + if (schema.allOf) { + schema['unevaluatedProperties'] = false; + } // Fix case where nullable is set and there is no type key if (!schema.type && schema.nullable) { @@ -307,6 +329,21 @@ function prepareSchemaForDiff(input: SchemaObject): SchemaObject { schema.type = 'string'; } } + + // Handle case where exclusiveMaximum or exclusiveMinimum is boolean (valid in 3.0) + // Note that this will generate diffs that may produce patches that need to consider the case that there may be exclusive maximums + if (typeof schema.exclusiveMaximum === 'boolean') { + if (typeof schema.maximum === 'number') { + schema.maximum = schema.maximum - 1; + } + delete schema.exclusiveMaximum; + } + if (typeof schema.exclusiveMinimum === 'boolean') { + if (typeof schema.minumum === 'number') { + schema.minumum = schema.minumum + 1; + } + delete schema.exclusiveMinimum; + } }, }); return schema; diff --git a/projects/optic/src/commands/capture/patches/patchers/shapes/handlers/unevaluatedProperties.ts b/projects/optic/src/commands/capture/patches/patchers/shapes/handlers/unevaluatedProperties.ts new file mode 100644 index 0000000000..fab3f2911a --- /dev/null +++ b/projects/optic/src/commands/capture/patches/patchers/shapes/handlers/unevaluatedProperties.ts @@ -0,0 +1,97 @@ +import { SchemaObject } from '../schema'; +import { + JsonSchemaKnownKeyword, + ErrorObject, + ShapeDiffResult, + ShapeDiffResultKind, + UnpatchableDiff, +} from '../diff'; +import { jsonPointerHelpers } from '@useoptic/json-pointer-helpers'; +import { CapturedInteraction } from '../../../../sources/captured-interactions'; +import { OAS3 } from '@useoptic/openapi-utilities'; + +export function* unevaluatedPropertiesDiffs( + validationError: ErrorObject, + example: any, + { + specJsonPath, + interaction, + schema, + }: { + specJsonPath: string; + interaction: CapturedInteraction; + schema: SchemaObject; + } +): IterableIterator { + if (validationError.keyword !== JsonSchemaKnownKeyword.unevaluatedProperties) + return; + const key = validationError.params.unevaluatedProperty; + const instancePath = jsonPointerHelpers.append( + validationError.instancePath, + key + ); + const propertyExamplePath = jsonPointerHelpers.append( + validationError.instancePath, + key + ); + + const parts = jsonPointerHelpers.decode( + validationError.schemaPath.substring(1) + ); + parts.pop(); + const baseSchemaPath = jsonPointerHelpers.compile(parts); + const baseSchema = jsonPointerHelpers.tryGet(schema, baseSchemaPath); + let pathToAllofVariant: string | null = null; + if (baseSchema.match && baseSchema.value.allOf) { + const allOfVariants = baseSchema.value.allOf; + // TODO in the future we can use a more complicated heuristic to choose which allOf variant is most relevant + const allOfIdx = allOfVariants.findIndex((schema) => + OAS3.isObjectType(schema.type) + ); + if (allOfIdx > -1) { + pathToAllofVariant = jsonPointerHelpers.compile([ + 'allOf', + String(allOfIdx), + 'properties', + ]); + } + } + + if (!pathToAllofVariant) { + // This could happen if the allOf variant has a primitive type - in this case we surface that this is unpatchable and let the user resolve this + // Or if a user sets unevalutedProperties to their schema, we don't know how to handle if unless there's an allOf + const schemaPath = validationError.schemaPath.substring(1); + yield { + validationError, + example: jsonPointerHelpers.get(example, validationError.instancePath), + unpatchable: true, + interaction, + bodyPath: specJsonPath, + schemaPath, + path: jsonPointerHelpers.append( + specJsonPath, + 'schema', + ...jsonPointerHelpers.decode(schemaPath) + ), + }; + return; + } + + const parentObjectPath = jsonPointerHelpers.join( + baseSchemaPath, + pathToAllofVariant + ); + const propertyPath = jsonPointerHelpers.append(parentObjectPath, key); + + yield { + description: `'${key}' is not documented`, + kind: ShapeDiffResultKind.AdditionalProperty, + keyword: JsonSchemaKnownKeyword.additionalProperties, + example: jsonPointerHelpers.get(example, instancePath), + propertyPath, + instancePath, + parentObjectPath, + propertyExamplePath, + key, + }; +} diff --git a/projects/rulesets-base/package.json b/projects/rulesets-base/package.json index f3cb6ab423..aec9f63ff1 100644 --- a/projects/rulesets-base/package.json +++ b/projects/rulesets-base/package.json @@ -2,7 +2,7 @@ "name": "@useoptic/rulesets-base", "license": "MIT", "packageManager": "yarn@4.0.2", - "version": "0.53.15", + "version": "0.53.16", "main": "build/index.js", "types": "build/index.d.ts", "files": [ diff --git a/projects/standard-rulesets/package.json b/projects/standard-rulesets/package.json index b85f4297df..6c6387fe44 100644 --- a/projects/standard-rulesets/package.json +++ b/projects/standard-rulesets/package.json @@ -2,7 +2,7 @@ "name": "@useoptic/standard-rulesets", "license": "MIT", "packageManager": "yarn@4.0.2", - "version": "0.53.15", + "version": "0.53.16", "main": "build/index.js", "types": "build/index.d.ts", "files": [