From 57bbfdaa18949e42837ee7c4c6a58efbd3fae725 Mon Sep 17 00:00:00 2001 From: Reuven Date: Sat, 18 Jun 2022 22:16:27 +0300 Subject: [PATCH 1/6] add direction to state --- diff/diff_breaking_test.go | 5 +++++ diff/required_diff.go | 13 ++++++++++--- diff/responses_diff.go | 4 ++++ diff/state.go | 13 +++++++++++++ 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/diff/diff_breaking_test.go b/diff/diff_breaking_test.go index 785d85fc..2c16b290 100644 --- a/diff/diff_breaking_test.go +++ b/diff/diff_breaking_test.go @@ -180,6 +180,7 @@ func TestBreaking_NewPathParam(t *testing.T) { }, s1, s2) require.NoError(t, err) + // new required path param breaks client require.Contains(t, d.PathsDiff.Modified[installCommandPath].OperationsDiff.Modified["GET"].ParametersDiff.Added[openapi3.ParameterInPath], "project") @@ -197,6 +198,7 @@ func TestBreaking_NewRequiredHeaderParam(t *testing.T) { }, s1, s2) require.NoError(t, err) + // new required header param breaks client require.Contains(t, d.PathsDiff.Modified[installCommandPath].OperationsDiff.Modified["GET"].ParametersDiff.Added[openapi3.ParameterInHeader], "network-policies") @@ -214,6 +216,7 @@ func TestBreaking_NewNonRequiredHeaderParam(t *testing.T) { }, s1, s2) require.NoError(t, err) + // new non-required header param doesn't break client require.Empty(t, d) } @@ -229,6 +232,7 @@ func TestBreaking_HeaderParamRequiredEnabled(t *testing.T) { }, s1, s2) require.NoError(t, err) + // changing an existing header param to required breaks client require.Equal(t, &diff.ValueDiff{ From: false, @@ -249,6 +253,7 @@ func TestBreaking_HeaderParamRequiredDisabled(t *testing.T) { }, s1, s2) require.NoError(t, err) + // changing an existing header param to non-required doesn't break client require.Empty(t, d) } diff --git a/diff/required_diff.go b/diff/required_diff.go index 77db0a8c..12d03da5 100644 --- a/diff/required_diff.go +++ b/diff/required_diff.go @@ -14,7 +14,7 @@ func (diff *RequiredPropertiesDiff) Empty() bool { return diff.StringsDiff.Empty() } -func (diff *RequiredPropertiesDiff) removeNonBreaking() { +func (diff *RequiredPropertiesDiff) removeNonBreaking(state *state) { if diff.Empty() { return } @@ -23,7 +23,14 @@ func (diff *RequiredPropertiesDiff) removeNonBreaking() { return } - diff.Deleted = nil + switch state.direction { + case directionRequest: + // if this is part of the request, then required properties can be deleted without breaking the client + diff.Deleted = nil + case directionResponse: + // if this is part of the response, then required properties can be added without breaking the client + diff.Added = nil + } } func getRequiredPropertiesDiff(config *Config, state *state, strings1, strings2 StringList) *RequiredPropertiesDiff { @@ -31,7 +38,7 @@ func getRequiredPropertiesDiff(config *Config, state *state, strings1, strings2 diff := getRequiredPropertiesDiffInternal(strings1, strings2) if config.BreakingOnly { - diff.removeNonBreaking() + diff.removeNonBreaking(state) } if diff.Empty() { diff --git a/diff/responses_diff.go b/diff/responses_diff.go index 16ebaafe..be5786d9 100644 --- a/diff/responses_diff.go +++ b/diff/responses_diff.go @@ -45,6 +45,10 @@ func newResponsesDiff() *ResponsesDiff { } func getResponsesDiff(config *Config, state *state, responses1, responses2 openapi3.Responses) (*ResponsesDiff, error) { + + defer state.setDirection(state.direction) + state.setDirection(directionResponse) + diff, err := getResponsesDiffInternal(config, state, responses1, responses2) if err != nil { return nil, err diff --git a/diff/state.go b/diff/state.go index 78977b30..7935c0c3 100644 --- a/diff/state.go +++ b/diff/state.go @@ -1,9 +1,17 @@ package diff +type direction int + +const ( + directionRequest direction = iota + directionResponse +) + type state struct { visitedSchemasBase visitedRefs visitedSchemasRevision visitedRefs cache schemaDiffCache + direction direction } func newState() *state { @@ -11,5 +19,10 @@ func newState() *state { visitedSchemasBase: visitedRefs{}, visitedSchemasRevision: visitedRefs{}, cache: schemaDiffCache{}, + direction: directionRequest, } } + +func (state *state) setDirection(direction direction) { + state.direction = direction +} From 7646c8e4357da3da06e267eaf382b3446e329b70 Mon Sep 17 00:00:00 2001 From: Reuven Date: Tue, 21 Jun 2022 10:03:38 +0300 Subject: [PATCH 2/6] add required-properies data files --- .../request-new-required-properties-base.yaml | 487 +++++++++++++++++ ...uest-new-required-properties-revision.yaml | 492 ++++++++++++++++++ .../request-required-properties-base.yaml | 492 ++++++++++++++++++ .../request-required-properties-revision.yaml | 491 +++++++++++++++++ ...response-new-required-properties-base.json | 101 ++++ ...onse-new-required-properties-revision.json | 105 ++++ .../response-required-properties-base.json | 105 ++++ ...response-required-properties-revision.json | 104 ++++ 8 files changed, 2377 insertions(+) create mode 100644 data/required-properties/request-new-required-properties-base.yaml create mode 100644 data/required-properties/request-new-required-properties-revision.yaml create mode 100644 data/required-properties/request-required-properties-base.yaml create mode 100644 data/required-properties/request-required-properties-revision.yaml create mode 100644 data/required-properties/response-new-required-properties-base.json create mode 100644 data/required-properties/response-new-required-properties-revision.json create mode 100644 data/required-properties/response-required-properties-base.json create mode 100644 data/required-properties/response-required-properties-revision.json diff --git a/data/required-properties/request-new-required-properties-base.yaml b/data/required-properties/request-new-required-properties-base.yaml new file mode 100644 index 00000000..548ecde3 --- /dev/null +++ b/data/required-properties/request-new-required-properties-base.yaml @@ -0,0 +1,487 @@ +openapi: 3.0.0 +info: + title: SmartChef API + version: 1.0.0 + +paths: + /products: + get: + summary: Returns a list of all products + tags: + - Products + parameters: + - $ref: '#/components/parameters/takeParam' + - $ref: '#/components/parameters/skipParam' + - in: query + name: gtin + schema: + type: string + maxLength: 13 + - in: query + name: name + schema: + type: string + - in: query + name: beschreibung + schema: + type: string + responses: + '200': + description: JSON Array of products + content: + application/json: + schema: + $ref: '#/components/schemas/ProductDto' + + post: + summary: Creates a new product + tags: + - Products + responses: + '201': + description: Returns the created product as JSON + content: + application/json: + schema: + $ref: '#/components/schemas/ProductDto' + '409': + description: GTIN already taken + + /products/{productId}: + get: + summary: Gets the product with the specified productId + parameters: + - in: path + name: productId + schema: + type: string + format: uuid + required: true + description: The UUID of the requested product + + tags: + - Products + responses: + '200': + description: Returns the requested product + content: + application/json: + schema: + $ref: '#/components/schemas/ProductDto' + '404': + description: Product with provided ID does not exist + + /products/{productId}/categories: + get: + summary: Gets the categories of the specified product + parameters: + - in: path + name: productId + schema: + type: string + format: uuid + required: true + description: The UUID of the requested product + + tags: + - Products + responses: + '200': + description: Returns the categories of the requested product + content: + application/json: + schema: + $ref: '#/components/schemas/ProductDto' + '404': + description: Product with provided ID does not exist + + '500': + description: Internal Server Error + '400': + description: Bad Request + + /users: + get: + tags: + - Users + summary: Gets all users + parameters: + - $ref: '#/components/parameters/takeParam' + - $ref: '#/components/parameters/skipParam' + - in: query + name: vorname + schema: + type: string + - in: query + name: nachname + schema: + type: string + responses: + '200': + description: Returns public users + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PublicUserDto' + + post: + tags: + - Users + summary: Creates a new user + requestBody: + description: Data for the new user + content: + application/json: + schema: + $ref: '#/components/schemas/CreateUserDto' + responses: + '201': + description: The created user + content: + application/json: + schema: + $ref: '#/components/schemas/PrivateUserDto' + '409': + description: Email already taken + + + /users/{userId}: + get: + tags: + - Users + summary: Gets the requested user + parameters: + - in: path + name: userId + schema: + type: string + format: uuid + required: true + description: The UUID of the requested user + responses: + '200': + description: Returns the requested user + content: + application/json: + schema: + $ref: '#/components/schemas/PublicUserDto' + + put: + tags: + - Users + summary: Updates a user + parameters: + - in: path + name: userId + schema: + type: string + format: uuid + required: true + description: The UUID of the user to be updated + requestBody: + description: User update data + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserDto' + responses: + '200': + description: Returns the updated user + content: + application/json: + schema: + $ref: '#/components/schemas/PublicUserDto' + + /auth/login: + post: + tags: + - Auth + summary: Logs in the user + requestBody: + description: User credentials + content: + application/json: + schema: + $ref: '#/components/schemas/LoginDto' + + responses: + '201': + description: Returns the created token pair + content: + application/json: + schema: + $ref: '#/components/schemas/TokenResponse' + '403': + description: Wrong email and/or password + + /auth/refresh: + post: + tags: + - Auth + summary: Gets a new access token using the provided refresh token + requestBody: + description: Refresh token + content: + application/json: + schema: + type: object + properties: + refreshToken: + type: string + format: jwt + responses: + '201': + description: Returns a new token pair + content: + application/json: + schema: + $ref: '#/components/schemas/TokenResponse' + + +components: + securitySchemes: + bearerAuth: # arbitrary name for the security scheme + type: http + scheme: bearer + bearerFormat: JWT + + + schemas: + ResourceDto: + type: object + properties: + id: + type: string + format: uuid + createdAt: + type: string + format: datetime + updatedAt: + type: string + format: datetime + + ProductDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - type: object + properties: + gtin: + type: string + name: + type: string + beschreibung: + type: string + menge: + type: number + minimum: 0 + einheit: + type: string + kategorieId: + type: string + format: uuid + herstellerId: + type: string + format: uuid + + TokenResponse: + type: object + properties: + accessToken: + type: string + format: jwt + refreshToken: + type: string + format: jwt + + PublicUserDto: + type: object + properties: + id: + type: string + format: uuid + vorname: + type: string + maxLength: 255 + nachname: + type: string + maxLength: 255 + + PrivateUserDto: + type: object + allOf: + - $ref: '#/components/schemas/PublicUserDto' + - type: object + properties: + email: + type: string + format: email + maxLength: 255 + + LoginDto: + type: object + properties: + email: + type: string + format: email + maxLength: 255 + + password: + type: string + maxLength: 255 + + HaushaltDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - type: object + properties: + name: + type: string + maxLength: 255 + + HaushaltProduktDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - $ref: '#/components/schemas/ProductDto' + - type: object + properties: + haushaltId: + type: string + format: uuid + ist: + type: number + soll: + type: number + + HerstellerDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - type: object + properties: + name: + type: string + + ProduktKategorieDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - type: object + properties: + name: + type: string + maxLength: 255 + parentId: + type: string + format: uuid + + RezeptDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - type: object + properties: + name: + type: string + maxLength: 255 + beschreibung: + type: string + maxLength: 255 + anleitungText: + type: string + maxLength: 4000 + url: + type: string + format: url + maxLength: 1024 + kategorieId: + type: string + format: string + + RezeptKategorieDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - type: object + properties: + name: + type: string + parentId: + type: string + format: uuid + + ZutatenDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - type: object + properties: + produktKategorieId: + type: string + maxLength: 255 + menge: + type: number + minimum: 0 + einheit: + type: string + maxLength: 255 + rezeptId: + type: string + format: uuid + + CreateUserDto: + type: object + required: + - password + - vorname + - nachname + properties: + password: + type: string + minLength: 8 + maxLength: 255 + vorname: + type: string + maxLength: 255 + nachname: + type: string + maxLength: 255 + + UpdateUserDto: + type: object + properties: + password: + type: string + maxLength: 255 + vorname: + type: string + maxLength: 255 + nachname: + type: string + maxLength: 255 + + parameters: + takeParam: + in: query + name: take + schema: + type: integer + minimum: 1 + maximum: 50 + description: The amount of users to return + skipParam: + in: query + name: skip + schema: + type: integer + minimum: 1 + maximum: 50 + description: The amount of users to skip + + +security: + - bearerAuth: [] \ No newline at end of file diff --git a/data/required-properties/request-new-required-properties-revision.yaml b/data/required-properties/request-new-required-properties-revision.yaml new file mode 100644 index 00000000..84f608c5 --- /dev/null +++ b/data/required-properties/request-new-required-properties-revision.yaml @@ -0,0 +1,492 @@ +openapi: 3.0.0 +info: + title: SmartChef API + version: 1.0.0 + +paths: + /products: + get: + summary: Returns a list of all products + tags: + - Products + parameters: + - $ref: '#/components/parameters/takeParam' + - $ref: '#/components/parameters/skipParam' + - in: query + name: gtin + schema: + type: string + maxLength: 13 + - in: query + name: name + schema: + type: string + - in: query + name: beschreibung + schema: + type: string + responses: + '200': + description: JSON Array of products + content: + application/json: + schema: + $ref: '#/components/schemas/ProductDto' + + post: + summary: Creates a new product + tags: + - Products + responses: + '201': + description: Returns the created product as JSON + content: + application/json: + schema: + $ref: '#/components/schemas/ProductDto' + '409': + description: GTIN already taken + + /products/{productId}: + get: + summary: Gets the product with the specified productId + parameters: + - in: path + name: productId + schema: + type: string + format: uuid + required: true + description: The UUID of the requested product + + tags: + - Products + responses: + '200': + description: Returns the requested product + content: + application/json: + schema: + $ref: '#/components/schemas/ProductDto' + '404': + description: Product with provided ID does not exist + + /products/{productId}/categories: + get: + summary: Gets the categories of the specified product + parameters: + - in: path + name: productId + schema: + type: string + format: uuid + required: true + description: The UUID of the requested product + + tags: + - Products + responses: + '200': + description: Returns the categories of the requested product + content: + application/json: + schema: + $ref: '#/components/schemas/ProductDto' + '404': + description: Product with provided ID does not exist + + '500': + description: Internal Server Error + '400': + description: Bad Request + + /users: + get: + tags: + - Users + summary: Gets all users + parameters: + - $ref: '#/components/parameters/takeParam' + - $ref: '#/components/parameters/skipParam' + - in: query + name: vorname + schema: + type: string + - in: query + name: nachname + schema: + type: string + responses: + '200': + description: Returns public users + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PublicUserDto' + + post: + tags: + - Users + summary: Creates a new user + requestBody: + description: Data for the new user + content: + application/json: + schema: + $ref: '#/components/schemas/CreateUserDto' + responses: + '201': + description: The created user + content: + application/json: + schema: + $ref: '#/components/schemas/PrivateUserDto' + '409': + description: Email already taken + + + /users/{userId}: + get: + tags: + - Users + summary: Gets the requested user + parameters: + - in: path + name: userId + schema: + type: string + format: uuid + required: true + description: The UUID of the requested user + responses: + '200': + description: Returns the requested user + content: + application/json: + schema: + $ref: '#/components/schemas/PublicUserDto' + + put: + tags: + - Users + summary: Updates a user + parameters: + - in: path + name: userId + schema: + type: string + format: uuid + required: true + description: The UUID of the user to be updated + requestBody: + description: User update data + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserDto' + responses: + '200': + description: Returns the updated user + content: + application/json: + schema: + $ref: '#/components/schemas/PublicUserDto' + + /auth/login: + post: + tags: + - Auth + summary: Logs in the user + requestBody: + description: User credentials + content: + application/json: + schema: + $ref: '#/components/schemas/LoginDto' + + responses: + '201': + description: Returns the created token pair + content: + application/json: + schema: + $ref: '#/components/schemas/TokenResponse' + '403': + description: Wrong email and/or password + + /auth/refresh: + post: + tags: + - Auth + summary: Gets a new access token using the provided refresh token + requestBody: + description: Refresh token + content: + application/json: + schema: + type: object + properties: + refreshToken: + type: string + format: jwt + responses: + '201': + description: Returns a new token pair + content: + application/json: + schema: + $ref: '#/components/schemas/TokenResponse' + + +components: + securitySchemes: + bearerAuth: # arbitrary name for the security scheme + type: http + scheme: bearer + bearerFormat: JWT + + + schemas: + ResourceDto: + type: object + properties: + id: + type: string + format: uuid + createdAt: + type: string + format: datetime + updatedAt: + type: string + format: datetime + + ProductDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - type: object + properties: + gtin: + type: string + name: + type: string + beschreibung: + type: string + menge: + type: number + minimum: 0 + einheit: + type: string + kategorieId: + type: string + format: uuid + herstellerId: + type: string + format: uuid + + TokenResponse: + type: object + properties: + accessToken: + type: string + format: jwt + refreshToken: + type: string + format: jwt + + PublicUserDto: + type: object + properties: + id: + type: string + format: uuid + vorname: + type: string + maxLength: 255 + nachname: + type: string + maxLength: 255 + + PrivateUserDto: + type: object + allOf: + - $ref: '#/components/schemas/PublicUserDto' + - type: object + properties: + email: + type: string + format: email + maxLength: 255 + + LoginDto: + type: object + properties: + email: + type: string + format: email + maxLength: 255 + + password: + type: string + maxLength: 255 + + HaushaltDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - type: object + properties: + name: + type: string + maxLength: 255 + + HaushaltProduktDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - $ref: '#/components/schemas/ProductDto' + - type: object + properties: + haushaltId: + type: string + format: uuid + ist: + type: number + soll: + type: number + + HerstellerDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - type: object + properties: + name: + type: string + + ProduktKategorieDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - type: object + properties: + name: + type: string + maxLength: 255 + parentId: + type: string + format: uuid + + RezeptDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - type: object + properties: + name: + type: string + maxLength: 255 + beschreibung: + type: string + maxLength: 255 + anleitungText: + type: string + maxLength: 4000 + url: + type: string + format: url + maxLength: 1024 + kategorieId: + type: string + format: string + + RezeptKategorieDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - type: object + properties: + name: + type: string + parentId: + type: string + format: uuid + + ZutatenDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - type: object + properties: + produktKategorieId: + type: string + maxLength: 255 + menge: + type: number + minimum: 0 + einheit: + type: string + maxLength: 255 + rezeptId: + type: string + format: uuid + + CreateUserDto: + type: object + required: + - email + - password + - vorname + - nachname + properties: + email: + type: string + format: email + maxLength: 255 + password: + type: string + minLength: 8 + maxLength: 255 + vorname: + type: string + maxLength: 255 + nachname: + type: string + maxLength: 255 + + UpdateUserDto: + type: object + properties: + password: + type: string + maxLength: 255 + vorname: + type: string + maxLength: 255 + nachname: + type: string + maxLength: 255 + + parameters: + takeParam: + in: query + name: take + schema: + type: integer + minimum: 1 + maximum: 50 + description: The amount of users to return + skipParam: + in: query + name: skip + schema: + type: integer + minimum: 1 + maximum: 50 + description: The amount of users to skip + + +security: + - bearerAuth: [] \ No newline at end of file diff --git a/data/required-properties/request-required-properties-base.yaml b/data/required-properties/request-required-properties-base.yaml new file mode 100644 index 00000000..84f608c5 --- /dev/null +++ b/data/required-properties/request-required-properties-base.yaml @@ -0,0 +1,492 @@ +openapi: 3.0.0 +info: + title: SmartChef API + version: 1.0.0 + +paths: + /products: + get: + summary: Returns a list of all products + tags: + - Products + parameters: + - $ref: '#/components/parameters/takeParam' + - $ref: '#/components/parameters/skipParam' + - in: query + name: gtin + schema: + type: string + maxLength: 13 + - in: query + name: name + schema: + type: string + - in: query + name: beschreibung + schema: + type: string + responses: + '200': + description: JSON Array of products + content: + application/json: + schema: + $ref: '#/components/schemas/ProductDto' + + post: + summary: Creates a new product + tags: + - Products + responses: + '201': + description: Returns the created product as JSON + content: + application/json: + schema: + $ref: '#/components/schemas/ProductDto' + '409': + description: GTIN already taken + + /products/{productId}: + get: + summary: Gets the product with the specified productId + parameters: + - in: path + name: productId + schema: + type: string + format: uuid + required: true + description: The UUID of the requested product + + tags: + - Products + responses: + '200': + description: Returns the requested product + content: + application/json: + schema: + $ref: '#/components/schemas/ProductDto' + '404': + description: Product with provided ID does not exist + + /products/{productId}/categories: + get: + summary: Gets the categories of the specified product + parameters: + - in: path + name: productId + schema: + type: string + format: uuid + required: true + description: The UUID of the requested product + + tags: + - Products + responses: + '200': + description: Returns the categories of the requested product + content: + application/json: + schema: + $ref: '#/components/schemas/ProductDto' + '404': + description: Product with provided ID does not exist + + '500': + description: Internal Server Error + '400': + description: Bad Request + + /users: + get: + tags: + - Users + summary: Gets all users + parameters: + - $ref: '#/components/parameters/takeParam' + - $ref: '#/components/parameters/skipParam' + - in: query + name: vorname + schema: + type: string + - in: query + name: nachname + schema: + type: string + responses: + '200': + description: Returns public users + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PublicUserDto' + + post: + tags: + - Users + summary: Creates a new user + requestBody: + description: Data for the new user + content: + application/json: + schema: + $ref: '#/components/schemas/CreateUserDto' + responses: + '201': + description: The created user + content: + application/json: + schema: + $ref: '#/components/schemas/PrivateUserDto' + '409': + description: Email already taken + + + /users/{userId}: + get: + tags: + - Users + summary: Gets the requested user + parameters: + - in: path + name: userId + schema: + type: string + format: uuid + required: true + description: The UUID of the requested user + responses: + '200': + description: Returns the requested user + content: + application/json: + schema: + $ref: '#/components/schemas/PublicUserDto' + + put: + tags: + - Users + summary: Updates a user + parameters: + - in: path + name: userId + schema: + type: string + format: uuid + required: true + description: The UUID of the user to be updated + requestBody: + description: User update data + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserDto' + responses: + '200': + description: Returns the updated user + content: + application/json: + schema: + $ref: '#/components/schemas/PublicUserDto' + + /auth/login: + post: + tags: + - Auth + summary: Logs in the user + requestBody: + description: User credentials + content: + application/json: + schema: + $ref: '#/components/schemas/LoginDto' + + responses: + '201': + description: Returns the created token pair + content: + application/json: + schema: + $ref: '#/components/schemas/TokenResponse' + '403': + description: Wrong email and/or password + + /auth/refresh: + post: + tags: + - Auth + summary: Gets a new access token using the provided refresh token + requestBody: + description: Refresh token + content: + application/json: + schema: + type: object + properties: + refreshToken: + type: string + format: jwt + responses: + '201': + description: Returns a new token pair + content: + application/json: + schema: + $ref: '#/components/schemas/TokenResponse' + + +components: + securitySchemes: + bearerAuth: # arbitrary name for the security scheme + type: http + scheme: bearer + bearerFormat: JWT + + + schemas: + ResourceDto: + type: object + properties: + id: + type: string + format: uuid + createdAt: + type: string + format: datetime + updatedAt: + type: string + format: datetime + + ProductDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - type: object + properties: + gtin: + type: string + name: + type: string + beschreibung: + type: string + menge: + type: number + minimum: 0 + einheit: + type: string + kategorieId: + type: string + format: uuid + herstellerId: + type: string + format: uuid + + TokenResponse: + type: object + properties: + accessToken: + type: string + format: jwt + refreshToken: + type: string + format: jwt + + PublicUserDto: + type: object + properties: + id: + type: string + format: uuid + vorname: + type: string + maxLength: 255 + nachname: + type: string + maxLength: 255 + + PrivateUserDto: + type: object + allOf: + - $ref: '#/components/schemas/PublicUserDto' + - type: object + properties: + email: + type: string + format: email + maxLength: 255 + + LoginDto: + type: object + properties: + email: + type: string + format: email + maxLength: 255 + + password: + type: string + maxLength: 255 + + HaushaltDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - type: object + properties: + name: + type: string + maxLength: 255 + + HaushaltProduktDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - $ref: '#/components/schemas/ProductDto' + - type: object + properties: + haushaltId: + type: string + format: uuid + ist: + type: number + soll: + type: number + + HerstellerDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - type: object + properties: + name: + type: string + + ProduktKategorieDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - type: object + properties: + name: + type: string + maxLength: 255 + parentId: + type: string + format: uuid + + RezeptDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - type: object + properties: + name: + type: string + maxLength: 255 + beschreibung: + type: string + maxLength: 255 + anleitungText: + type: string + maxLength: 4000 + url: + type: string + format: url + maxLength: 1024 + kategorieId: + type: string + format: string + + RezeptKategorieDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - type: object + properties: + name: + type: string + parentId: + type: string + format: uuid + + ZutatenDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - type: object + properties: + produktKategorieId: + type: string + maxLength: 255 + menge: + type: number + minimum: 0 + einheit: + type: string + maxLength: 255 + rezeptId: + type: string + format: uuid + + CreateUserDto: + type: object + required: + - email + - password + - vorname + - nachname + properties: + email: + type: string + format: email + maxLength: 255 + password: + type: string + minLength: 8 + maxLength: 255 + vorname: + type: string + maxLength: 255 + nachname: + type: string + maxLength: 255 + + UpdateUserDto: + type: object + properties: + password: + type: string + maxLength: 255 + vorname: + type: string + maxLength: 255 + nachname: + type: string + maxLength: 255 + + parameters: + takeParam: + in: query + name: take + schema: + type: integer + minimum: 1 + maximum: 50 + description: The amount of users to return + skipParam: + in: query + name: skip + schema: + type: integer + minimum: 1 + maximum: 50 + description: The amount of users to skip + + +security: + - bearerAuth: [] \ No newline at end of file diff --git a/data/required-properties/request-required-properties-revision.yaml b/data/required-properties/request-required-properties-revision.yaml new file mode 100644 index 00000000..5f268617 --- /dev/null +++ b/data/required-properties/request-required-properties-revision.yaml @@ -0,0 +1,491 @@ +openapi: 3.0.0 +info: + title: SmartChef API + version: 1.0.0 + +paths: + /products: + get: + summary: Returns a list of all products + tags: + - Products + parameters: + - $ref: '#/components/parameters/takeParam' + - $ref: '#/components/parameters/skipParam' + - in: query + name: gtin + schema: + type: string + maxLength: 13 + - in: query + name: name + schema: + type: string + - in: query + name: beschreibung + schema: + type: string + responses: + '200': + description: JSON Array of products + content: + application/json: + schema: + $ref: '#/components/schemas/ProductDto' + + post: + summary: Creates a new product + tags: + - Products + responses: + '201': + description: Returns the created product as JSON + content: + application/json: + schema: + $ref: '#/components/schemas/ProductDto' + '409': + description: GTIN already taken + + /products/{productId}: + get: + summary: Gets the product with the specified productId + parameters: + - in: path + name: productId + schema: + type: string + format: uuid + required: true + description: The UUID of the requested product + + tags: + - Products + responses: + '200': + description: Returns the requested product + content: + application/json: + schema: + $ref: '#/components/schemas/ProductDto' + '404': + description: Product with provided ID does not exist + + /products/{productId}/categories: + get: + summary: Gets the categories of the specified product + parameters: + - in: path + name: productId + schema: + type: string + format: uuid + required: true + description: The UUID of the requested product + + tags: + - Products + responses: + '200': + description: Returns the categories of the requested product + content: + application/json: + schema: + $ref: '#/components/schemas/ProductDto' + '404': + description: Product with provided ID does not exist + + '500': + description: Internal Server Error + '400': + description: Bad Request + + /users: + get: + tags: + - Users + summary: Gets all users + parameters: + - $ref: '#/components/parameters/takeParam' + - $ref: '#/components/parameters/skipParam' + - in: query + name: vorname + schema: + type: string + - in: query + name: nachname + schema: + type: string + responses: + '200': + description: Returns public users + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PublicUserDto' + + post: + tags: + - Users + summary: Creates a new user + requestBody: + description: Data for the new user + content: + application/json: + schema: + $ref: '#/components/schemas/CreateUserDto' + responses: + '201': + description: The created user + content: + application/json: + schema: + $ref: '#/components/schemas/PrivateUserDto' + '409': + description: Email already taken + + + /users/{userId}: + get: + tags: + - Users + summary: Gets the requested user + parameters: + - in: path + name: userId + schema: + type: string + format: uuid + required: true + description: The UUID of the requested user + responses: + '200': + description: Returns the requested user + content: + application/json: + schema: + $ref: '#/components/schemas/PublicUserDto' + + put: + tags: + - Users + summary: Updates a user + parameters: + - in: path + name: userId + schema: + type: string + format: uuid + required: true + description: The UUID of the user to be updated + requestBody: + description: User update data + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserDto' + responses: + '200': + description: Returns the updated user + content: + application/json: + schema: + $ref: '#/components/schemas/PublicUserDto' + + /auth/login: + post: + tags: + - Auth + summary: Logs in the user + requestBody: + description: User credentials + content: + application/json: + schema: + $ref: '#/components/schemas/LoginDto' + + responses: + '201': + description: Returns the created token pair + content: + application/json: + schema: + $ref: '#/components/schemas/TokenResponse' + '403': + description: Wrong email and/or password + + /auth/refresh: + post: + tags: + - Auth + summary: Gets a new access token using the provided refresh token + requestBody: + description: Refresh token + content: + application/json: + schema: + type: object + properties: + refreshToken: + type: string + format: jwt + responses: + '201': + description: Returns a new token pair + content: + application/json: + schema: + $ref: '#/components/schemas/TokenResponse' + + +components: + securitySchemes: + bearerAuth: # arbitrary name for the security scheme + type: http + scheme: bearer + bearerFormat: JWT + + + schemas: + ResourceDto: + type: object + properties: + id: + type: string + format: uuid + createdAt: + type: string + format: datetime + updatedAt: + type: string + format: datetime + + ProductDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - type: object + properties: + gtin: + type: string + name: + type: string + beschreibung: + type: string + menge: + type: number + minimum: 0 + einheit: + type: string + kategorieId: + type: string + format: uuid + herstellerId: + type: string + format: uuid + + TokenResponse: + type: object + properties: + accessToken: + type: string + format: jwt + refreshToken: + type: string + format: jwt + + PublicUserDto: + type: object + properties: + id: + type: string + format: uuid + vorname: + type: string + maxLength: 255 + nachname: + type: string + maxLength: 255 + + PrivateUserDto: + type: object + allOf: + - $ref: '#/components/schemas/PublicUserDto' + - type: object + properties: + email: + type: string + format: email + maxLength: 255 + + LoginDto: + type: object + properties: + email: + type: string + format: email + maxLength: 255 + + password: + type: string + maxLength: 255 + + HaushaltDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - type: object + properties: + name: + type: string + maxLength: 255 + + HaushaltProduktDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - $ref: '#/components/schemas/ProductDto' + - type: object + properties: + haushaltId: + type: string + format: uuid + ist: + type: number + soll: + type: number + + HerstellerDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - type: object + properties: + name: + type: string + + ProduktKategorieDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - type: object + properties: + name: + type: string + maxLength: 255 + parentId: + type: string + format: uuid + + RezeptDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - type: object + properties: + name: + type: string + maxLength: 255 + beschreibung: + type: string + maxLength: 255 + anleitungText: + type: string + maxLength: 4000 + url: + type: string + format: url + maxLength: 1024 + kategorieId: + type: string + format: string + + RezeptKategorieDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - type: object + properties: + name: + type: string + parentId: + type: string + format: uuid + + ZutatenDto: + type: object + allOf: + - $ref: '#/components/schemas/ResourceDto' + - type: object + properties: + produktKategorieId: + type: string + maxLength: 255 + menge: + type: number + minimum: 0 + einheit: + type: string + maxLength: 255 + rezeptId: + type: string + format: uuid + + CreateUserDto: + type: object + required: + - password + - vorname + - nachname + properties: + email: + type: string + format: email + maxLength: 255 + password: + type: string + minLength: 8 + maxLength: 255 + vorname: + type: string + maxLength: 255 + nachname: + type: string + maxLength: 255 + + UpdateUserDto: + type: object + properties: + password: + type: string + maxLength: 255 + vorname: + type: string + maxLength: 255 + nachname: + type: string + maxLength: 255 + + parameters: + takeParam: + in: query + name: take + schema: + type: integer + minimum: 1 + maximum: 50 + description: The amount of users to return + skipParam: + in: query + name: skip + schema: + type: integer + minimum: 1 + maximum: 50 + description: The amount of users to skip + + +security: + - bearerAuth: [] \ No newline at end of file diff --git a/data/required-properties/response-new-required-properties-base.json b/data/required-properties/response-new-required-properties-base.json new file mode 100644 index 00000000..f1db7dd0 --- /dev/null +++ b/data/required-properties/response-new-required-properties-base.json @@ -0,0 +1,101 @@ +{ + "info": { + "title": "title", + "version": "1.0.102" + }, + "openapi": "3.0.1", + "paths": { + "/web/search": { + "get": { + "operationId": "searchArticles", + "parameters": [ + { + "in": "header", + "name": "user-agent", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "*/*": { + "schema": { + "properties": { + "helpAndSupport": { + "properties": { + "expandLink": { + "properties": { + "title": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "title", + "url" + ], + "type": "object" + }, + "pages": { + "items": { + "properties": { + "contentPreview": { + "type": "string" + }, + "id": { + "type": "string" + }, + "image": { + "type": "string" + }, + "title": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "totalResultsCount": { + "format": "int32", + "type": "integer" + } + }, + "required": [ + "expandLink", + "pages", + "totalResultsCount" + ], + "type": "object" + } + }, + "required": [ + "helpAndSupport" + ], + "type": "object" + } + } + }, + "description": "OK" + } + }, + "tags": [ + "web-search-controller" + ] + } + } + }, + "servers": [ + { + "url": "/api/" + } + ] +} \ No newline at end of file diff --git a/data/required-properties/response-new-required-properties-revision.json b/data/required-properties/response-new-required-properties-revision.json new file mode 100644 index 00000000..074629e4 --- /dev/null +++ b/data/required-properties/response-new-required-properties-revision.json @@ -0,0 +1,105 @@ +{ + "info": { + "title": "title", + "version": "1.0.102" + }, + "openapi": "3.0.1", + "paths": { + "/web/search": { + "get": { + "operationId": "searchArticles", + "parameters": [ + { + "in": "header", + "name": "user-agent", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "*/*": { + "schema": { + "properties": { + "helpAndSupport": { + "properties": { + "expandLink": { + "properties": { + "title": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "title", + "url" + ], + "type": "object" + }, + "pages": { + "items": { + "properties": { + "contentPreview": { + "type": "string" + }, + "id": { + "type": "string" + }, + "image": { + "type": "string" + }, + "title": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "title": { + "type": "string" + }, + "totalResultsCount": { + "format": "int32", + "type": "integer" + } + }, + "required": [ + "expandLink", + "pages", + "title", + "totalResultsCount" + ], + "type": "object" + } + }, + "required": [ + "helpAndSupport" + ], + "type": "object" + } + } + }, + "description": "OK" + } + }, + "tags": [ + "web-search-controller" + ] + } + } + }, + "servers": [ + { + "url": "/api/" + } + ] +} \ No newline at end of file diff --git a/data/required-properties/response-required-properties-base.json b/data/required-properties/response-required-properties-base.json new file mode 100644 index 00000000..074629e4 --- /dev/null +++ b/data/required-properties/response-required-properties-base.json @@ -0,0 +1,105 @@ +{ + "info": { + "title": "title", + "version": "1.0.102" + }, + "openapi": "3.0.1", + "paths": { + "/web/search": { + "get": { + "operationId": "searchArticles", + "parameters": [ + { + "in": "header", + "name": "user-agent", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "*/*": { + "schema": { + "properties": { + "helpAndSupport": { + "properties": { + "expandLink": { + "properties": { + "title": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "title", + "url" + ], + "type": "object" + }, + "pages": { + "items": { + "properties": { + "contentPreview": { + "type": "string" + }, + "id": { + "type": "string" + }, + "image": { + "type": "string" + }, + "title": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "title": { + "type": "string" + }, + "totalResultsCount": { + "format": "int32", + "type": "integer" + } + }, + "required": [ + "expandLink", + "pages", + "title", + "totalResultsCount" + ], + "type": "object" + } + }, + "required": [ + "helpAndSupport" + ], + "type": "object" + } + } + }, + "description": "OK" + } + }, + "tags": [ + "web-search-controller" + ] + } + } + }, + "servers": [ + { + "url": "/api/" + } + ] +} \ No newline at end of file diff --git a/data/required-properties/response-required-properties-revision.json b/data/required-properties/response-required-properties-revision.json new file mode 100644 index 00000000..5151fa5f --- /dev/null +++ b/data/required-properties/response-required-properties-revision.json @@ -0,0 +1,104 @@ +{ + "info": { + "title": "title", + "version": "1.0.102" + }, + "openapi": "3.0.1", + "paths": { + "/web/search": { + "get": { + "operationId": "searchArticles", + "parameters": [ + { + "in": "header", + "name": "user-agent", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "*/*": { + "schema": { + "properties": { + "helpAndSupport": { + "properties": { + "expandLink": { + "properties": { + "title": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "title", + "url" + ], + "type": "object" + }, + "pages": { + "items": { + "properties": { + "contentPreview": { + "type": "string" + }, + "id": { + "type": "string" + }, + "image": { + "type": "string" + }, + "title": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "title": { + "type": "string" + }, + "totalResultsCount": { + "format": "int32", + "type": "integer" + } + }, + "required": [ + "expandLink", + "pages", + "totalResultsCount" + ], + "type": "object" + } + }, + "required": [ + "helpAndSupport" + ], + "type": "object" + } + } + }, + "description": "OK" + } + }, + "tags": [ + "web-search-controller" + ] + } + } + }, + "servers": [ + { + "url": "/api/" + } + ] +} \ No newline at end of file From c4303956a91d76299cbab9fb251618d3534877d7 Mon Sep 17 00:00:00 2001 From: Reuven Date: Tue, 21 Jun 2022 10:05:02 +0300 Subject: [PATCH 3/6] separate and ammend breaking property tests --- diff/diff_breaking_property_test.go | 251 ++++++++++++++++++++++++++++ diff/diff_breaking_test.go | 89 +--------- 2 files changed, 253 insertions(+), 87 deletions(-) create mode 100644 diff/diff_breaking_property_test.go diff --git a/diff/diff_breaking_property_test.go b/diff/diff_breaking_property_test.go new file mode 100644 index 00000000..60ce1598 --- /dev/null +++ b/diff/diff_breaking_property_test.go @@ -0,0 +1,251 @@ +package diff_test + +import ( + "fmt" + "testing" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/stretchr/testify/require" + "github.com/tufin/oasdiff/diff" +) + +func getReqPropFile(file string) string { + return fmt.Sprintf("../data/required-properties/%s", file) +} + +func TestBreaking_NewRequiredProperty(t *testing.T) { + s1 := l(t, 1) + s2 := l(t, 1) + + s2.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Properties["courseId"] = &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: "string", + Description: "Unique ID of the course", + }, + } + s2.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Required = []string{"courseId"} + + d, err := diff.Get(&diff.Config{ + BreakingOnly: true, + }, s1, s2) + require.NoError(t, err) + + // new required property in request header breaks client + require.NotEmpty(t, d) +} + +func TestBreaking_NewNonRequiredProperty(t *testing.T) { + s1 := l(t, 1) + s2 := l(t, 1) + + s2.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Properties["courseId"] = &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: "string", + Description: "Unique ID of the course", + }, + } + + d, err := diff.Get(&diff.Config{ + BreakingOnly: true, + }, s1, s2) + require.NoError(t, err) + + // new optional property in request header doesn't break client + require.Empty(t, d) +} + +func TestBreaking_PropertyRequiredEnabled(t *testing.T) { + s1 := l(t, 1) + s2 := l(t, 1) + + sr := openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: "string", + Description: "Unique ID of the course", + }, + } + + s1.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Properties["courseId"] = &sr + s1.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Required = []string{} + + s2.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Properties["courseId"] = &sr + s2.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Required = []string{"courseId"} + + d, err := diff.Get(&diff.Config{ + BreakingOnly: true, + }, s1, s2) + require.NoError(t, err) + + // changing an existing property in request header to required breaks client + require.NotEmpty(t, d) +} + +func TestBreaking_PropertyRequiredDisabled(t *testing.T) { + s1 := l(t, 1) + s2 := l(t, 1) + + sr := openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: "string", + Description: "Unique ID of the course", + }, + } + + s1.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Properties["courseId"] = &sr + s1.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Required = []string{"courseId"} + + s2.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Properties["courseId"] = &sr + s2.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Required = []string{} + + d, err := diff.Get(&diff.Config{ + BreakingOnly: true, + }, s1, s2) + require.NoError(t, err) + + // changing an existing property in request header to optional doesn't break client + require.Empty(t, d) +} + +func TestBreaking_RespBodyRequiredPropertyDisabled(t *testing.T) { + loader := openapi3.NewLoader() + + s1, err := loader.LoadFromFile(getReqPropFile("response-required-properties-base.json")) + require.NoError(t, err) + + s2, err := loader.LoadFromFile(getReqPropFile("response-required-properties-revision.json")) + require.NoError(t, err) + + d, err := diff.Get(&diff.Config{ + BreakingOnly: true, + }, s1, s2) + require.NoError(t, err) + + // changing an existing property in response body to optional breaks client + require.NotEmpty(t, d) +} + +func TestBreaking_RespBodyRequiredPropertyEnabled(t *testing.T) { + loader := openapi3.NewLoader() + + s1, err := loader.LoadFromFile(getReqPropFile("response-required-properties-revision.json")) + require.NoError(t, err) + + s2, err := loader.LoadFromFile(getReqPropFile("response-required-properties-base.json")) + require.NoError(t, err) + + d, err := diff.Get(&diff.Config{ + BreakingOnly: true, + }, s1, s2) + require.NoError(t, err) + + // changing an existing property in response body to required doesn't break client + require.Empty(t, d) +} + +func TestBreaking_ReqBodyRequiredPropertyDisabled(t *testing.T) { + loader := openapi3.NewLoader() + + s1, err := loader.LoadFromFile(getReqPropFile("request-required-properties-base.yaml")) + require.NoError(t, err) + + s2, err := loader.LoadFromFile(getReqPropFile("request-required-properties-revision.yaml")) + require.NoError(t, err) + + d, err := diff.Get(&diff.Config{ + BreakingOnly: true, + }, s1, s2) + require.NoError(t, err) + + // changing an existing property in request body to optional doesn't break client + require.Empty(t, d) +} + +func TestBreaking_ReqBodyRequiredPropertyEnabled(t *testing.T) { + loader := openapi3.NewLoader() + + s1, err := loader.LoadFromFile(getReqPropFile("request-required-properties-revision.yaml")) + require.NoError(t, err) + + s2, err := loader.LoadFromFile(getReqPropFile("request-required-properties-base.yaml")) + require.NoError(t, err) + + d, err := diff.Get(&diff.Config{ + BreakingOnly: true, + }, s1, s2) + require.NoError(t, err) + + // changing an existing property in request body to required breaks client + require.NotEmpty(t, d) +} + +func TestBreaking_ReqBodyNewRequiredProperty(t *testing.T) { + loader := openapi3.NewLoader() + + s1, err := loader.LoadFromFile(getReqPropFile("request-new-required-properties-base.yaml")) + require.NoError(t, err) + + s2, err := loader.LoadFromFile(getReqPropFile("request-new-required-properties-revision.yaml")) + require.NoError(t, err) + + d, err := diff.Get(&diff.Config{ + BreakingOnly: true, + }, s1, s2) + require.NoError(t, err) + + // adding a new required property in request body breaks client + require.NotEmpty(t, d) +} + +func TestBreaking_ReqBodyDeleteRequiredProperty(t *testing.T) { + loader := openapi3.NewLoader() + + s1, err := loader.LoadFromFile(getReqPropFile("request-new-required-properties-revision.yaml")) + require.NoError(t, err) + + s2, err := loader.LoadFromFile(getReqPropFile("request-new-required-properties-base.yaml")) + require.NoError(t, err) + + d, err := diff.Get(&diff.Config{ + BreakingOnly: true, + }, s1, s2) + require.NoError(t, err) + + // deleting a required property in request doesn't break client + require.Empty(t, d) +} + +func TestBreaking_RespBodyNewRequiredProperty(t *testing.T) { + loader := openapi3.NewLoader() + + s1, err := loader.LoadFromFile(getReqPropFile("response-new-required-properties-base.json")) + require.NoError(t, err) + + s2, err := loader.LoadFromFile(getReqPropFile("response-new-required-properties-revision.json")) + require.NoError(t, err) + + d, err := diff.Get(&diff.Config{ + BreakingOnly: true, + }, s1, s2) + require.NoError(t, err) + + // adding a new required property in response body doesn't break client + require.Empty(t, d) +} + +func TestBreaking_RespBodyDeleteRequiredProperty(t *testing.T) { + loader := openapi3.NewLoader() + + s1, err := loader.LoadFromFile(getReqPropFile("response-new-required-properties-revision.json")) + require.NoError(t, err) + + s2, err := loader.LoadFromFile(getReqPropFile("response-new-required-properties-base.json")) + require.NoError(t, err) + + d, err := diff.Get(&diff.Config{ + BreakingOnly: true, + }, s1, s2) + require.NoError(t, err) + + // deleting a required property in response body breaks client + require.NotEmpty(t, d) +} diff --git a/diff/diff_breaking_test.go b/diff/diff_breaking_test.go index 2c16b290..11e3a919 100644 --- a/diff/diff_breaking_test.go +++ b/diff/diff_breaking_test.go @@ -68,91 +68,6 @@ func TestCompareWithDefault_Nil(t *testing.T) { ) } -func TestBreaking_NewRequiredProperty(t *testing.T) { - s1 := l(t, 1) - s2 := l(t, 1) - - s2.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Properties["courseId"] = &openapi3.SchemaRef{ - Value: &openapi3.Schema{ - Type: "string", - Description: "Unique ID of the course", - }, - } - s2.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Required = []string{"courseId"} - - d, err := diff.Get(&diff.Config{ - BreakingOnly: true, - }, s1, s2) - require.NoError(t, err) - require.NotEmpty(t, d) -} - -func TestBreaking_NewNonRequiredProperty(t *testing.T) { - s1 := l(t, 1) - s2 := l(t, 1) - - s2.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Properties["courseId"] = &openapi3.SchemaRef{ - Value: &openapi3.Schema{ - Type: "string", - Description: "Unique ID of the course", - }, - } - - d, err := diff.Get(&diff.Config{ - BreakingOnly: true, - }, s1, s2) - require.NoError(t, err) - require.Empty(t, d) -} - -func TestBreaking_PropertyRequiredEnabled(t *testing.T) { - s1 := l(t, 1) - s2 := l(t, 1) - - sr := openapi3.SchemaRef{ - Value: &openapi3.Schema{ - Type: "string", - Description: "Unique ID of the course", - }, - } - - s1.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Properties["courseId"] = &sr - s1.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Required = []string{} - - s2.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Properties["courseId"] = &sr - s2.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Required = []string{"courseId"} - - d, err := diff.Get(&diff.Config{ - BreakingOnly: true, - }, s1, s2) - require.NoError(t, err) - require.NotEmpty(t, d) -} - -func TestBreaking_PropertyRequiredDisabled(t *testing.T) { - s1 := l(t, 1) - s2 := l(t, 1) - - sr := openapi3.SchemaRef{ - Value: &openapi3.Schema{ - Type: "string", - Description: "Unique ID of the course", - }, - } - - s1.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Properties["courseId"] = &sr - s1.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Required = []string{"courseId"} - - s2.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Properties["courseId"] = &sr - s2.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Required = []string{} - - d, err := diff.Get(&diff.Config{ - BreakingOnly: true, - }, s1, s2) - require.NoError(t, err) - require.Empty(t, d) -} - func deleteParam(op *openapi3.Operation, in string, name string) { result := openapi3.NewParameters() @@ -216,7 +131,7 @@ func TestBreaking_NewNonRequiredHeaderParam(t *testing.T) { }, s1, s2) require.NoError(t, err) - // new non-required header param doesn't break client + // new optional header param doesn't break client require.Empty(t, d) } @@ -253,7 +168,7 @@ func TestBreaking_HeaderParamRequiredDisabled(t *testing.T) { }, s1, s2) require.NoError(t, err) - // changing an existing header param to non-required doesn't break client + // changing an existing header param to optional doesn't break client require.Empty(t, d) } From 6380188a3e28bf81177b3bf5ba1598cd84188347 Mon Sep 17 00:00:00 2001 From: Reuven Date: Tue, 21 Jun 2022 16:01:07 +0300 Subject: [PATCH 4/6] consider direction in schema diffs --- diff/schema_diff.go | 22 ++++++++++++++++------ diff/schemas_diff.go | 20 ++++++++++++++++++++ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/diff/schema_diff.go b/diff/schema_diff.go index 4e469c07..e20b3846 100644 --- a/diff/schema_diff.go +++ b/diff/schema_diff.go @@ -56,7 +56,7 @@ func (diff *SchemaDiff) Empty() bool { return diff == nil || *diff == SchemaDiff{} } -func (diff *SchemaDiff) removeNonBreaking(schema2 *openapi3.SchemaRef) { +func (diff *SchemaDiff) removeNonBreaking(state *state, schema2 *openapi3.SchemaRef) { if diff.Empty() { return @@ -131,7 +131,7 @@ func (diff *SchemaDiff) removeNonBreaking(schema2 *openapi3.SchemaRef) { } // Object - diff.removeAddedButNonRequiredProperties(schema2) + diff.removeChangedButNonRequiredProperties(state, schema2) if !diff.AdditionalPropertiesAllowedDiff.CompareWithDefault(true, false, true) { diff.AdditionalPropertiesAllowedDiff = nil @@ -146,8 +146,17 @@ func (diff *SchemaDiff) removeNonBreaking(schema2 *openapi3.SchemaRef) { } } -func (diff *SchemaDiff) removeAddedButNonRequiredProperties(schema2 *openapi3.SchemaRef) { +func getChangedSet(propertiesDiff *SchemasDiff, direction direction) *StringList { + if direction == directionRequest { + return &propertiesDiff.Added + } + return &propertiesDiff.Deleted +} +// removeChangedButNonRequiredProperties deletes non-required property changes that don't break client +// In request: remove added but non-required properties +// In response: remove deleted but non-required properties +func (diff *SchemaDiff) removeChangedButNonRequiredProperties(state *state, schema2 *openapi3.SchemaRef) { if diff.Empty() || diff.PropertiesDiff.Empty() { return } @@ -157,14 +166,15 @@ func (diff *SchemaDiff) removeAddedButNonRequiredProperties(schema2 *openapi3.Sc } requiredMap := StringList(schema2.Value.Required).toStringSet() + changedSet := getChangedSet(diff.PropertiesDiff, state.direction) newList := StringList{} - for _, property := range diff.PropertiesDiff.Added { + for _, property := range *changedSet { if _, ok := requiredMap[property]; ok { newList = append(newList, property) } } - diff.PropertiesDiff.Added = newList + *changedSet = newList if diff.PropertiesDiff.Empty() { diff.PropertiesDiff = nil @@ -183,7 +193,7 @@ func getSchemaDiff(config *Config, state *state, schema1, schema2 *openapi3.Sche } if config.BreakingOnly { - diff.removeNonBreaking(schema2) + diff.removeNonBreaking(state, schema2) } if diff.Empty() { diff --git a/diff/schemas_diff.go b/diff/schemas_diff.go index 340b7f03..b3cf1562 100644 --- a/diff/schemas_diff.go +++ b/diff/schemas_diff.go @@ -22,6 +22,22 @@ func (schemasDiff *SchemasDiff) Empty() bool { len(schemasDiff.Modified) == 0 } +func (schemasDiff *SchemasDiff) removeNonBreaking(state *state) { + + if schemasDiff.Empty() { + return + } + + switch state.direction { + case directionRequest: + // In request: deleting properties is non-breaking (for client) + schemasDiff.Deleted = nil + case directionResponse: + // In response: adding properties is non-breaking (for client) + schemasDiff.Added = nil + } +} + func newSchemasDiff() *SchemasDiff { return &SchemasDiff{ Added: StringList{}, @@ -43,6 +59,10 @@ func getSchemasDiff(config *Config, state *state, schemas1, schemas2 openapi3.Sc return nil, err } + if config.BreakingOnly { + diff.removeNonBreaking(state) + } + if diff.Empty() { return nil, nil } From 48c42f2d0dda4438bd37ba39c63a05d978dad3e1 Mon Sep 17 00:00:00 2001 From: Reuven Date: Tue, 21 Jun 2022 17:09:13 +0300 Subject: [PATCH 5/6] go fmt --- diff/schema_diff.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/diff/schema_diff.go b/diff/schema_diff.go index e20b3846..d59402fa 100644 --- a/diff/schema_diff.go +++ b/diff/schema_diff.go @@ -147,7 +147,7 @@ func (diff *SchemaDiff) removeNonBreaking(state *state, schema2 *openapi3.Schema } func getChangedSet(propertiesDiff *SchemasDiff, direction direction) *StringList { - if direction == directionRequest { + if direction == directionRequest { return &propertiesDiff.Added } return &propertiesDiff.Deleted From 66a8e66c2176673cdce42c59b2bfe45c0b205148 Mon Sep 17 00:00:00 2001 From: Reuven Date: Tue, 21 Jun 2022 20:26:16 +0300 Subject: [PATCH 6/6] add to-do comments --- diff/callbacks_diff.go | 1 + diff/content_diff.go | 1 + diff/encodings_diff.go | 1 + diff/headers_diff.go | 1 + diff/parameters_diff.go | 1 + 5 files changed, 5 insertions(+) diff --git a/diff/callbacks_diff.go b/diff/callbacks_diff.go index ff138835..9a4583f0 100644 --- a/diff/callbacks_diff.go +++ b/diff/callbacks_diff.go @@ -31,6 +31,7 @@ func (diff *CallbacksDiff) removeNonBreaking() { return } + // TODO: check breaking conditions diff.Added = nil } diff --git a/diff/content_diff.go b/diff/content_diff.go index fc71fe2a..7ae214e3 100644 --- a/diff/content_diff.go +++ b/diff/content_diff.go @@ -38,6 +38,7 @@ func (diff *ContentDiff) removeNonBreaking() { return } + // TODO: check breaking conditions diff.MediaTypeAdded = nil } diff --git a/diff/encodings_diff.go b/diff/encodings_diff.go index 4aa1b8ba..7e485100 100644 --- a/diff/encodings_diff.go +++ b/diff/encodings_diff.go @@ -29,6 +29,7 @@ func (diff *EncodingsDiff) removeNonBreaking() { return } + // TODO: check breaking conditions diff.Added = nil } diff --git a/diff/headers_diff.go b/diff/headers_diff.go index 2c6e9686..121b17ed 100644 --- a/diff/headers_diff.go +++ b/diff/headers_diff.go @@ -30,6 +30,7 @@ func (headersDiff *HeadersDiff) removeNonBreaking() { return } + // TODO: check if we need to consider header.Required flags headersDiff.Added = nil } diff --git a/diff/parameters_diff.go b/diff/parameters_diff.go index 37bfb23b..a98b1a1f 100644 --- a/diff/parameters_diff.go +++ b/diff/parameters_diff.go @@ -30,6 +30,7 @@ func (parametersDiff *ParametersDiff) removeNonBreaking(params2 openapi3.Paramet return } + // TODO: check vs. SchemaDiff.removeChangedButNonRequiredProperties parametersDiff.removeAddedButNonRequired(params2) }