From 5d42683e8ef839fd63be944f7fc18895e0a464ba Mon Sep 17 00:00:00 2001 From: blva <40155621+blva@users.noreply.github.com> Date: Thu, 29 Jun 2023 18:52:54 +0100 Subject: [PATCH] Add more checks (#298) --- BREAKING-CHANGES-EXAMPLES.md | 68 ++++-- checker/check-api-security-updated.go | 166 +++++++++++++++ checker/check-api-security-updated_test.go | 195 ++++++++++++++++++ checker/check-components-security-updated.go | 144 +++++++++++++ .../check-components-security-updated_test.go | 181 ++++++++++++++++ ...eck-request-body-required-value-updated.go | 1 + ...equest-body-required-value-updated_test.go | 2 +- ...ck-request-parameters-type-changed_test.go | 87 ++++++++ ...go => check-response-mediatype-updated.go} | 22 +- .../check-response-mediatype-updated_test.go | 57 +++++ ...eck-response-optional-property-updated.go} | 34 ++- ...response-optional-property-updated_test.go | 111 ++++++++++ ...check-response-property-became-optional.go | 4 +- ...eck-response-property-type-changed_test.go | 61 ++++++ ...required-property-became-non-write-only.go | 2 +- ...heck-response-required-property-removed.go | 59 ------ ...heck-response-required-property-updated.go | 91 ++++++++ ...response-required-property-updated_test.go | 107 ++++++++++ checker/checker_breaking_test.go | 1 + checker/checker_not_breaking_test.go | 19 +- checker/default_checks.go | 6 +- checker/localizations/localizations.go | 48 ++++- checker/localizations_src/en/messages.yaml | 23 +++ checker/localizations_src/ru/messages.yaml | 23 +++ data/checker/add_new_media_type_base.yaml | 58 ++++++ data/checker/add_new_media_type_revision.yaml | 68 ++++++ data/checker/api_security_added_base.yaml | 33 +++ data/checker/api_security_added_revision.yaml | 37 ++++ .../api_security_global_added_base.yaml | 33 +++ .../api_security_global_added_revision.yaml | 37 ++++ data/checker/api_security_updated_base.yaml | 46 +++++ .../api_security_updated_revision.yaml | 43 ++++ .../component_security_updated_base.yaml | 37 ++++ .../component_security_updated_revision.yaml | 40 ++++ .../request_parameter_type_changed_base.yaml | 72 +++++++ ...sponse_optional_property_removed_base.yaml | 45 ++++ ...se_optional_property_removed_revision.yaml | 42 ++++ ...response_required_property_added_base.yaml | 45 ++++ ...onse_required_property_added_revision.yaml | 49 +++++ .../response_schema_type_changed_base.yaml | 29 +++ ...response_schema_type_changed_revision.yaml | 45 ++++ 41 files changed, 2175 insertions(+), 96 deletions(-) create mode 100644 checker/check-api-security-updated.go create mode 100644 checker/check-api-security-updated_test.go create mode 100644 checker/check-components-security-updated.go create mode 100644 checker/check-components-security-updated_test.go create mode 100644 checker/check-request-parameters-type-changed_test.go rename checker/{check-response-mediatype-removed.go => check-response-mediatype-updated.go} (58%) create mode 100644 checker/check-response-mediatype-updated_test.go rename checker/{check-response-optional-property-removed.go => check-response-optional-property-updated.go} (53%) create mode 100644 checker/check-response-optional-property-updated_test.go create mode 100644 checker/check-response-property-type-changed_test.go delete mode 100644 checker/check-response-required-property-removed.go create mode 100644 checker/check-response-required-property-updated.go create mode 100644 checker/check-response-required-property-updated_test.go create mode 100644 data/checker/add_new_media_type_base.yaml create mode 100644 data/checker/add_new_media_type_revision.yaml create mode 100644 data/checker/api_security_added_base.yaml create mode 100644 data/checker/api_security_added_revision.yaml create mode 100644 data/checker/api_security_global_added_base.yaml create mode 100644 data/checker/api_security_global_added_revision.yaml create mode 100644 data/checker/api_security_updated_base.yaml create mode 100644 data/checker/api_security_updated_revision.yaml create mode 100644 data/checker/component_security_updated_base.yaml create mode 100644 data/checker/component_security_updated_revision.yaml create mode 100644 data/checker/request_parameter_type_changed_base.yaml create mode 100644 data/checker/response_optional_property_removed_base.yaml create mode 100644 data/checker/response_optional_property_removed_revision.yaml create mode 100644 data/checker/response_required_property_added_base.yaml create mode 100644 data/checker/response_required_property_added_revision.yaml create mode 100644 data/checker/response_schema_type_changed_base.yaml create mode 100644 data/checker/response_schema_type_changed_revision.yaml diff --git a/BREAKING-CHANGES-EXAMPLES.md b/BREAKING-CHANGES-EXAMPLES.md index 51178093..98f22d01 100644 --- a/BREAKING-CHANGES-EXAMPLES.md +++ b/BREAKING-CHANGES-EXAMPLES.md @@ -3,8 +3,8 @@ These examples are automatically generated from unit tests. ## Examples of breaking changes [Removing a success status is breaking](checker/check-response-status-updated_test.go?plain=1#L92) [adding a new required property in request body is breaking](checker/checker_breaking_property_test.go?plain=1#L352) -[adding a pattern to a schema is breaking for recursive properties](checker/checker_breaking_test.go?plain=1#L474) -[adding a pattern to a schema is breaking](checker/checker_breaking_test.go?plain=1#L458) +[adding a pattern to a schema is breaking for recursive properties](checker/checker_breaking_test.go?plain=1#L475) +[adding a pattern to a schema is breaking](checker/checker_breaking_test.go?plain=1#L459) [adding a required request body is breaking](checker/checker_breaking_test.go?plain=1#L65) [changing a request body to enum is breaking](checker/checker_breaking_property_test.go?plain=1#L122) [changing a request body type and changing it to enum simultaneously is breaking](checker/checker_breaking_property_test.go?plain=1#L152) @@ -32,12 +32,11 @@ These examples are automatically generated from unit tests. [changing request's body schema type from number to string is breaking](checker/checker_breaking_request_type_changed_test.go?plain=1#L31) [changing request's body schema type from number/none to integer/int32 is breaking](checker/checker_breaking_request_type_changed_test.go?plain=1#L89) [changing request's body schema type from string to number is breaking](checker/checker_breaking_request_type_changed_test.go?plain=1#L11) -[changing request's body to required is breaking](checker/check-request-body-required-value-updated_test.go?plain=1#L11) [changing response's body schema type from integer to number is breaking](checker/checker_breaking_response_type_changed_test.go?plain=1#L69) [changing response's body schema type from number to string is breaking](checker/checker_breaking_response_type_changed_test.go?plain=1#L31) [changing response's body schema type from string to number is breaking](checker/checker_breaking_response_type_changed_test.go?plain=1#L11) [changing response's embedded property schema type from string/none to integer/int32 is breaking](checker/checker_breaking_response_type_changed_test.go?plain=1#L108) -[deleting a media-type from response is breaking](checker/checker_breaking_test.go?plain=1#L428) +[deleting a media-type from response is breaking](checker/checker_breaking_test.go?plain=1#L429) [deleting a path is breaking](checker/checker_breaking_test.go?plain=1#L43) [deleting a path with some operations having sunset date in the future is breaking](checker/checker_deprecation_test.go?plain=1#L273) [deleting a required property in request is breaking with warn](checker/checker_breaking_property_test.go?plain=1#L368) @@ -53,9 +52,9 @@ These examples are automatically generated from unit tests. [deprecating an operation with a deprecation policy but without specifying sunset date is breaking](checker/checker_deprecation_test.go?plain=1#L84) [increasing max length in response is breaking](checker/checker_breaking_min_max_test.go?plain=1#L93) [increasing min items in request is breaking](checker/checker_breaking_min_max_test.go?plain=1#L236) -[modifying a pattern in a schema is breaking](checker/checker_breaking_test.go?plain=1#L490) -[modifying a pattern in request parameter is breaking](checker/checker_breaking_test.go?plain=1#L506) -[modifying the default value of an optional request parameter is breaking](checker/checker_breaking_test.go?plain=1#L536) +[modifying a pattern in a schema is breaking](checker/checker_breaking_test.go?plain=1#L491) +[modifying a pattern in request parameter is breaking](checker/checker_breaking_test.go?plain=1#L507) +[modifying the default value of an optional request parameter is breaking](checker/checker_breaking_test.go?plain=1#L537) [new required header param is breaking](checker/checker_breaking_test.go?plain=1#L171) [new required path param is breaking](checker/checker_breaking_test.go?plain=1#L155) [new required property in request header is breaking](checker/checker_breaking_property_test.go?plain=1#L17) @@ -63,18 +62,18 @@ These examples are automatically generated from unit tests. [reducing max length in request is breaking](checker/checker_breaking_min_max_test.go?plain=1#L12) [reducing min items in response is breaking](checker/checker_breaking_min_max_test.go?plain=1#L220) [reducing min length in response is breaking](checker/checker_breaking_min_max_test.go?plain=1#L62) -[removing an existing optional response header is breaking as warn](checker/checker_breaking_test.go?plain=1#L409) +[removing an existing optional response header is breaking as warn](checker/checker_breaking_test.go?plain=1#L410) [removing an existing required response header is breaking as error](checker/checker_breaking_test.go?plain=1#L227) [removing an existing response with non-successful status is breaking (optional)](checker/checker_breaking_test.go?plain=1#L264) [removing an existing response with successful status is breaking](checker/checker_breaking_test.go?plain=1#L246) -[removing an schema object from components is breaking (optional)](checker/checker_breaking_test.go?plain=1#L591) +[removing an schema object from components is breaking (optional)](checker/checker_breaking_test.go?plain=1#L592) [removing the path without a deprecation policy and without specifying sunset date is breaking if some APIs are not alpha stability level](checker/checker_deprecation_test.go?plain=1#L137) [removing the path without a deprecation policy and without specifying sunset date is breaking if some APIs are not draft stability level](checker/checker_deprecation_test.go?plain=1#L191) -[removing/updating a property enum in response is breaking (optional)](checker/checker_breaking_test.go?plain=1#L323) -[removing/updating a tag is breaking (optional)](checker/checker_breaking_test.go?plain=1#L340) -[removing/updating an enum in request body is breaking (optional)](checker/checker_breaking_test.go?plain=1#L301) +[removing/updating a property enum in response is breaking (optional)](checker/checker_breaking_test.go?plain=1#L324) +[removing/updating a tag is breaking (optional)](checker/checker_breaking_test.go?plain=1#L341) +[removing/updating an enum in request body is breaking (optional)](checker/checker_breaking_test.go?plain=1#L302) [removing/updating an operation id is breaking (optional)](checker/checker_breaking_test.go?plain=1#L282) -[setting the default value of an optional request parameter is breaking](checker/checker_breaking_test.go?plain=1#L554) +[setting the default value of an optional request parameter is breaking](checker/checker_breaking_test.go?plain=1#L555) ## Examples of non-breaking changes [adding a media-type to response is not breaking](checker/checker_not_breaking_test.go?plain=1#L179) @@ -82,8 +81,11 @@ These examples are automatically generated from unit tests. [adding a new required property under AllOf in response body is not breaking](checker/checker_breaking_property_test.go?plain=1#L432) [adding a new required read-only property in request body is not breaking](checker/checker_breaking_property_test.go?plain=1#L486) [adding a non-existent required property in request body is not breaking](checker/checker_breaking_property_test.go?plain=1#L294) +[adding a required property to response is not breaking](checker/checker_not_breaking_test.go?plain=1#L284) +[adding a tag is not breaking](checker/checker_not_breaking_test.go?plain=1#L261) [adding an enum value is not breaking](checker/checker_not_breaking_test.go?plain=1#L77) [adding an enum value to request body is not breaking](checker/checker_breaking_property_test.go?plain=1#L138) +[adding an operation ID is not breaking](checker/checker_not_breaking_test.go?plain=1#L272) [adding an optional request body is not breaking](checker/checker_not_breaking_test.go?plain=1#L32) [both max lengths in request are nil is not breaking](checker/checker_breaking_min_max_test.go?plain=1#L178) [both max lengths in response are nil is not breaking](checker/checker_breaking_min_max_test.go?plain=1#L192) @@ -105,7 +107,7 @@ These examples are automatically generated from unit tests. [changing servers is not breaking](checker/checker_not_breaking_test.go?plain=1#L247) [deleting a non-required non-write-only property in response body is not breaking](checker/checker_breaking_property_test.go?plain=1#L531) [deleting a path after sunset date of all contained operations is not breaking](checker/checker_deprecation_test.go?plain=1#L258) -[deleting a pattern from a schema is not breaking](checker/checker_breaking_test.go?plain=1#L444) +[deleting a pattern from a schema is not breaking](checker/checker_breaking_test.go?plain=1#L445) [deleting a required write-only property in response body is not breaking](checker/checker_breaking_property_test.go?plain=1#L514) [deleting a tag is not breaking](checker/checker_not_breaking_test.go?plain=1#L65) [deleting an operation after sunset date is not breaking](checker/checker_deprecation_test.go?plain=1#L69) @@ -117,8 +119,8 @@ These examples are automatically generated from unit tests. [deprecating an operation without a deprecation policy and without specifying sunset date is not breaking](checker/checker_deprecation_test.go?plain=1#L103) [increasing max length in request is not breaking](checker/checker_breaking_min_max_test.go?plain=1#L76) [increasing min items in response is not breaking](checker/checker_breaking_min_max_test.go?plain=1#L250) -[modifying a pattern to ".*" in a schema is not breaking](checker/checker_breaking_test.go?plain=1#L522) -[modifying the default value of a required request parameter is not breaking](checker/checker_breaking_test.go?plain=1#L572) +[modifying a pattern to ".*" in a schema is not breaking](checker/checker_breaking_test.go?plain=1#L523) +[modifying the default value of a required request parameter is not breaking](checker/checker_breaking_test.go?plain=1#L573) [new optional header param is not breaking](checker/checker_not_breaking_test.go?plain=1#L113) [new optional property in request header is not breaking](checker/checker_breaking_property_test.go?plain=1#L38) [new required response header param is not breaking](checker/checker_not_breaking_test.go?plain=1#L147) @@ -127,30 +129,56 @@ These examples are automatically generated from unit tests. [reducing max length in response is not breaking](checker/checker_breaking_min_max_test.go?plain=1#L31) [reducing min items in request is not breaking](checker/checker_breaking_min_max_test.go?plain=1#L206) [reducing min length in request is not breaking](checker/checker_breaking_min_max_test.go?plain=1#L48) -[removing an existing response with error status is not breaking](checker/checker_breaking_test.go?plain=1#L393) -[removing an existing response with unparseable status is not breaking](checker/checker_breaking_test.go?plain=1#L377) +[removing an existing response with error status is not breaking](checker/checker_breaking_test.go?plain=1#L394) +[removing an existing response with unparseable status is not breaking](checker/checker_breaking_test.go?plain=1#L378) [removing the path without a deprecation policy and without specifying sunset date is not breaking for alpha level](checker/checker_deprecation_test.go?plain=1#L118) [removing the path without a deprecation policy and without specifying sunset date is not breaking for draft level](checker/checker_deprecation_test.go?plain=1#L172) [renaming a path parameter is not breaking](checker/checker_breaking_test.go?plain=1#L135) ## Examples of info-level changes for changelog +[Adding a new global security to the API](checker/check-api-security-updated_test.go?plain=1#L11) +[Adding a new media type to response](checker/check-response-mediatype-updated_test.go?plain=1#L11) +[Adding a new oauth security scope](checker/check-components-security-updated_test.go?plain=1#L107) [Adding a new operation id](checker/check-api-operation-id-updated_test.go?plain=1#L61) +[Adding a new security component](checker/check-components-security-updated_test.go?plain=1#L61) +[Adding a new security to the API endpoint](checker/check-api-security-updated_test.go?plain=1#L105) [Adding a new tag](checker/check-api-tag-updated_test.go?plain=1#L11) [Adding a non-success response status](checker/check-response-status-updated_test.go?plain=1#L38) +[Adding a required property to response body is detected](checker/check-response-required-property-updated_test.go?plain=1#L11) +[Adding a security scope from an API global security](checker/check-api-security-updated_test.go?plain=1#L81) +[Adding a security scope to an API endpoint security](checker/check-api-security-updated_test.go?plain=1#L174) [Adding a success response status](checker/check-response-status-updated_test.go?plain=1#L11) +[Adding an optional write-only property to a response](checker/check-response-optional-property-updated_test.go?plain=1#L36) +[Changing a response property schema type](checker/check-response-property-type-changed_test.go?plain=1#L36) +[Changing a response schema type](checker/check-response-property-type-changed_test.go?plain=1#L11) +[Changing request header parameter type](checker/check-request-parameters-type-changed_test.go?plain=1#L63) +[Changing request path parameter type](checker/check-request-parameters-type-changed_test.go?plain=1#L11) +[Changing request query parameter type](checker/check-request-parameters-type-changed_test.go?plain=1#L37) +[Changing security component oauth's url](checker/check-components-security-updated_test.go?plain=1#L11) +[Changing security component type](checker/check-components-security-updated_test.go?plain=1#L36) +[Removing a global security from the API](checker/check-api-security-updated_test.go?plain=1#L34) +[Removing a new media type to response](checker/check-response-mediatype-updated_test.go?plain=1#L35) +[Removing a new oauth security scope](checker/check-components-security-updated_test.go?plain=1#L132) +[Removing a new security component](checker/check-components-security-updated_test.go?plain=1#L84) +[Removing a new security to the API endpoint](checker/check-api-security-updated_test.go?plain=1#L128) [Removing a non-success response status](checker/check-response-status-updated_test.go?plain=1#L65) +[Removing a security scope from an API endpoint security](checker/check-api-security-updated_test.go?plain=1#L151) +[Removing a security scope from an API global security](checker/check-api-security-updated_test.go?plain=1#L57) +[Removing an existent property that was required in response body is detected](checker/check-response-required-property-updated_test.go?plain=1#L34) [Removing an existing operation id](checker/check-api-operation-id-updated_test.go?plain=1#L11) [Removing an existing tag](checker/check-api-tag-updated_test.go?plain=1#L36) +[Removing an optional write-only property from a response](checker/check-response-optional-property-updated_test.go?plain=1#L11) [Updating an existing operation id](checker/check-api-operation-id-updated_test.go?plain=1#L36) [Updating an existing tag](checker/check-api-tag-updated_test.go?plain=1#L62) -[adding a tag](checker/checker_not_breaking_test.go?plain=1#L261) -[adding an operation ID](checker/checker_not_breaking_test.go?plain=1#L272) +[adding a required write-only property to response body is detected](checker/check-response-required-property-updated_test.go?plain=1#L58) [changing an existing header param from required to optional](checker/checker_request_parameter_required_value_updated_test.go?plain=1#L36) [changing an existing header param to optional](checker/checker_not_breaking_test.go?plain=1#L127) [changing an existing request body from required to optional](checker/checker_not_breaking_test.go?plain=1#L47) [changing request's body to optional](checker/check-request-body-required-value-updated_test.go?plain=1#L36) +[changing request's body to required is breaking](checker/check-request-body-required-value-updated_test.go?plain=1#L11) [deprecating an operation with sunset greater than min](checker/checker_not_breaking_test.go?plain=1#L193) [new header, query and cookie request params](checker/check-new-request-non-path-parameter_test.go?plain=1#L11) [new paths or path operations](checker/check-api-added_test.go?plain=1#L11) [path operations that became deprecated](checker/checker_deprecation_test.go?plain=1#L324) [path operations that were re-activated](checker/checker_deprecation_test.go?plain=1#L344) +[removing a required write-only property that was required in response body is detected](checker/check-response-required-property-updated_test.go?plain=1#L84) diff --git a/checker/check-api-security-updated.go b/checker/check-api-security-updated.go new file mode 100644 index 00000000..292f88bf --- /dev/null +++ b/checker/check-api-security-updated.go @@ -0,0 +1,166 @@ +package checker + +import ( + "fmt" + + "github.com/tufin/oasdiff/diff" +) + +const ( + APISecurityRemovedCheckId = "api-security-removed" + APISecurityAddedCheckId = "api-security-added" + APISecurityScopeAddedId = "api-security-scope-added" + APISecurityScopeRemovedId = "api-security-scope-removed" + APIGlobalSecurityRemovedCheckId = "api-global-security-removed" + APIGlobalSecurityAddedCheckId = "api-global-security-added" + APIGlobalSecurityScopeAddedId = "api-global-security-scope-added" + APIGlobalSecurityScopeRemovedId = "api-global-security-scope-removed" +) + +func checkGlobalSecurity(diffReport *diff.Diff, operationsSources *diff.OperationsSourcesMap, config BackwardCompatibilityCheckConfig) []BackwardCompatibilityError { + result := make([]BackwardCompatibilityError, 0) + if diffReport.SecurityDiff == nil { + return result + } + + for _, addedSecurity := range diffReport.SecurityDiff.Added { + result = append(result, BackwardCompatibilityError{ + Id: APIGlobalSecurityAddedCheckId, + Level: INFO, + Text: fmt.Sprintf(config.i18n(APIGlobalSecurityAddedCheckId), ColorizedValue(addedSecurity)), + Operation: "N/A", + Path: "", + Source: "security." + addedSecurity, + OperationId: "N/A", + }) + } + + for _, removedSecurity := range diffReport.SecurityDiff.Deleted { + result = append(result, BackwardCompatibilityError{ + Id: APIGlobalSecurityRemovedCheckId, + Level: INFO, + Text: fmt.Sprintf(config.i18n(APIGlobalSecurityRemovedCheckId), ColorizedValue(removedSecurity)), + Operation: "N/A", + Path: "", + Source: "security." + removedSecurity, + OperationId: "N/A", + }) + } + + for _, updatedSecurity := range diffReport.SecurityDiff.Modified { + for securitySchemeName, updatedSecuritySchemeScopes := range updatedSecurity { + for _, addedScope := range updatedSecuritySchemeScopes.Added { + result = append(result, BackwardCompatibilityError{ + Id: APIGlobalSecurityScopeAddedId, + Level: INFO, + Text: fmt.Sprintf(config.i18n(APIGlobalSecurityScopeAddedId), ColorizedValue(addedScope), ColorizedValue(securitySchemeName)), + Operation: "N/A", + Path: "", + Source: "security.scopes." + addedScope, + OperationId: "N/A", + }) + } + for _, deletedScope := range updatedSecuritySchemeScopes.Deleted { + result = append(result, BackwardCompatibilityError{ + Id: APIGlobalSecurityScopeRemovedId, + Level: INFO, + Text: fmt.Sprintf(config.i18n(APIGlobalSecurityScopeRemovedId), ColorizedValue(deletedScope), ColorizedValue(securitySchemeName)), + Operation: "N/A", + Path: "", + Source: "security.scopes." + deletedScope, + OperationId: "N/A", + }) + } + } + } + + return result + +} + +func APISecurityUpdatedCheck(diffReport *diff.Diff, operationsSources *diff.OperationsSourcesMap, config BackwardCompatibilityCheckConfig) []BackwardCompatibilityError { + result := make([]BackwardCompatibilityError, 0) + + result = append(result, checkGlobalSecurity(diffReport, operationsSources, config)...) + + if diffReport.PathsDiff == nil || diffReport.PathsDiff.Modified == nil { + return result + } + + for path, pathItem := range diffReport.PathsDiff.Modified { + if pathItem.OperationsDiff == nil { + continue + } + for operation, operationItem := range pathItem.OperationsDiff.Modified { + + source := (*operationsSources)[operationItem.Revision] + + if operationItem.SecurityDiff == nil { + continue + } + + for _, addedSecurity := range operationItem.SecurityDiff.Added { + if addedSecurity == "" { + continue + } + result = append(result, BackwardCompatibilityError{ + Id: APISecurityAddedCheckId, + Level: INFO, + Text: fmt.Sprintf(config.i18n(APISecurityAddedCheckId), ColorizedValue(addedSecurity)), + Operation: operation, + OperationId: operationItem.Revision.OperationID, + Path: path, + Source: source, + }) + } + + for _, deletedSecurity := range operationItem.SecurityDiff.Deleted { + if deletedSecurity == "" { + continue + } + result = append(result, BackwardCompatibilityError{ + Id: APISecurityRemovedCheckId, + Level: INFO, + Text: fmt.Sprintf(config.i18n(APISecurityRemovedCheckId), ColorizedValue(deletedSecurity)), + Operation: operation, + OperationId: operationItem.Revision.OperationID, + Path: path, + Source: source, + }) + } + + for _, updatedSecurity := range operationItem.SecurityDiff.Modified { + if updatedSecurity.Empty() { + continue + } + for securitySchemeName, updatedSecuritySchemeScopes := range updatedSecurity { + for _, addedScope := range updatedSecuritySchemeScopes.Added { + result = append(result, BackwardCompatibilityError{ + Id: APISecurityScopeAddedId, + Level: INFO, + Text: fmt.Sprintf(config.i18n(APISecurityScopeAddedId), ColorizedValue(addedScope), ColorizedValue(securitySchemeName)), + Operation: operation, + OperationId: operationItem.Revision.OperationID, + Path: path, + Source: source, + }) + } + for _, deletedScope := range updatedSecuritySchemeScopes.Deleted { + result = append(result, BackwardCompatibilityError{ + Id: APISecurityScopeRemovedId, + Level: INFO, + Text: fmt.Sprintf(config.i18n(APISecurityScopeRemovedId), ColorizedValue(deletedScope), ColorizedValue(securitySchemeName)), + Operation: operation, + OperationId: operationItem.Revision.OperationID, + Path: path, + Source: source, + }) + } + } + } + + } + } + + return result +} diff --git a/checker/check-api-security-updated_test.go b/checker/check-api-security-updated_test.go new file mode 100644 index 00000000..a69efcac --- /dev/null +++ b/checker/check-api-security-updated_test.go @@ -0,0 +1,195 @@ +package checker_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tufin/oasdiff/checker" + "github.com/tufin/oasdiff/diff" +) + +// CL: Adding a new global security to the API +func TestAPIGlobalSecurityyAdded(t *testing.T) { + s1, _ := open("../data/checker/api_security_global_added_base.yaml") + s2, err := open("../data/checker/api_security_global_added_revision.yaml") + require.Empty(t, err) + + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.APISecurityUpdatedCheck), d, osm, checker.INFO) + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "api-global-security-added", + Text: "the security scheme 'petstore_auth' was added to the API", + Comment: "", + Level: checker.INFO, + Operation: "N/A", + Path: "", + Source: "security.petstore_auth", + OperationId: "N/A", + }}, errs) +} + +// CL: Removing a global security from the API +func TestAPIGlobalSecurityyDeleted(t *testing.T) { + s1, _ := open("../data/checker/api_security_global_added_revision.yaml") + s2, err := open("../data/checker/api_security_global_added_base.yaml") + require.Empty(t, err) + + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.APISecurityUpdatedCheck), d, osm, checker.INFO) + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "api-global-security-removed", + Text: "the security scheme 'petstore_auth' was removed from the API", + Comment: "", + Level: checker.INFO, + Operation: "N/A", + Path: "", + Source: "security.petstore_auth", + OperationId: "N/A", + }}, errs) +} + +// CL: Removing a security scope from an API global security +func TestAPIGlobalSecurityScopeRemoved(t *testing.T) { + s1, _ := open("../data/checker/api_security_global_added_revision.yaml") + s2, err := open("../data/checker/api_security_global_added_revision.yaml") + require.Empty(t, err) + + s2.Spec.Security[0]["petstore_auth"] = s2.Spec.Security[0]["petstore_auth"][:1] + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.APISecurityUpdatedCheck), d, osm, checker.INFO) + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "api-global-security-scope-removed", + Text: "the security scope 'read:pets' was removed from the global security scheme 'petstore_auth'", + Comment: "", + Level: checker.INFO, + Operation: "N/A", + Path: "", + Source: "security.scopes.read:pets", + OperationId: "N/A", + }}, errs) +} + +// CL: Adding a security scope from an API global security +func TestAPIGlobalSecurityScopeAdded(t *testing.T) { + s1, _ := open("../data/checker/api_security_global_added_revision.yaml") + s2, err := open("../data/checker/api_security_global_added_revision.yaml") + require.Empty(t, err) + + s1.Spec.Security[0]["petstore_auth"] = s2.Spec.Security[0]["petstore_auth"][:1] + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.APISecurityUpdatedCheck), d, osm, checker.INFO) + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "api-global-security-scope-added", + Text: "the security scope 'read:pets' was added to the global security scheme 'petstore_auth'", + Comment: "", + Level: checker.INFO, + Operation: "N/A", + Path: "", + Source: "security.scopes.read:pets", + OperationId: "N/A", + }}, errs) +} + +// CL: Adding a new security to the API endpoint +func TestAPISecurityAdded(t *testing.T) { + s1, _ := open("../data/checker/api_security_added_base.yaml") + s2, err := open("../data/checker/api_security_added_revision.yaml") + require.Empty(t, err) + + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.APISecurityUpdatedCheck), d, osm, checker.INFO) + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "api-security-added", + Text: "the endpoint scheme security 'petstore_auth' was added to the API", + Comment: "", + Level: checker.INFO, + Operation: "POST", + Path: "/subscribe", + Source: "../data/checker/api_security_added_revision.yaml", + OperationId: "", + }}, errs) +} + +// CL: Removing a new security to the API endpoint +func TestAPISecurityDeleted(t *testing.T) { + s1, _ := open("../data/checker/api_security_added_revision.yaml") + s2, err := open("../data/checker/api_security_added_base.yaml") + require.Empty(t, err) + + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.APISecurityUpdatedCheck), d, osm, checker.INFO) + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "api-security-removed", + Text: "the endpoint scheme security 'petstore_auth' was removed from the API", + Comment: "", + Level: checker.INFO, + Operation: "POST", + Path: "/subscribe", + Source: "../data/checker/api_security_added_base.yaml", + OperationId: "", + }}, errs) +} + +// CL: Removing a security scope from an API endpoint security +func TestAPISecurityScopeRemoved(t *testing.T) { + s1, _ := open("../data/checker/api_security_updated_base.yaml") + s2, err := open("../data/checker/api_security_updated_revision.yaml") + require.Empty(t, err) + + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.APISecurityUpdatedCheck), d, osm, checker.INFO) + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "api-security-scope-removed", + Text: "the security scope 'read:pets' was removed from the endpoint's security scheme 'petstore_auth'", + Comment: "", + Level: checker.INFO, + Operation: "POST", + Path: "/subscribe", + Source: "../data/checker/api_security_updated_revision.yaml", + OperationId: "", + }}, errs) +} + +// CL: Adding a security scope to an API endpoint security +func TestAPISecurityScopeAdded(t *testing.T) { + s1, _ := open("../data/checker/api_security_updated_revision.yaml") + s2, err := open("../data/checker/api_security_updated_base.yaml") + require.Empty(t, err) + + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.APISecurityUpdatedCheck), d, osm, checker.INFO) + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "api-security-scope-added", + Text: "the security scope 'read:pets' was added to the endpoint's security scheme 'petstore_auth'", + Comment: "", + Level: checker.INFO, + Operation: "POST", + Path: "/subscribe", + Source: "../data/checker/api_security_updated_base.yaml", + OperationId: "", + }}, errs) +} diff --git a/checker/check-components-security-updated.go b/checker/check-components-security-updated.go new file mode 100644 index 00000000..01dd6646 --- /dev/null +++ b/checker/check-components-security-updated.go @@ -0,0 +1,144 @@ +package checker + +import ( + "fmt" + + "github.com/tufin/oasdiff/diff" +) + +const ( + APIComponentsSecurityRemovedCheckId = "api-security-component-removed" + APIComponentsSecurityAddedCheckId = "api-security-component-added" + APIComponentsSecurityComponentOauthUrlUpdated = "api-security-component-oauth-url-changed" + APIComponentsSecurityTyepUpdated = "api-security-component-type-changed" + APIComponentsSecurityOauthTokenUrlUpdated = "api-security-component-oauth-token-url-changed" + APIComponentSecurityOauthScopeAdded = "api-security-component-oauth-scope-added" + APIComponentSecurityOauthScopeRemoved = "api-security-component-oauth-scope-removed" + APIComponentSecurityOauthScopeUpdated = "api-security-component-oauth-scope-changed" +) + +func checkOAuthUpdates(updatedSecurity *diff.SecuritySchemeDiff, config BackwardCompatibilityCheckConfig, updatedSecurityName string) []BackwardCompatibilityError { + result := make([]BackwardCompatibilityError, 0) + + if updatedSecurity.OAuthFlowsDiff == nil { + return result + } + + if updatedSecurity.OAuthFlowsDiff.ImplicitDiff == nil { + return result + } + + if urlDiff := updatedSecurity.OAuthFlowsDiff.ImplicitDiff.AuthorizationURLDiff; urlDiff != nil { + result = append(result, BackwardCompatibilityError{ + Id: APIComponentsSecurityComponentOauthUrlUpdated, + Level: INFO, + Text: fmt.Sprintf(config.i18n(APIComponentsSecurityComponentOauthUrlUpdated), ColorizedValue(updatedSecurityName), ColorizedValue(urlDiff.From), ColorizedValue(urlDiff.To)), + Operation: "N/A", + Path: "N/A", + Source: "N/A", + OperationId: "N/A", + }) + } + + if tokenDiff := updatedSecurity.OAuthFlowsDiff.ImplicitDiff.TokenURLDiff; tokenDiff != nil { + result = append(result, BackwardCompatibilityError{ + Id: APIComponentsSecurityOauthTokenUrlUpdated, + Level: INFO, + Text: fmt.Sprintf(config.i18n(APIComponentsSecurityOauthTokenUrlUpdated), ColorizedValue(updatedSecurityName), ColorizedValue(tokenDiff.From), ColorizedValue(tokenDiff.To)), + Operation: "N/A", + Path: "N/A", + Source: "N/A", + OperationId: "N/A", + }) + } + + if scopesDiff := updatedSecurity.OAuthFlowsDiff.ImplicitDiff.ScopesDiff; scopesDiff != nil { + for _, addedScope := range scopesDiff.Added { + result = append(result, BackwardCompatibilityError{ + Id: APIComponentSecurityOauthScopeAdded, + Level: INFO, + Text: fmt.Sprintf(config.i18n(APIComponentSecurityOauthScopeAdded), ColorizedValue(updatedSecurityName), ColorizedValue(addedScope)), + Operation: "N/A", + Path: "N/A", + Source: "N/A", + OperationId: "N/A", + }) + } + + for _, removedScope := range scopesDiff.Deleted { + result = append(result, BackwardCompatibilityError{ + Id: APIComponentSecurityOauthScopeRemoved, + Level: INFO, + Text: fmt.Sprintf(config.i18n(APIComponentSecurityOauthScopeRemoved), ColorizedValue(updatedSecurityName), ColorizedValue(removedScope)), + Operation: "N/A", + Path: "N/A", + Source: "N/A", + OperationId: "N/A", + }) + } + + for name, modifiedScope := range scopesDiff.Modified { + result = append(result, BackwardCompatibilityError{ + Id: APIComponentSecurityOauthScopeUpdated, + Level: INFO, + Text: fmt.Sprintf(config.i18n(APIComponentSecurityOauthScopeUpdated), ColorizedValue(updatedSecurityName), ColorizedValue(name), ColorizedValue(modifiedScope.From), ColorizedValue(modifiedScope.To)), + Operation: "N/A", + Path: "N/A", + Source: "N/A", + OperationId: "N/A", + }) + } + + } + + return result +} + +func APIComponentsSecurityUpdatedCheck(diffReport *diff.Diff, operationsSources *diff.OperationsSourcesMap, config BackwardCompatibilityCheckConfig) []BackwardCompatibilityError { + result := make([]BackwardCompatibilityError, 0) + if diffReport.ComponentsDiff.SecuritySchemesDiff == nil { + return result + } + + for _, updatedSecurity := range diffReport.ComponentsDiff.SecuritySchemesDiff.Added { + result = append(result, BackwardCompatibilityError{ + Id: APIComponentsSecurityAddedCheckId, + Level: INFO, + Text: fmt.Sprintf(config.i18n(APIComponentsSecurityAddedCheckId), ColorizedValue(updatedSecurity)), + Operation: "N/A", + Path: "N/A", + Source: "N/A", + OperationId: "N/A", + }) + } + + for _, updatedSecurity := range diffReport.ComponentsDiff.SecuritySchemesDiff.Deleted { + result = append(result, BackwardCompatibilityError{ + Id: APIComponentsSecurityRemovedCheckId, + Level: INFO, + Text: fmt.Sprintf(config.i18n(APIComponentsSecurityRemovedCheckId), ColorizedValue(updatedSecurity)), + Operation: "N/A", + Path: "N/A", + Source: "N/A", + OperationId: "N/A", + }) + } + + for updatedSecurityName, updatedSecurity := range diffReport.ComponentsDiff.SecuritySchemesDiff.Modified { + result = append(result, checkOAuthUpdates(updatedSecurity, config, updatedSecurityName)...) + + if updatedSecurity.TypeDiff != nil { + result = append(result, BackwardCompatibilityError{ + Id: APIComponentsSecurityTyepUpdated, + Level: INFO, + Text: fmt.Sprintf(config.i18n(APIComponentsSecurityTyepUpdated), ColorizedValue(updatedSecurityName), ColorizedValue(updatedSecurity.TypeDiff.From), ColorizedValue(updatedSecurity.TypeDiff.To)), + Operation: "N/A", + Path: "N/A", + Source: "N/A", + OperationId: "N/A", + }) + } + } + + return result +} diff --git a/checker/check-components-security-updated_test.go b/checker/check-components-security-updated_test.go new file mode 100644 index 00000000..5db5c39d --- /dev/null +++ b/checker/check-components-security-updated_test.go @@ -0,0 +1,181 @@ +package checker_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tufin/oasdiff/checker" + "github.com/tufin/oasdiff/diff" +) + +// CL: Changing security component oauth's url +func TestComponentSecurityOauthURLUpdated(t *testing.T) { + s1, _ := open("../data/checker/component_security_updated_base.yaml") + s2, err := open("../data/checker/component_security_updated_base.yaml") + require.Empty(t, err) + + s2.Spec.Components.SecuritySchemes["petstore_auth"].Value.Flows.Implicit.AuthorizationURL = "http://example.new.org/api/oauth/dialog" + + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.APIComponentsSecurityUpdatedCheck), d, osm, checker.INFO) + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "api-security-component-oauth-url-changed", + Text: "the component security scheme 'petstore_auth' oauth url changed from 'http://example.org/api/oauth/dialog' to 'http://example.new.org/api/oauth/dialog'", + Comment: "", + Level: checker.INFO, + Operation: "N/A", + Path: "N/A", + Source: "N/A", + OperationId: "N/A", + }}, errs) +} + +// CL: Changing security component type +func TestComponentSecurityTypeUpdated(t *testing.T) { + s1, _ := open("../data/checker/component_security_updated_base.yaml") + s2, err := open("../data/checker/component_security_updated_base.yaml") + require.Empty(t, err) + + s2.Spec.Components.SecuritySchemes["petstore_auth"].Value.Type = "http" + + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.APIComponentsSecurityUpdatedCheck), d, osm, checker.INFO) + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "api-security-component-type-changed", + Text: "the component security scheme 'petstore_auth' type changed from 'oauth2' to 'http'", + Comment: "", + Level: checker.INFO, + Operation: "N/A", + Path: "N/A", + Source: "N/A", + OperationId: "N/A", + }}, errs) +} + +// CL: Adding a new security component +func TestComponentSecurityAdded(t *testing.T) { + s1, _ := open("../data/checker/component_security_updated_base.yaml") + s2, err := open("../data/checker/component_security_updated_revision.yaml") + require.Empty(t, err) + + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.APIComponentsSecurityUpdatedCheck), d, osm, checker.INFO) + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "api-security-component-added", + Text: "the component security scheme 'BasicAuth' was added", + Comment: "", + Level: checker.INFO, + Operation: "N/A", + Path: "N/A", + Source: "N/A", + OperationId: "N/A", + }}, errs) +} + +// CL: Removing a new security component +func TestComponentSecurityRemoved(t *testing.T) { + s1, _ := open("../data/checker/component_security_updated_revision.yaml") + s2, err := open("../data/checker/component_security_updated_base.yaml") + require.Empty(t, err) + + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.APIComponentsSecurityUpdatedCheck), d, osm, checker.INFO) + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "api-security-component-removed", + Text: "the component security scheme 'BasicAuth' was removed", + Comment: "", + Level: checker.INFO, + Operation: "N/A", + Path: "N/A", + Source: "N/A", + OperationId: "N/A", + }}, errs) +} + +// CL: Adding a new oauth security scope +func TestComponentSecurityOauthScopeAdded(t *testing.T) { + s1, _ := open("../data/checker/component_security_updated_base.yaml") + s2, err := open("../data/checker/component_security_updated_base.yaml") + require.Empty(t, err) + + s2.Spec.Components.SecuritySchemes["petstore_auth"].Value.Flows.Implicit.Scopes["admin:pets"] = "grants access to admin operations" + + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.APIComponentsSecurityUpdatedCheck), d, osm, checker.INFO) + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "api-security-component-oauth-scope-added", + Text: "the component security scheme 'petstore_auth' oauth scope 'admin:pets' was added", + Comment: "", + Level: checker.INFO, + Operation: "N/A", + Path: "N/A", + Source: "N/A", + OperationId: "N/A", + }}, errs) +} + +// CL: Removing a new oauth security scope +func TestComponentSecurityOauthScopeRemoved(t *testing.T) { + s1, _ := open("../data/checker/component_security_updated_base.yaml") + s2, err := open("../data/checker/component_security_updated_base.yaml") + require.Empty(t, err) + + // Add to s1 so that it's deletion is identified + s1.Spec.Components.SecuritySchemes["petstore_auth"].Value.Flows.Implicit.Scopes["admin:pets"] = "grants access to admin operations" + + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.APIComponentsSecurityUpdatedCheck), d, osm, checker.INFO) + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "api-security-component-oauth-scope-removed", + Text: "the component security scheme 'petstore_auth' oauth scope 'admin:pets' was removed", + Comment: "", + Level: checker.INFO, + Operation: "N/A", + Path: "N/A", + Source: "N/A", + OperationId: "N/A", + }}, errs) +} + +// CL: Removing a new oauth security scope +func TestComponentSecurityOauthScopeUpdated(t *testing.T) { + s1, _ := open("../data/checker/component_security_updated_base.yaml") + s2, err := open("../data/checker/component_security_updated_base.yaml") + require.Empty(t, err) + + s2.Spec.Components.SecuritySchemes["petstore_auth"].Value.Flows.Implicit.Scopes["read:pets"] = "grants access to pets (deprecated)" + + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.APIComponentsSecurityUpdatedCheck), d, osm, checker.INFO) + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "api-security-component-oauth-scope-changed", + Text: "the component security scheme 'petstore_auth' oauth scope 'read:pets' was updated from 'read your pets' to 'grants access to pets (deprecated)'", + Comment: "", + Level: checker.INFO, + Operation: "N/A", + Path: "N/A", + Source: "N/A", + OperationId: "N/A", + }}, errs) +} diff --git a/checker/check-request-body-required-value-updated.go b/checker/check-request-body-required-value-updated.go index 9a6b363f..6f92f996 100644 --- a/checker/check-request-body-required-value-updated.go +++ b/checker/check-request-body-required-value-updated.go @@ -17,6 +17,7 @@ func RequestBodyRequiredUpdatedCheck(diffReport *diff.Diff, operationsSources *d if operationItem.RequestBodyDiff == nil { continue } + if operationItem.RequestBodyDiff.RequiredDiff == nil { continue } diff --git a/checker/check-request-body-required-value-updated_test.go b/checker/check-request-body-required-value-updated_test.go index 4134acc3..d55537b3 100644 --- a/checker/check-request-body-required-value-updated_test.go +++ b/checker/check-request-body-required-value-updated_test.go @@ -8,7 +8,7 @@ import ( "github.com/tufin/oasdiff/diff" ) -// BC: changing request's body to required is breaking +// CL: changing request's body to required is breaking func TestRequestBodyBecameRequired(t *testing.T) { s1, _ := open("../data/checker/request_body_became_required_base.yaml") s2, err := open("../data/checker/request_body_became_required_base.yaml") diff --git a/checker/check-request-parameters-type-changed_test.go b/checker/check-request-parameters-type-changed_test.go new file mode 100644 index 00000000..e432006f --- /dev/null +++ b/checker/check-request-parameters-type-changed_test.go @@ -0,0 +1,87 @@ +package checker_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tufin/oasdiff/checker" + "github.com/tufin/oasdiff/diff" +) + +// CL: Changing request path parameter type +func TestRequestPathParamTypeChanged(t *testing.T) { + s1, err := open("../data/checker/request_parameter_type_changed_base.yaml") + require.Empty(t, err) + s2, err := open("../data/checker/request_parameter_type_changed_base.yaml") + require.Empty(t, err) + + s2.Spec.Paths["/api/v1.0/groups"].Post.Parameters[0].Value.Schema.Value.Type = "int" + + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.RequestParameterTypeChangedCheck), d, osm, checker.INFO) + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "request-parameter-type-changed", + Text: "for the 'path' request parameter 'groupId', the type/format was changed from 'string'/'none' to 'int'/'none'", + Comment: "", + Level: checker.ERR, + Operation: "POST", + Path: "/api/v1.0/groups", + Source: "../data/checker/request_parameter_type_changed_base.yaml", + OperationId: "createOneGroup", + }}, errs) +} + +// CL: Changing request query parameter type +func TestRequestQueryParamTypeChanged(t *testing.T) { + s1, err := open("../data/checker/request_parameter_type_changed_base.yaml") + require.Empty(t, err) + s2, err := open("../data/checker/request_parameter_type_changed_base.yaml") + require.Empty(t, err) + + s2.Spec.Paths["/api/v1.0/groups"].Post.Parameters[1].Value.Schema.Value.Type = "int" + + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.RequestParameterTypeChangedCheck), d, osm, checker.INFO) + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "request-parameter-type-changed", + Text: "for the 'query' request parameter 'token', the type/format was changed from 'string'/'uuid' to 'int'/'uuid'", + Comment: "", + Level: checker.ERR, + Operation: "POST", + Path: "/api/v1.0/groups", + Source: "../data/checker/request_parameter_type_changed_base.yaml", + OperationId: "createOneGroup", + }}, errs) +} + +// CL: Changing request header parameter type +func TestRequestQueryHeaderTypeChanged(t *testing.T) { + s1, err := open("../data/checker/request_parameter_type_changed_base.yaml") + require.Empty(t, err) + s2, err := open("../data/checker/request_parameter_type_changed_base.yaml") + require.Empty(t, err) + + s2.Spec.Paths["/api/v1.0/groups"].Post.Parameters[2].Value.Schema.Value.Type = "int" + + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.RequestParameterTypeChangedCheck), d, osm, checker.INFO) + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "request-parameter-type-changed", + Text: "for the 'header' request parameter 'X-Request-ID', the type/format was changed from 'string'/'uuid' to 'int'/'uuid'", + Comment: "", + Level: checker.ERR, + Operation: "POST", + Path: "/api/v1.0/groups", + Source: "../data/checker/request_parameter_type_changed_base.yaml", + OperationId: "createOneGroup", + }}, errs) +} diff --git a/checker/check-response-mediatype-removed.go b/checker/check-response-mediatype-updated.go similarity index 58% rename from checker/check-response-mediatype-removed.go rename to checker/check-response-mediatype-updated.go index 997bca00..02edb119 100644 --- a/checker/check-response-mediatype-removed.go +++ b/checker/check-response-mediatype-updated.go @@ -6,7 +6,12 @@ import ( "github.com/tufin/oasdiff/diff" ) -func ResponseMediaTypeRemoved(diffReport *diff.Diff, operationsSources *diff.OperationsSourcesMap, config BackwardCompatibilityCheckConfig) []BackwardCompatibilityError { +const ( + ResponseMediaTypeUpdatedId = "response-media-type-removed" + ResponseMediaTypeAddedId = "response-media-type-added" +) + +func ResponseMediaTypeUpdated(diffReport *diff.Diff, operationsSources *diff.OperationsSourcesMap, config BackwardCompatibilityCheckConfig) []BackwardCompatibilityError { result := make([]BackwardCompatibilityError, 0) if diffReport.PathsDiff == nil { return result @@ -32,9 +37,20 @@ func ResponseMediaTypeRemoved(diffReport *diff.Diff, operationsSources *diff.Ope } for _, mediaType := range responsesDiff.ContentDiff.MediaTypeDeleted { result = append(result, BackwardCompatibilityError{ - Id: "response-media-type-removed", + Id: ResponseMediaTypeUpdatedId, Level: ERR, - Text: fmt.Sprintf(config.i18n("response-media-type-removed"), ColorizedValue(mediaType), ColorizedValue(responseStatus)), + Text: fmt.Sprintf(config.i18n(ResponseMediaTypeUpdatedId), ColorizedValue(mediaType), ColorizedValue(responseStatus)), + Operation: operation, + OperationId: operationItem.Revision.OperationID, + Path: path, + Source: source, + }) + } + for _, mediaType := range responsesDiff.ContentDiff.MediaTypeAdded { + result = append(result, BackwardCompatibilityError{ + Id: ResponseMediaTypeAddedId, + Level: INFO, + Text: fmt.Sprintf(config.i18n(ResponseMediaTypeAddedId), ColorizedValue(mediaType), ColorizedValue(responseStatus)), Operation: operation, OperationId: operationItem.Revision.OperationID, Path: path, diff --git a/checker/check-response-mediatype-updated_test.go b/checker/check-response-mediatype-updated_test.go new file mode 100644 index 00000000..19a2dc34 --- /dev/null +++ b/checker/check-response-mediatype-updated_test.go @@ -0,0 +1,57 @@ +package checker_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tufin/oasdiff/checker" + "github.com/tufin/oasdiff/diff" +) + +// CL: Adding a new media type to response +func TestAddNewMediaType(t *testing.T) { + s1, err := open("../data/checker/add_new_media_type_base.yaml") + require.Empty(t, err) + s2, err := open("../data/checker/add_new_media_type_revision.yaml") + require.Empty(t, err) + + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.ResponseMediaTypeUpdated), d, osm, checker.INFO) + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "response-media-type-added", + Text: "added the media type 'application/xml' for the response with the status '200'", + Comment: "", + Level: checker.INFO, + Operation: "POST", + Path: "/api/v1.0/groups", + Source: "../data/checker/add_new_media_type_revision.yaml", + OperationId: "createOneGroup", + }}, errs) +} + +// CL: Removing a new media type to response +func TestDeleteNewMediaType(t *testing.T) { + s1, err := open("../data/checker/add_new_media_type_revision.yaml") + require.Empty(t, err) + s2, err := open("../data/checker/add_new_media_type_base.yaml") + require.Empty(t, err) + + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.ResponseMediaTypeUpdated), d, osm, checker.INFO) + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "response-media-type-removed", + Text: "removed the media type 'application/xml' for the response with the status '200'", + Comment: "", + Level: checker.ERR, + Operation: "POST", + Path: "/api/v1.0/groups", + Source: "../data/checker/add_new_media_type_base.yaml", + OperationId: "createOneGroup", + }}, errs) +} diff --git a/checker/check-response-optional-property-removed.go b/checker/check-response-optional-property-updated.go similarity index 53% rename from checker/check-response-optional-property-removed.go rename to checker/check-response-optional-property-updated.go index ee8d64f6..0846b5cb 100644 --- a/checker/check-response-optional-property-removed.go +++ b/checker/check-response-optional-property-updated.go @@ -8,7 +8,7 @@ import ( "golang.org/x/exp/slices" ) -func ResponseOptionalPropertyRemovedCheck(diffReport *diff.Diff, operationsSources *diff.OperationsSourcesMap, config BackwardCompatibilityCheckConfig) []BackwardCompatibilityError { +func ResponseOptionalPropertyUpdatedCheck(diffReport *diff.Diff, operationsSources *diff.OperationsSourcesMap, config BackwardCompatibilityCheckConfig) []BackwardCompatibilityError { result := make([]BackwardCompatibilityError, 0) if diffReport.PathsDiff == nil { return result @@ -35,22 +35,48 @@ func ResponseOptionalPropertyRemovedCheck(diffReport *diff.Diff, operationsSourc CheckDeletedPropertiesDiff( mediaTypeDiff.SchemaDiff, func(propertyPath string, propertyName string, propertyItem *openapi3.Schema, parent *diff.SchemaDiff) { + level := WARN + id := "response-optional-property-removed" if propertyItem.WriteOnly { + level = INFO + id = "response-optional-write-only-property-removed" + } + if slices.Contains(parent.Base.Value.Required, propertyName) { + // covered by response-required-property-removed return } + result = append(result, BackwardCompatibilityError{ + Id: id, + Level: level, + Text: fmt.Sprintf(config.i18n(id), ColorizedValue(propertyFullName(propertyPath, propertyName)), ColorizedValue(responseStatus)), + Operation: operation, + OperationId: operationItem.Revision.OperationID, + Path: path, + Source: source, + }) + }) + CheckAddedPropertiesDiff( + mediaTypeDiff.SchemaDiff, + func(propertyPath string, propertyName string, propertyItem *openapi3.Schema, parent *diff.SchemaDiff) { + id := "response-optional-property-added" + if propertyItem.WriteOnly { + id = "response-optional-write-only-property-added" + } if slices.Contains(parent.Base.Value.Required, propertyName) { + // covered by response-required-property-added return } result = append(result, BackwardCompatibilityError{ - Id: "response-optional-property-removed", - Level: WARN, - Text: fmt.Sprintf(config.i18n("response-optional-property-removed"), ColorizedValue(propertyFullName(propertyPath, propertyName)), ColorizedValue(responseStatus)), + Id: id, + Level: INFO, + Text: fmt.Sprintf(config.i18n(id), ColorizedValue(propertyFullName(propertyPath, propertyName)), ColorizedValue(responseStatus)), Operation: operation, OperationId: operationItem.Revision.OperationID, Path: path, Source: source, }) }) + } } } diff --git a/checker/check-response-optional-property-updated_test.go b/checker/check-response-optional-property-updated_test.go new file mode 100644 index 00000000..67213515 --- /dev/null +++ b/checker/check-response-optional-property-updated_test.go @@ -0,0 +1,111 @@ +package checker_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tufin/oasdiff/checker" + "github.com/tufin/oasdiff/diff" +) + +// CL: Removing an optional write-only property from a response +func TestResponseOptionalPropertyUpdatedCheck(t *testing.T) { + s1, err := open("../data/checker/response_optional_property_removed_base.yaml") + require.Empty(t, err) + s2, err := open("../data/checker/response_optional_property_removed_revision.yaml") + require.Empty(t, err) + + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.ResponseOptionalPropertyUpdatedCheck), d, osm, checker.INFO) + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "response-optional-property-removed", + Text: "removed the optional property 'data/id' from the response with the '200' status", + Comment: "", + Level: checker.WARN, + Operation: "POST", + Path: "/api/v1.0/groups", + Source: "../data/checker/response_optional_property_removed_revision.yaml", + OperationId: "createOneGroup", + }, + }, errs) +} + +// CL: Adding an optional write-only property to a response +func TestResponseOptionalPropertyAddedCheck(t *testing.T) { + s1, err := open("../data/checker/response_optional_property_removed_revision.yaml") + require.Empty(t, err) + s2, err := open("../data/checker/response_optional_property_removed_base.yaml") + require.Empty(t, err) + + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.ResponseOptionalPropertyUpdatedCheck), d, osm, checker.INFO) + + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "response-optional-property-added", + Text: "added the optional property 'data/id' to the response with the '200' status", + Comment: "", + Level: checker.INFO, + Operation: "POST", + Path: "/api/v1.0/groups", + Source: "../data/checker/response_optional_property_removed_base.yaml", + OperationId: "createOneGroup", + }, + }, errs) +} + +// CL: Removing an optional write-only property from a response +func TestResponseOptionalWriteOnlyPropertyRemovedCheck(t *testing.T) { + s1, err := open("../data/checker/response_optional_property_removed_base.yaml") + require.Empty(t, err) + s2, err := open("../data/checker/response_optional_property_removed_revision.yaml") + require.Empty(t, err) + + s1.Spec.Paths["/api/v1.0/groups"].Post.Responses["200"].Value.Content["application/json"].Schema.Value.Properties["data"].Value.Properties["id"].Value.WriteOnly = true + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.ResponseOptionalPropertyUpdatedCheck), d, osm, checker.INFO) + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "response-optional-write-only-property-removed", + Text: "removed the optional write-only property 'data/id' from the response with the '200' status", + Level: checker.INFO, + Operation: "POST", + Path: "/api/v1.0/groups", + Source: "../data/checker/response_optional_property_removed_revision.yaml", + OperationId: "createOneGroup", + }, + }, errs) +} + +// CL: Adding an optional write-only property to a response +func TestResponseOptionalWriteOnlyPropertyAddedCheck(t *testing.T) { + s1, err := open("../data/checker/response_optional_property_removed_revision.yaml") + require.Empty(t, err) + s2, err := open("../data/checker/response_optional_property_removed_base.yaml") + require.Empty(t, err) + + s2.Spec.Paths["/api/v1.0/groups"].Post.Responses["200"].Value.Content["application/json"].Schema.Value.Properties["data"].Value.Properties["id"].Value.WriteOnly = true + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.ResponseOptionalPropertyUpdatedCheck), d, osm, checker.INFO) + + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "response-optional-write-only-property-added", + Text: "added the optional write-only property 'data/id' to the response with the '200' status", + Level: checker.INFO, + Operation: "POST", + Path: "/api/v1.0/groups", + Source: "../data/checker/response_optional_property_removed_base.yaml", + OperationId: "createOneGroup", + }, + }, errs) +} diff --git a/checker/check-response-property-became-optional.go b/checker/check-response-property-became-optional.go index 07db4b28..1083467f 100644 --- a/checker/check-response-property-became-optional.go +++ b/checker/check-response-property-became-optional.go @@ -37,7 +37,7 @@ func ResponsePropertyBecameOptionalCheck(diffReport *diff.Diff, operationsSource if mediaTypeDiff.SchemaDiff.RequiredDiff != nil { for _, changedRequiredPropertyName := range mediaTypeDiff.SchemaDiff.RequiredDiff.Deleted { if mediaTypeDiff.SchemaDiff.Revision.Value.Properties[changedRequiredPropertyName] == nil { - // removed properties processed by the ResponseRequiredPropertyRemovedCheck check + // removed properties processed by the ResponseRequiredPropertyUpdatedCheck check continue } if mediaTypeDiff.SchemaDiff.Revision.Value.Properties[changedRequiredPropertyName].Value.WriteOnly { @@ -71,7 +71,7 @@ func ResponsePropertyBecameOptionalCheck(diffReport *diff.Diff, operationsSource continue } if propertyDiff.Revision.Value.Properties[changedRequiredPropertyName] == nil { - // removed properties processed by the ResponseRequiredPropertyRemovedCheck check + // removed properties processed by the ResponseRequiredPropertyUpdatedCheck check continue } result = append(result, BackwardCompatibilityError{ diff --git a/checker/check-response-property-type-changed_test.go b/checker/check-response-property-type-changed_test.go new file mode 100644 index 00000000..e4531b6d --- /dev/null +++ b/checker/check-response-property-type-changed_test.go @@ -0,0 +1,61 @@ +package checker_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tufin/oasdiff/checker" + "github.com/tufin/oasdiff/diff" +) + +// CL: Changing a response schema type +func TestResponseSchemaTypeChangedCheck(t *testing.T) { + s1, err := open("../data/checker/response_schema_type_changed_base.yaml") + require.Empty(t, err) + s2, err := open("../data/checker/response_schema_type_changed_revision.yaml") + require.Empty(t, err) + + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.ResponsePropertyTypeChangedCheck), d, osm, checker.ERR) + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "response-body-type-changed", + Text: "the response's body type/format changed from 'string'/'none' to 'object'/'none' for status '200'", + Comment: "", + Level: checker.ERR, + Operation: "POST", + Path: "/api/v1.0/groups", + Source: "../data/checker/response_schema_type_changed_revision.yaml", + OperationId: "createOneGroup", + }, + }, errs) +} + +// CL: Changing a response property schema type +func TestResponsePropertyTypeChangedCheck(t *testing.T) { + s1, err := open("../data/checker/response_schema_type_changed_revision.yaml") + require.Empty(t, err) + s2, err := open("../data/checker/response_schema_type_changed_revision.yaml") + require.Empty(t, err) + + s2.Spec.Paths["/api/v1.0/groups"].Post.Responses["200"].Value.Content["application/json"].Schema.Value.Properties["data"].Value.Properties["name"].Value.Type = "integer" + + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.ResponsePropertyTypeChangedCheck), d, osm, checker.ERR) + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "response-property-type-changed", + Text: "the response's property type/format changed from 'string'/'none' to 'integer'/'none' for status '200'", + Comment: "", + Level: checker.ERR, + Operation: "POST", + Path: "/api/v1.0/groups", + Source: "../data/checker/response_schema_type_changed_revision.yaml", + OperationId: "createOneGroup", + }, + }, errs) +} diff --git a/checker/check-response-required-property-became-non-write-only.go b/checker/check-response-required-property-became-non-write-only.go index 59f3a5d4..cf96d2b0 100644 --- a/checker/check-response-required-property-became-non-write-only.go +++ b/checker/check-response-required-property-became-non-write-only.go @@ -46,7 +46,7 @@ func ResponseRequiredPropertyBecameNonWriteOnlyCheck(diffReport *diff.Diff, oper return } if parent.Revision.Value.Properties[propertyName] == nil { - // removed properties processed by the ResponseRequiredPropertyRemovedCheck check + // removed properties processed by the ResponseRequiredPropertyUpdatedCheck check return } if !slices.Contains(parent.Base.Value.Required, propertyName) { diff --git a/checker/check-response-required-property-removed.go b/checker/check-response-required-property-removed.go deleted file mode 100644 index 90892de4..00000000 --- a/checker/check-response-required-property-removed.go +++ /dev/null @@ -1,59 +0,0 @@ -package checker - -import ( - "fmt" - - "github.com/getkin/kin-openapi/openapi3" - "github.com/tufin/oasdiff/diff" - "golang.org/x/exp/slices" -) - -func ResponseRequiredPropertyRemovedCheck(diffReport *diff.Diff, operationsSources *diff.OperationsSourcesMap, config BackwardCompatibilityCheckConfig) []BackwardCompatibilityError { - result := make([]BackwardCompatibilityError, 0) - if diffReport.PathsDiff == nil { - return result - } - for path, pathItem := range diffReport.PathsDiff.Modified { - if pathItem.OperationsDiff == nil { - continue - } - for operation, operationItem := range pathItem.OperationsDiff.Modified { - source := (*operationsSources)[operationItem.Revision] - - if operationItem.ResponsesDiff == nil { - continue - } - - for responseStatus, responseDiff := range operationItem.ResponsesDiff.Modified { - if responseDiff.ContentDiff == nil || - responseDiff.ContentDiff.MediaTypeModified == nil { - continue - } - - modifiedMediaTypes := responseDiff.ContentDiff.MediaTypeModified - for _, mediaTypeDiff := range modifiedMediaTypes { - CheckDeletedPropertiesDiff( - mediaTypeDiff.SchemaDiff, - func(propertyPath string, propertyName string, propertyItem *openapi3.Schema, parent *diff.SchemaDiff) { - if propertyItem.WriteOnly { - return - } - if !slices.Contains(parent.Base.Value.Required, propertyName) { - return - } - result = append(result, BackwardCompatibilityError{ - Id: "response-required-property-removed", - Level: ERR, - Text: fmt.Sprintf(config.i18n("response-required-property-removed"), ColorizedValue(propertyFullName(propertyPath, propertyName)), ColorizedValue(responseStatus)), - Operation: operation, - OperationId: operationItem.Revision.OperationID, - Path: path, - Source: source, - }) - }) - } - } - } - } - return result -} diff --git a/checker/check-response-required-property-updated.go b/checker/check-response-required-property-updated.go new file mode 100644 index 00000000..7e8ba560 --- /dev/null +++ b/checker/check-response-required-property-updated.go @@ -0,0 +1,91 @@ +package checker + +import ( + "fmt" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/tufin/oasdiff/diff" + "golang.org/x/exp/slices" +) + +const ( + ResponseRequiredPropertyRemovedCheckId = "response-required-property-removed" + ResponseRequiredWriteOnlyPropertyRemovedCheckId = "response-required-write-only-property-removed" + ResponseRequiredPropertyAddedCheckId = "response-required-property-added" + ResponseRequiredWriteOnlyPropertyAddedCheckId = "response-required-write-only-property-added" +) + +func ResponseRequiredPropertyUpdatedCheck(diffReport *diff.Diff, operationsSources *diff.OperationsSourcesMap, config BackwardCompatibilityCheckConfig) []BackwardCompatibilityError { + result := make([]BackwardCompatibilityError, 0) + if diffReport.PathsDiff == nil { + return result + } + for path, pathItem := range diffReport.PathsDiff.Modified { + if pathItem.OperationsDiff == nil { + continue + } + for operation, operationItem := range pathItem.OperationsDiff.Modified { + source := (*operationsSources)[operationItem.Revision] + + if operationItem.ResponsesDiff == nil { + continue + } + + for responseStatus, responseDiff := range operationItem.ResponsesDiff.Modified { + if responseDiff.ContentDiff == nil || + responseDiff.ContentDiff.MediaTypeModified == nil { + continue + } + + modifiedMediaTypes := responseDiff.ContentDiff.MediaTypeModified + for _, mediaTypeDiff := range modifiedMediaTypes { + CheckDeletedPropertiesDiff( + mediaTypeDiff.SchemaDiff, + func(propertyPath string, propertyName string, propertyItem *openapi3.Schema, parent *diff.SchemaDiff) { + level := ERR + id := ResponseRequiredPropertyRemovedCheckId + if propertyItem.WriteOnly { + level = INFO + id = ResponseRequiredWriteOnlyPropertyRemovedCheckId + } + if !slices.Contains(parent.Base.Value.Required, propertyName) { + // Covered by response-optional-property-removed + return + } + result = append(result, BackwardCompatibilityError{ + Id: id, + Level: level, + Text: fmt.Sprintf(config.i18n(id), ColorizedValue(propertyFullName(propertyPath, propertyName)), ColorizedValue(responseStatus)), + Operation: operation, + OperationId: operationItem.Revision.OperationID, + Path: path, + Source: source, + }) + }) + CheckAddedPropertiesDiff( + mediaTypeDiff.SchemaDiff, + func(propertyPath string, propertyName string, propertyItem *openapi3.Schema, parent *diff.SchemaDiff) { + id := ResponseRequiredPropertyAddedCheckId + if propertyItem.WriteOnly { + id = ResponseRequiredWriteOnlyPropertyAddedCheckId + } + if !slices.Contains(parent.Revision.Value.Required, propertyName) { + // Covered by response-optional-property-added + return + } + result = append(result, BackwardCompatibilityError{ + Id: id, + Level: INFO, + Text: fmt.Sprintf(config.i18n(id), ColorizedValue(propertyFullName(propertyPath, propertyName)), ColorizedValue(responseStatus)), + Operation: operation, + OperationId: operationItem.Revision.OperationID, + Path: path, + Source: source, + }) + }) + } + } + } + } + return result +} diff --git a/checker/check-response-required-property-updated_test.go b/checker/check-response-required-property-updated_test.go new file mode 100644 index 00000000..f7693ee9 --- /dev/null +++ b/checker/check-response-required-property-updated_test.go @@ -0,0 +1,107 @@ +package checker_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tufin/oasdiff/checker" + "github.com/tufin/oasdiff/diff" +) + +// CL: Adding a required property to response body is detected +func TestResponseRequiredPropertyAdded(t *testing.T) { + s1, _ := open("../data/checker/response_required_property_added_base.yaml") + s2, err := open("../data/checker/response_required_property_added_revision.yaml") + require.Empty(t, err) + + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.ResponseRequiredPropertyUpdatedCheck), d, osm, checker.INFO) + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "response-required-property-added", + Text: "added the required property 'data/new' to the response with the '200' status", + Comment: "", + Level: checker.INFO, + Operation: "POST", + Path: "/api/v1.0/groups", + Source: "../data/checker/response_required_property_added_revision.yaml", + OperationId: "createOneGroup", + }}, errs) +} + +// CL: Removing an existent property that was required in response body is detected +func TestResponseRequiredPropertyRemoved(t *testing.T) { + s1, _ := open("../data/checker/response_required_property_added_revision.yaml") + s2, err := open("../data/checker/response_required_property_added_base.yaml") + require.Empty(t, err) + + s2.Spec.Components.Schemas["GroupView"].Value.Properties["data"].Value.Required = []string{"name", "id"} + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.ResponseRequiredPropertyUpdatedCheck), d, osm, checker.INFO) + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "response-required-property-removed", + Text: "removed the required property 'data/new' from the response with the '200' status", + Comment: "", + Level: checker.ERR, + Operation: "POST", + Path: "/api/v1.0/groups", + Source: "../data/checker/response_required_property_added_base.yaml", + OperationId: "createOneGroup", + }}, errs) +} + +// CL: adding a required write-only property to response body is detected +func TestResponseRequiredWriteOnlyPropertyAdded(t *testing.T) { + s1, err := open("../data/checker/response_required_property_added_base.yaml") + require.Empty(t, err) + s2, err := open("../data/checker/response_required_property_added_revision.yaml") + require.Empty(t, err) + + s2.Spec.Components.Schemas["GroupView"].Value.Properties["data"].Value.Properties["new"].Value.WriteOnly = true + + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.ResponseRequiredPropertyUpdatedCheck), d, osm, checker.INFO) + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "response-required-write-only-property-added", + Text: "added the required write-only property 'data/new' to the response with the '200' status", + Comment: "", + Level: checker.INFO, + Operation: "POST", + Path: "/api/v1.0/groups", + Source: "../data/checker/response_required_property_added_revision.yaml", + OperationId: "createOneGroup", + }}, errs) +} + +// CL: removing a required write-only property that was required in response body is detected +func TestResponseRequiredWriteOnlyPropertyRemoved(t *testing.T) { + s1, _ := open("../data/checker/response_required_property_added_revision.yaml") + s2, err := open("../data/checker/response_required_property_added_base.yaml") + require.Empty(t, err) + + s1.Spec.Components.Schemas["GroupView"].Value.Properties["data"].Value.Properties["new"].Value.WriteOnly = true + s2.Spec.Components.Schemas["GroupView"].Value.Properties["data"].Value.Required = []string{"name", "id"} + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + errs := checker.CheckBackwardCompatibilityUntilLevel(singleCheckConfig(checker.ResponseRequiredPropertyUpdatedCheck), d, osm, checker.INFO) + require.NotEmpty(t, errs) + require.Equal(t, checker.BackwardCompatibilityErrors{ + { + Id: "response-required-write-only-property-removed", + Text: "removed the required write-only property 'data/new' from the response with the '200' status", + Comment: "", + Level: checker.INFO, + Operation: "POST", + Path: "/api/v1.0/groups", + Source: "../data/checker/response_required_property_added_base.yaml", + OperationId: "createOneGroup", + }}, errs) +} diff --git a/checker/checker_breaking_test.go b/checker/checker_breaking_test.go index daad1c20..83e73c57 100644 --- a/checker/checker_breaking_test.go +++ b/checker/checker_breaking_test.go @@ -296,6 +296,7 @@ func TestBreaking_OperationIdRemoved(t *testing.T) { require.NotEmpty(t, errs) require.Len(t, errs, 1) require.Equal(t, "api-operation-id-removed", errs[0].Id) + verifyNonBreakingChangeIsChangelogEntry(t, d, osm, "api-operation-id-removed") } // BC: removing/updating an enum in request body is breaking (optional) diff --git a/checker/checker_not_breaking_test.go b/checker/checker_not_breaking_test.go index 535ac0e5..c6b06b92 100644 --- a/checker/checker_not_breaking_test.go +++ b/checker/checker_not_breaking_test.go @@ -258,8 +258,8 @@ func TestBreaking_Servers(t *testing.T) { require.Empty(t, errs) } -// CL: adding a tag -func TestBreaking_TagAddedWithCustomCheck(t *testing.T) { +// BC: adding a tag is not breaking +func TestBreaking_TagAdded(t *testing.T) { s1 := l(t, 1) s2 := l(t, 1) @@ -269,7 +269,7 @@ func TestBreaking_TagAddedWithCustomCheck(t *testing.T) { verifyNonBreakingChangeIsChangelogEntry(t, d, osm, "api-tag-added") } -// CL: adding an operation ID +// BC: adding an operation ID is not breaking func TestBreaking_OperationIdAdded(t *testing.T) { s1 := l(t, 1) s2 := l(t, 1) @@ -280,3 +280,16 @@ func TestBreaking_OperationIdAdded(t *testing.T) { require.NoError(t, err) verifyNonBreakingChangeIsChangelogEntry(t, d, osm, "api-operation-id-added") } + +// BC: adding a required property to response is not breaking +func TestBreaking_RequiredResponsePropertyAdded(t *testing.T) { + s1, err := open("../data/checker/response_required_property_added_base.yaml") + require.NoError(t, err) + + s2, err := open("../data/checker/response_required_property_added_revision.yaml") + require.NoError(t, err) + + d, osm, err := diff.GetWithOperationsSourcesMap(getConfig(), s1, s2) + require.NoError(t, err) + verifyNonBreakingChangeIsChangelogEntry(t, d, osm, "response-required-property-added") +} diff --git a/checker/default_checks.go b/checker/default_checks.go index 9203f68b..515ef6fd 100644 --- a/checker/default_checks.go +++ b/checker/default_checks.go @@ -80,11 +80,11 @@ func defaultChecks() []BackwardCompatibilityCheck { ResponseHeaderBecameOptional, ResponseHeaderRemoved, ResponseSuccessStatusUpdated, - ResponseMediaTypeRemoved, + ResponseMediaTypeUpdated, NewRequestPathParameterCheck, NewRequestNonPathParameterCheck, NewRequiredRequestHeaderPropertyCheck, - ResponseRequiredPropertyRemovedCheck, + ResponseRequiredPropertyUpdatedCheck, UncheckedRequestAllOfWarnCheck, UncheckedResponseAllOfWarnCheck, RequestPropertyRemovedCheck, @@ -125,6 +125,8 @@ func defaultChecks() []BackwardCompatibilityCheck { ResponsePropertyMaxIncreasedCheck, ResponsePropertyMinDecreasedCheck, RequestParameterDefaultValueChanged, + APIComponentsSecurityUpdatedCheck, + APISecurityUpdatedCheck, } } diff --git a/checker/localizations/localizations.go b/checker/localizations/localizations.go index 44a14f6c..7a23b284 100644 --- a/checker/localizations/localizations.go +++ b/checker/localizations/localizations.go @@ -1,6 +1,6 @@ // Code generated by go-localize; DO NOT EDIT. // This file was generated by robots at -// 2023-06-14 11:19:39.638828 +0100 IST m=+0.005087918 +// 2023-06-29 18:16:42.541901 +0100 IST m=+0.004539542 package localizations @@ -14,6 +14,10 @@ import ( var localizations = map[string]string{ "en.messages.added-required-request-body": "added required request body", "en.messages.api-deprecated-sunset-parse": "api sunset date '%s' can't be parsed for deprecated API: %v", + "en.messages.api-global-security-added": "the security scheme %s was added to the API", + "en.messages.api-global-security-removed": "the security scheme %s was removed from the API", + "en.messages.api-global-security-scope-added": "the security scope %s was added to the global security scheme %s", + "en.messages.api-global-security-scope-removed": "the security scope %s was removed from the global security scheme %s", "en.messages.api-operation-id-added": "api operation id %s was added", "en.messages.api-operation-id-removed": "api operation id %s removed and replaced with %s", "en.messages.api-path-removed-before-sunset": "api path removed before the sunset date %s", @@ -21,6 +25,18 @@ var localizations = map[string]string{ "en.messages.api-removed-before-sunset": "api removed before the sunset date %s", "en.messages.api-removed-without-deprecation": "api removed without deprecation", "en.messages.api-schema-removed": "removed the schema %s from openapi components", + "en.messages.api-security-added": "the endpoint scheme security %s was added to the API", + "en.messages.api-security-component-added": "the component security scheme %s was added", + "en.messages.api-security-component-oauth-scope-added": "the component security scheme %s oauth scope %s was added", + "en.messages.api-security-component-oauth-scope-changed": "the component security scheme %s oauth scope %s was updated from %s to %s", + "en.messages.api-security-component-oauth-scope-removed": "the component security scheme %s oauth scope %s was removed", + "en.messages.api-security-component-oauth-url-changed": "the component security scheme %s oauth url changed from %s to %s", + "en.messages.api-security-component-removed": "the component security scheme %s was removed", + "en.messages.api-security-component-type-changed": "the component security scheme %s type changed from %s to %s", + "en.messages.api-security-removed": "the endpoint scheme security %s was removed from the API", + "en.messages.api-security-scope-added": "the security scope %s was added to the endpoint's security scheme %s", + "en.messages.api-security-scope-removed": "the security scope %s was removed from the endpoint's security scheme %s", + "en.messages.api-security-updated": "the endpoint scheme security %s was updated from %s to %s", "en.messages.api-sunset-date-changed-too-small": "api sunset date changed to earlier date from %s to %s, new sunset date must be not earlier than %s at least %d days from now", "en.messages.api-sunset-date-too-small": "api sunset date '%s' is too small, must be at least %d days from now", "en.messages.api-tag-added": "api tag %s added", @@ -113,11 +129,15 @@ var localizations = map[string]string{ "en.messages.response-body-min-length-decreased": "the response's body minLength was decreased from %s to %s", "en.messages.response-body-type-changed": "the response's body type/format changed from %s/%s to %s/%s for status %s", "en.messages.response-header-became-optional": "the response header %s became optional for the status %s", + "en.messages.response-media-type-added": "added the media type %s for the response with the status %s", "en.messages.response-media-type-removed": "removed the media type %s for the response with the status %s", "en.messages.response-mediatype-enum-value-removed": "response schema %s enum value removed %s", "en.messages.response-non-success-status-added": "added the non-success response with the status %s", "en.messages.response-non-success-status-removed": "removed the non-success response with the status %s", + "en.messages.response-optional-property-added": "added the optional property %s to the response with the %s status", "en.messages.response-optional-property-removed": "removed the optional property %s from the response with the %s status", + "en.messages.response-optional-write-only-property-added": "added the optional write-only property %s to the response with the %s status", + "en.messages.response-optional-write-only-property-removed": "removed the optional write-only property %s from the response with the %s status", "en.messages.response-property-became-nullable": "the response property %s became nullable for the status %s", "en.messages.response-property-became-optional": "the response property %s became optional for the status %s", "en.messages.response-property-enum-value-added": "added the new '%s' enum value the %s response property for the response status %s", @@ -131,15 +151,22 @@ var localizations = map[string]string{ "en.messages.response-property-min-items-unset": "the %s response property's minItems was unset from %s for the response status %s", "en.messages.response-property-min-length-decreased": "the %s response property's minLength was decreased from %s to %s for the response status %s", "en.messages.response-property-type-changed": "the response's property type/format changed from %s/%s to %s/%s for status %s", + "en.messages.response-required-property-added": "added the required property %s to the response with the %s status", "en.messages.response-required-property-became-not-write-only": "the response required property %s became not write-only for the status %s", "en.messages.response-required-property-became-not-write-only-comment": "It is valid only if the property was always returned before the specification has been changed", "en.messages.response-required-property-removed": "removed the required property %s from the response with the %s status", + "en.messages.response-required-write-only-property-added": "added the required write-only property %s to the response with the %s status", + "en.messages.response-required-write-only-property-removed": "removed the required write-only property %s from the response with the %s status", "en.messages.response-success-status-added": "added the success response with the status %s", "en.messages.response-success-status-removed": "removed the success response with the status %s", "en.messages.sunset-deleted": "api sunset date deleted, but deprecated=true kept", "en.messages.total-errors": "Backward compatibility errors (%d):\n", "ru.messages.added-required-request-body": "добавлено обязательное тело запроса", "ru.messages.api-deprecated-sunset-parse": "API deprecated без валидно парсящейся '%s' даты sunset: %v", + "ru.messages.api-global-security-added": "схема безопасности %s была добавлена к API", + "ru.messages.api-global-security-removed": "схема безопасности %s была удалена из API", + "ru.messages.api-global-security-scope-added": "к глобальной схеме безопасности %s была добавлена область безопасности %s", + "ru.messages.api-global-security-scope-removed": "из глобальной схемы безопасности %s была удалена область безопасности %s", "ru.messages.api-operation-id-added": "добавлен идентификатор операции API %s", "ru.messages.api-operation-id-removed": "Идентификатор операции API %s удален и заменен на %s", "ru.messages.api-path-added": "API path добавлено", @@ -150,6 +177,18 @@ var localizations = map[string]string{ "ru.messages.api-removed-before-sunset": "API удалёг до даты sunset %s", "ru.messages.api-removed-without-deprecation": "API удалён без deprecation", "ru.messages.api-schema-removed": "удалена схема %s из компонентов openapi", + "ru.messages.api-security-added": "схема безопасности точки доступа %s была добавлена к API", + "ru.messages.api-security-component-added": "компонент схемы безопасности %s был добавлен", + "ru.messages.api-security-component-oauth-scope-added": "добавлено разрешение OAuth %s для компонента схемы безопасности %s", + "ru.messages.api-security-component-oauth-scope-changed": "разрешение OAuth %s для компонента схемы безопасности %s было обновлено с %s на %s", + "ru.messages.api-security-component-oauth-scope-removed": "удалено разрешение OAuth %s для компонента схемы безопасности %s", + "ru.messages.api-security-component-oauth-url-changed": "URL OAuth компонента схемы безопасности %s был изменен с %s на %s", + "ru.messages.api-security-component-removed": "компонент схемы безопасности %s был удален", + "ru.messages.api-security-component-type-changed": "тип компонента схемы безопасности %s был изменен с %s на %s", + "ru.messages.api-security-removed": "схема безопасности точки доступа %s была удалена из API", + "ru.messages.api-security-scope-added": "к схеме безопасности эндпоинта %s была добавлена область безопасности %s", + "ru.messages.api-security-scope-removed": "из схемы безопасности эндпоинта %s была удалена область безопасности %s", + "ru.messages.api-security-updated": "схема безопасности точки доступа %s была обновлена с %s на %s", "ru.messages.api-sunset-date-changed-too-small": "дата sunset у API изменена на более раннюю с %s на %s, новая дата sunset должна быть либо не раньше %s, либо, как минимум, %d дней от текущего дня", "ru.messages.api-sunset-date-too-small": "дата API sunset date '%s' слишком ранняя, должно быть как минимум %d дней от текущего дня", "ru.messages.api-tag-added": "тег API %s добавлен", @@ -239,11 +278,15 @@ var localizations = map[string]string{ "ru.messages.response-body-min-length-decreased": "значение minLength для тела ответа уменьшено с %s до %s", "ru.messages.response-body-type-changed": "у тела ответа type/format изменился с %s/%s на %s/%s для ответа со статусом %s", "ru.messages.response-header-became-optional": "заголовок ответа %s стал необязательным для ответа со статусом %s", + "ru.messages.response-media-type-added": "добавлен тип медиа %s для ответа со статусом %s", "ru.messages.response-media-type-removed": "удалён media type %s для ответа со статусом %s", "ru.messages.response-mediatype-enum-value-removed": "значение перечисления схемы ответа %s удалено %s", "ru.messages.response-non-success-status-added": "добавлен ответ об отсутствии успеха со статусом %s", "ru.messages.response-non-success-status-removed": "удален неуспешный (не 2xx) статус ответа %s", + "ru.messages.response-optional-property-added": "добавлено необязательное свойство %s в ответе со статусом %s", "ru.messages.response-optional-property-removed": "удалено необязательное поле %s из ответа со статусом %s", + "ru.messages.response-optional-write-only-property-added": "добавлено необязательное свойство только для записи %s в ответе со статусом %s", + "ru.messages.response-optional-write-only-property-removed": "удалено необязательное свойство только для записи %s из ответа со статусом %s", "ru.messages.response-property-became-nullable": "поле ответа %s стало обнуляемым для ответа со статусом %s", "ru.messages.response-property-became-optional": "поле ответа %s стало необязательным для ответа со статусом %s", "ru.messages.response-property-enum-value-added": "добавлено новое enum значение %s в поле ответа %s для ответа со статусом %s", @@ -257,9 +300,12 @@ var localizations = map[string]string{ "ru.messages.response-property-min-items-unset": "у поля ответа %s удалено значение minItems, предыдущее значение - %s, для ответа со статусом %s", "ru.messages.response-property-min-length-decreased": "для поля ответа %s minLength уменьшен с %s до %s для ответа со статусом %s", "ru.messages.response-property-type-changed": "у поля type/format изменился с %s/%s на %s/%s для ответа со статусом %s", + "ru.messages.response-required-property-added": "добавил требуемое свойство %s в ответ со статусом %s", "ru.messages.response-required-property-became-not-write-only": "обязательное поле ответа %s перестало быть write-only для ответа со статусом %s", "ru.messages.response-required-property-became-not-write-only-comment": "Изменение допустимо только в том случае, если свойство ВСЕГДА возвращалось ДО изменения спецификации.", "ru.messages.response-required-property-removed": "удалено обязательное поле ответа %s из ответа со статусом %s", + "ru.messages.response-required-write-only-property-added": "добавлено обязательное свойство только для записи %s в ответе со статусом %s", + "ru.messages.response-required-write-only-property-removed": "удалено обязательное свойство только для записи %s из ответа со статусом %s", "ru.messages.response-success-status-added": "добавлен ответ об успехе со статусом %s", "ru.messages.response-success-status-removed": "удален успешный (2xx) статус ответа %s", "ru.messages.sunset-deleted": "удалена дата sunset date у API, но сохранён deprecated=true", diff --git a/checker/localizations_src/en/messages.yaml b/checker/localizations_src/en/messages.yaml index 651ec05b..ae20a49c 100644 --- a/checker/localizations_src/en/messages.yaml +++ b/checker/localizations_src/en/messages.yaml @@ -92,6 +92,7 @@ response-header-became-optional: the response header %s became optional for the required-response-header-removed: the mandatory response header %s removed for the status %s optional-response-header-removed: the optional response header %s removed for the status %s response-media-type-removed: removed the media type %s for the response with the status %s +response-media-type-added: added the media type %s for the response with the status %s response-optional-property-removed: removed the optional property %s from the response with the %s status response-property-became-optional: the response property %s became optional for the status %s response-property-became-nullable: the response property %s became nullable for the status %s @@ -114,6 +115,7 @@ response-property-type-changed: the response's property type/format changed from response-required-property-became-not-write-only: the response required property %s became not write-only for the status %s response-required-property-became-not-write-only-comment: It is valid only if the property was always returned before the specification has been changed response-required-property-removed: removed the required property %s from the response with the %s status +response-required-property-added: added the required property %s to the response with the %s status response-success-status-removed: removed the success response with the status %s response-non-success-status-removed: removed the non-success response with the status %s response-success-status-added: added the success response with the status %s @@ -124,3 +126,24 @@ response-body-max-increased: the response's body max was increased from %s to %s response-property-max-increased: the %s response property's max was increased from %s to %s for the response status %s response-body-min-decreased: the response's body min was decreased from %s to %s response-property-min-decreased: the %s response property's min was decreased from %s to %s for the response status %s +api-security-added: the endpoint scheme security %s was added to the API +api-security-removed: the endpoint scheme security %s was removed from the API +api-security-updated: the endpoint scheme security %s was updated from %s to %s +api-global-security-added: the security scheme %s was added to the API +api-global-security-removed: the security scheme %s was removed from the API +api-global-security-scope-removed: the security scope %s was removed from the global security scheme %s +api-global-security-scope-added: the security scope %s was added to the global security scheme %s +api-security-scope-removed: the security scope %s was removed from the endpoint's security scheme %s +api-security-scope-added: the security scope %s was added to the endpoint's security scheme %s +api-security-component-type-changed: the component security scheme %s type changed from %s to %s +api-security-component-oauth-url-changed: the component security scheme %s oauth url changed from %s to %s +api-security-component-added: the component security scheme %s was added +api-security-component-removed: the component security scheme %s was removed +api-security-component-oauth-scope-added: the component security scheme %s oauth scope %s was added +api-security-component-oauth-scope-removed: the component security scheme %s oauth scope %s was removed +api-security-component-oauth-scope-changed: the component security scheme %s oauth scope %s was updated from %s to %s +response-optional-property-added: added the optional property %s to the response with the %s status +response-optional-write-only-property-added: added the optional write-only property %s to the response with the %s status +response-optional-write-only-property-removed: removed the optional write-only property %s from the response with the %s status +response-required-write-only-property-added: added the required write-only property %s to the response with the %s status +response-required-write-only-property-removed: removed the required write-only property %s from the response with the %s status \ No newline at end of file diff --git a/checker/localizations_src/ru/messages.yaml b/checker/localizations_src/ru/messages.yaml index 241d78e9..1c041d9f 100644 --- a/checker/localizations_src/ru/messages.yaml +++ b/checker/localizations_src/ru/messages.yaml @@ -92,6 +92,7 @@ response-header-became-optional: заголовок ответа %s стал н required-response-header-removed: удалён ранее обязательный заголовок ответа %s для ответа со статусом %s optional-response-header-removed: удалён ранее необязательный заголовок ответа %s для ответа со статусом %s response-media-type-removed: удалён media type %s для ответа со статусом %s +response-media-type-added: добавлен тип медиа %s для ответа со статусом %s response-optional-property-removed: удалено необязательное поле %s из ответа со статусом %s response-property-became-optional: поле ответа %s стало необязательным для ответа со статусом %s response-property-became-nullable: поле ответа %s стало обнуляемым для ответа со статусом %s @@ -114,6 +115,7 @@ response-property-type-changed: у поля type/format изменился с %s response-required-property-became-not-write-only: обязательное поле ответа %s перестало быть write-only для ответа со статусом %s response-required-property-became-not-write-only-comment: Изменение допустимо только в том случае, если свойство ВСЕГДА возвращалось ДО изменения спецификации. response-required-property-removed: удалено обязательное поле ответа %s из ответа со статусом %s +response-required-property-added: добавил требуемое свойство %s в ответ со статусом %s response-success-status-removed: удален успешный (2xx) статус ответа %s response-non-success-status-removed: удален неуспешный (не 2xx) статус ответа %s request-allOf-modified: изменено allOf для поля запроса %s @@ -124,3 +126,24 @@ response-body-min-decreased: у тела ответа min уменьшено с response-property-min-decreased: для поля ответа %s min уменьшен с %s до %s для ответа со статусом %s response-success-status-added: добавлен ответ об успехе со статусом %s response-non-success-status-added: добавлен ответ об отсутствии успеха со статусом %s +api-security-added: схема безопасности точки доступа %s была добавлена к API +api-security-removed: схема безопасности точки доступа %s была удалена из API +api-security-updated: схема безопасности точки доступа %s была обновлена с %s на %s +api-global-security-added: схема безопасности %s была добавлена к API +api-global-security-removed: схема безопасности %s была удалена из API +api-global-security-scope-removed: из глобальной схемы безопасности %s была удалена область безопасности %s +api-global-security-scope-added: к глобальной схеме безопасности %s была добавлена область безопасности %s +api-security-scope-removed: из схемы безопасности эндпоинта %s была удалена область безопасности %s +api-security-scope-added: к схеме безопасности эндпоинта %s была добавлена область безопасности %s +api-security-component-type-changed: тип компонента схемы безопасности %s был изменен с %s на %s +api-security-component-oauth-url-changed: URL OAuth компонента схемы безопасности %s был изменен с %s на %s +api-security-component-added: компонент схемы безопасности %s был добавлен +api-security-component-removed: компонент схемы безопасности %s был удален +api-security-component-oauth-scope-added: добавлено разрешение OAuth %s для компонента схемы безопасности %s +api-security-component-oauth-scope-removed: удалено разрешение OAuth %s для компонента схемы безопасности %s +api-security-component-oauth-scope-changed: разрешение OAuth %s для компонента схемы безопасности %s было обновлено с %s на %s +response-optional-property-added: добавлено необязательное свойство %s в ответе со статусом %s +response-optional-write-only-property-added: добавлено необязательное свойство только для записи %s в ответе со статусом %s +response-optional-write-only-property-removed: удалено необязательное свойство только для записи %s из ответа со статусом %s +response-required-write-only-property-added: добавлено обязательное свойство только для записи %s в ответе со статусом %s +response-required-write-only-property-removed: удалено обязательное свойство только для записи %s из ответа со статусом %s \ No newline at end of file diff --git a/data/checker/add_new_media_type_base.yaml b/data/checker/add_new_media_type_base.yaml new file mode 100644 index 00000000..2740e8a2 --- /dev/null +++ b/data/checker/add_new_media_type_base.yaml @@ -0,0 +1,58 @@ +openapi: 3.0.1 +info: + title: Tufin + version: "2.0" +servers: +- url: https://localhost:9080 +paths: + /api/v1.0/groups: + post: + operationId: createOneGroup + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/GroupView' + description: Creates one project. + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GroupView' + description: OK + "409": + content: + application/json: + schema: + $ref: '#/components/schemas/GroupView' + description: Conflict + summary: Create One Project +components: + parameters: + groupId: + in: path + name: groupId + required: true + schema: + type: string + schemas: + GroupView: + type: object + properties: + data: + type: object + properties: + created: + type: string + format: date-time + readOnly: true + pattern: "^[a-z]+$" + id: + type: string + readOnly: true + name: + type: string + required: + - name \ No newline at end of file diff --git a/data/checker/add_new_media_type_revision.yaml b/data/checker/add_new_media_type_revision.yaml new file mode 100644 index 00000000..d2987a1f --- /dev/null +++ b/data/checker/add_new_media_type_revision.yaml @@ -0,0 +1,68 @@ +openapi: 3.0.1 +info: + title: Tufin + version: "2.0" +servers: +- url: https://localhost:9080 +paths: + /api/v1.0/groups: + post: + operationId: createOneGroup + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/GroupView' + description: Creates one project. + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GroupView' + application/xml: # Another media type + schema: + type: object + properties: + id: + type: integer + name: + type: string + fullTime: + type: boolean + description: OK + "409": + content: + application/json: + schema: + $ref: '#/components/schemas/GroupView' + description: Conflict + summary: Create One Project +components: + parameters: + groupId: + in: path + name: groupId + required: true + schema: + type: string + schemas: + GroupView: + type: object + properties: + data: + type: object + properties: + created: + type: string + format: date-time + readOnly: true + pattern: "^[a-z]+$" + id: + type: string + readOnly: true + name: + type: string + required: + - name \ No newline at end of file diff --git a/data/checker/api_security_added_base.yaml b/data/checker/api_security_added_base.yaml new file mode 100644 index 00000000..4cdf8bcb --- /dev/null +++ b/data/checker/api_security_added_base.yaml @@ -0,0 +1,33 @@ +openapi: 3.0.0 +info: + title: Security Requirement Example + version: 1.0.0 +paths: + /subscribe: + post: + requestBody: + content: + application/json: + schema: + type: object + properties: + inProgressUrl: + type: string + failedUrl: + type: string + successUrl: + type: string + responses: + "200": + description: OK +components: + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: http://example.org/api/oauth/dialog + scopes: + write:pets: modify pets in your account + read:pets: read your pets + \ No newline at end of file diff --git a/data/checker/api_security_added_revision.yaml b/data/checker/api_security_added_revision.yaml new file mode 100644 index 00000000..a9e1084c --- /dev/null +++ b/data/checker/api_security_added_revision.yaml @@ -0,0 +1,37 @@ +openapi: 3.0.0 +info: + title: Security Requirement Example + version: 1.0.0 +paths: + /subscribe: + post: + requestBody: + content: + application/json: + schema: + type: object + properties: + inProgressUrl: + type: string + failedUrl: + type: string + successUrl: + type: string + responses: + "200": + description: OK + security: + - petstore_auth: + - write:pets + - read:pets +components: + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: http://example.org/api/oauth/dialog + scopes: + write:pets: modify pets in your account + read:pets: read your pets + \ No newline at end of file diff --git a/data/checker/api_security_global_added_base.yaml b/data/checker/api_security_global_added_base.yaml new file mode 100644 index 00000000..4cdf8bcb --- /dev/null +++ b/data/checker/api_security_global_added_base.yaml @@ -0,0 +1,33 @@ +openapi: 3.0.0 +info: + title: Security Requirement Example + version: 1.0.0 +paths: + /subscribe: + post: + requestBody: + content: + application/json: + schema: + type: object + properties: + inProgressUrl: + type: string + failedUrl: + type: string + successUrl: + type: string + responses: + "200": + description: OK +components: + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: http://example.org/api/oauth/dialog + scopes: + write:pets: modify pets in your account + read:pets: read your pets + \ No newline at end of file diff --git a/data/checker/api_security_global_added_revision.yaml b/data/checker/api_security_global_added_revision.yaml new file mode 100644 index 00000000..fed79d89 --- /dev/null +++ b/data/checker/api_security_global_added_revision.yaml @@ -0,0 +1,37 @@ +openapi: 3.0.0 +info: + title: Security Requirement Example + version: 1.0.0 +security: +- petstore_auth: + - write:pets + - read:pets +paths: + /subscribe: + post: + requestBody: + content: + application/json: + schema: + type: object + properties: + inProgressUrl: + type: string + failedUrl: + type: string + successUrl: + type: string + responses: + "200": + description: OK +components: + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: http://example.org/api/oauth/dialog + scopes: + write:pets: modify pets in your account + read:pets: read your pets + \ No newline at end of file diff --git a/data/checker/api_security_updated_base.yaml b/data/checker/api_security_updated_base.yaml new file mode 100644 index 00000000..f0754390 --- /dev/null +++ b/data/checker/api_security_updated_base.yaml @@ -0,0 +1,46 @@ +openapi: 3.0.0 +info: + title: Security Requirement Example + version: 1.0.0 +paths: + /subscribe: + post: + requestBody: + content: + application/json: + schema: + type: object + properties: + inProgressUrl: + type: string + failedUrl: + type: string + successUrl: + type: string + responses: + "200": + description: OK + security: + - petstore_auth: + - write:pets + - read:pets +components: + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: http://example.org/api/oauth/dialog + scopes: + write:pets: modify pets in your account + read:pets: read your pets + catstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: http://example.org/api/oauth/dialog + scopes: + write:pets: modify pets in your account + read:pets: read your pets + + \ No newline at end of file diff --git a/data/checker/api_security_updated_revision.yaml b/data/checker/api_security_updated_revision.yaml new file mode 100644 index 00000000..93bd2530 --- /dev/null +++ b/data/checker/api_security_updated_revision.yaml @@ -0,0 +1,43 @@ +openapi: 3.0.0 +info: + title: Security Requirement Example + version: 1.0.0 +paths: + /subscribe: + post: + requestBody: + content: + application/json: + schema: + type: object + properties: + inProgressUrl: + type: string + failedUrl: + type: string + successUrl: + type: string + responses: + "200": + description: OK + security: + - petstore_auth: + - write:pets +components: + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: http://example.org/api/oauth/dialog + scopes: + write:pets: modify pets in your account + read:pets: read your pets + catstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: http://example.org/api/oauth/dialog + scopes: + write:pets: modify pets in your account + read:pets: read your pets diff --git a/data/checker/component_security_updated_base.yaml b/data/checker/component_security_updated_base.yaml new file mode 100644 index 00000000..05785e38 --- /dev/null +++ b/data/checker/component_security_updated_base.yaml @@ -0,0 +1,37 @@ +openapi: 3.0.0 +info: + title: Security Requirement Example + version: 1.0.0 +paths: + /subscribe: + post: + requestBody: + content: + application/json: + schema: + type: object + properties: + inProgressUrl: + type: string + failedUrl: + type: string + successUrl: + type: string + responses: + "200": + description: OK + security: + - petstore_auth: + - write:pets + - read:pets +components: + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: http://example.org/api/oauth/dialog + scopes: + write:pets: modify pets in your account + read:pets: read your pets + \ No newline at end of file diff --git a/data/checker/component_security_updated_revision.yaml b/data/checker/component_security_updated_revision.yaml new file mode 100644 index 00000000..8a370298 --- /dev/null +++ b/data/checker/component_security_updated_revision.yaml @@ -0,0 +1,40 @@ +openapi: 3.0.0 +info: + title: Security Requirement Example + version: 1.0.0 +paths: + /subscribe: + post: + requestBody: + content: + application/json: + schema: + type: object + properties: + inProgressUrl: + type: string + failedUrl: + type: string + successUrl: + type: string + responses: + "200": + description: OK + security: + - petstore_auth: + - write:pets + - read:pets +components: + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: http://example.org/api/oauth/dialog + scopes: + write:pets: modify pets in your account + read:pets: read your pets + BasicAuth: + type: http + scheme: basic + \ No newline at end of file diff --git a/data/checker/request_parameter_type_changed_base.yaml b/data/checker/request_parameter_type_changed_base.yaml new file mode 100644 index 00000000..c0333d48 --- /dev/null +++ b/data/checker/request_parameter_type_changed_base.yaml @@ -0,0 +1,72 @@ +openapi: 3.0.1 +info: + title: Tufin + version: "2.0" +servers: +- url: https://localhost:9080 +paths: + /api/v1.0/groups: + post: + operationId: createOneGroup + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/GroupView' + description: Creates one project. + required: true + parameters: + - in: path + name: groupId + required: true + schema: + type: string + - in: query + name: token + schema: + description: RFC 4122 UUID + example: 26734565-dbcc-449a-a370-0beaaf04b0e8 + format: uuid + pattern: ^(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12})$ + type: string + maxLength: 29 + - in: header + name: X-Request-ID + schema: + type: string + format: uuid + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GroupView' + description: OK + "409": + content: + application/json: + schema: + $ref: '#/components/schemas/GroupView' + description: Conflict + summary: Create One Project +components: + schemas: + GroupView: + type: object + properties: + data: + type: object + properties: + created: + type: string + format: date-time + readOnly: true + pattern: "^[a-z]+$" + id: + type: string + readOnly: true + name: + type: string + required: + - name \ No newline at end of file diff --git a/data/checker/response_optional_property_removed_base.yaml b/data/checker/response_optional_property_removed_base.yaml new file mode 100644 index 00000000..b4a51193 --- /dev/null +++ b/data/checker/response_optional_property_removed_base.yaml @@ -0,0 +1,45 @@ +openapi: 3.0.1 +info: + title: Tufin + version: "2.0" +servers: +- url: https://localhost:9080 +paths: + /api/v1.0/groups: + post: + operationId: createOneGroup + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GroupView' + description: OK + summary: Create One Project +components: + parameters: + groupId: + in: path + name: groupId + required: true + schema: + type: string + schemas: + GroupView: + type: object + properties: + data: + type: object + properties: + created: + type: string + format: date-time + readOnly: true + pattern: "^[a-z]+$" + id: + type: string + readOnly: true + name: + type: string + required: + - name \ No newline at end of file diff --git a/data/checker/response_optional_property_removed_revision.yaml b/data/checker/response_optional_property_removed_revision.yaml new file mode 100644 index 00000000..d53c03d6 --- /dev/null +++ b/data/checker/response_optional_property_removed_revision.yaml @@ -0,0 +1,42 @@ +openapi: 3.0.1 +info: + title: Tufin + version: "2.0" +servers: +- url: https://localhost:9080 +paths: + /api/v1.0/groups: + post: + operationId: createOneGroup + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GroupView' + description: OK + summary: Create One Project +components: + parameters: + groupId: + in: path + name: groupId + required: true + schema: + type: string + schemas: + GroupView: + type: object + properties: + data: + type: object + properties: + created: + type: string + format: date-time + readOnly: true + pattern: "^[a-z]+$" + name: + type: string + required: + - name \ No newline at end of file diff --git a/data/checker/response_required_property_added_base.yaml b/data/checker/response_required_property_added_base.yaml new file mode 100644 index 00000000..b4a51193 --- /dev/null +++ b/data/checker/response_required_property_added_base.yaml @@ -0,0 +1,45 @@ +openapi: 3.0.1 +info: + title: Tufin + version: "2.0" +servers: +- url: https://localhost:9080 +paths: + /api/v1.0/groups: + post: + operationId: createOneGroup + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GroupView' + description: OK + summary: Create One Project +components: + parameters: + groupId: + in: path + name: groupId + required: true + schema: + type: string + schemas: + GroupView: + type: object + properties: + data: + type: object + properties: + created: + type: string + format: date-time + readOnly: true + pattern: "^[a-z]+$" + id: + type: string + readOnly: true + name: + type: string + required: + - name \ No newline at end of file diff --git a/data/checker/response_required_property_added_revision.yaml b/data/checker/response_required_property_added_revision.yaml new file mode 100644 index 00000000..f66e7db2 --- /dev/null +++ b/data/checker/response_required_property_added_revision.yaml @@ -0,0 +1,49 @@ +openapi: 3.0.1 +info: + title: Tufin + version: "2.0" +servers: +- url: https://localhost:9080 +paths: + /api/v1.0/groups: + post: + operationId: createOneGroup + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GroupView' + description: OK + summary: Create One Project +components: + parameters: + groupId: + in: path + name: groupId + required: true + schema: + type: string + schemas: + GroupView: + type: object + properties: + data: + type: object + properties: + created: + type: string + format: date-time + readOnly: true + pattern: "^[a-z]+$" + id: + type: string + readOnly: true + name: + type: string + new: + type: string + required: + - name + - new + diff --git a/data/checker/response_schema_type_changed_base.yaml b/data/checker/response_schema_type_changed_base.yaml new file mode 100644 index 00000000..ea4f910f --- /dev/null +++ b/data/checker/response_schema_type_changed_base.yaml @@ -0,0 +1,29 @@ +openapi: 3.0.1 +info: + title: Tufin + version: "2.0" +servers: +- url: https://localhost:9080 +paths: + /api/v1.0/groups: + post: + operationId: createOneGroup + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GroupView' + description: OK + summary: Create One Project +components: + parameters: + groupId: + in: path + name: groupId + required: true + schema: + type: string + schemas: + GroupView: + type: string \ No newline at end of file diff --git a/data/checker/response_schema_type_changed_revision.yaml b/data/checker/response_schema_type_changed_revision.yaml new file mode 100644 index 00000000..0bce6d27 --- /dev/null +++ b/data/checker/response_schema_type_changed_revision.yaml @@ -0,0 +1,45 @@ +openapi: 3.0.1 +info: + title: Tufin + version: "2.0" +servers: +- url: https://localhost:9080 +paths: + /api/v1.0/groups: + post: + operationId: createOneGroup + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GroupView' + description: OK + summary: Create One Project +components: + parameters: + groupId: + in: path + name: groupId + required: true + schema: + type: string + schemas: + GroupView: + type: object + properties: + data: + type: string + properties: + created: + type: string + format: date-time + readOnly: true + pattern: "^[a-z]+$" + id: + type: string + readOnly: true + name: + type: string + required: + - name \ No newline at end of file