Repository: getkin/kin-openapi
Pull request #226 (2020-06-15 07:15:11)

Responses Object MUST contain at least one response code
Fixes https://github.com/getkin/kin-openapi/issues/224


Patch:
diff --git a/openapi3/operation.go b/openapi3/operation.go
index 0841863..5e367e0 100644
--- a/openapi3/operation.go
+++ b/openapi3/operation.go
@@ -71,14 +71,12 @@ func (operation *Operation) AddResponse(status int, response *Response) {
 		responses = NewResponses()
 		operation.Responses = responses
 	}
-	if status == 0 {
-		responses["default"] = &ResponseRef{
-			Value: response,
-		}
-	} else {
-		responses[strconv.FormatInt(int64(status), 10)] = &ResponseRef{
-			Value: response,
-		}
+	code := "default"
+	if status != 0 {
+		code = strconv.FormatInt(int64(status), 10)
+	}
+	responses[code] = &ResponseRef{
+		Value: response,
 	}
 }
 
diff --git a/openapi3/response.go b/openapi3/response.go
index db83db7..3e1a732 100644
--- a/openapi3/response.go
+++ b/openapi3/response.go
@@ -12,7 +12,9 @@ import (
 type Responses map[string]*ResponseRef
 
 func NewResponses() Responses {
-	return make(Responses, 8)
+	r := make(Responses)
+	r["default"] = &ResponseRef{Value: NewResponse().WithDescription("")}
+	return r
 }
 
 func (responses Responses) Default() *ResponseRef {
@@ -24,6 +26,9 @@ func (responses Responses) Get(status int) *ResponseRef {
 }
 
 func (responses Responses) Validate(c context.Context) error {
+	if len(responses) == 0 {
+		return errors.New("The Responses Object MUST contain at least one response code")
+	}
 	for _, v := range responses {
 		if err := v.Validate(c); err != nil {
 			return err


Test patch:
diff --git a/openapi3/response_issue224_test.go b/openapi3/response_issue224_test.go
new file mode 100644
index 0000000..6dae39d
--- /dev/null
+++ b/openapi3/response_issue224_test.go
@@ -0,0 +1,460 @@
+package openapi3
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestEmptyResponsesAreInvalid(t *testing.T) {
+	spec := `{
+    "openapi": "3.0.0",
+    "servers": [
+        {
+            "url": "http://petstore.swagger.io/v2"
+        }
+    ],
+    "info": {
+        "description": ":dog: :cat: :rabbit: This is a sample server Petstore server.  You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/).  For this sample, you can use the api key to test the authorization filters.",
+        "version": "1.0.0",
+        "title": "Swagger Petstore",
+        "termsOfService": "http://swagger.io/terms/",
+        "contact": {
+            "email": "apiteam@swagger.io"
+        },
+        "license": {
+            "name": "Apache 2.0",
+            "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
+        }
+    },
+    "tags": [
+        {
+            "name": "pet",
+            "description": "Everything about your Pets",
+            "externalDocs": {
+                "description": "Find out more",
+                "url": "http://swagger.io"
+            }
+        },
+        {
+            "name": "store",
+            "description": "Access to Petstore orders"
+        },
+        {
+            "name": "user",
+            "description": "Operations about user",
+            "externalDocs": {
+                "description": "Find out more about our store",
+                "url": "http://swagger.io"
+            }
+        }
+    ],
+    "paths": {
+        "/pet": {
+            "post": {
+                "tags": [
+                    "pet"
+                ],
+                "summary": "Add a new pet to the store",
+                "description": "",
+                "operationId": "addPet",
+                "responses": {
+
+                },
+                "security": [
+                    {
+                        "petstore_auth": [
+                            "write:pets",
+                            "read:pets"
+                        ]
+                    }
+                ],
+                "requestBody": {
+                    "$ref": "#/components/requestBodies/Pet"
+                },
+                "parameters": []
+            },
+            "put": {
+                "tags": [
+                    "pet"
+                ],
+                "summary": "Update an existing pet",
+                "description": "",
+                "operationId": "updatePet",
+                "responses": {
+
+                },
+                "security": [
+                    {
+                        "petstore_auth": [
+                            "write:pets",
+                            "read:pets"
+                        ]
+                    }
+                ],
+                "requestBody": {
+                    "$ref": "#/components/requestBodies/Pet"
+                },
+                "parameters": []
+            }
+        },
+        "/pet/{petId}": {
+            "get": {
+                "tags": [
+                    "pet"
+                ],
+                "summary": "Find pet by ID",
+                "description": "Returns a single pet",
+                "operationId": "getPetById",
+                "parameters": [
+                    {
+                        "name": "petId",
+                        "in": "path",
+                        "description": "ID of pet to return",
+                        "required": true,
+                        "schema": {
+                            "type": "integer",
+                            "format": "int64"
+                        }
+                    }
+                ],
+                "responses": {
+
+                },
+                "security": [
+                    {
+                        "api_key": []
+                    }
+                ]
+            },
+            "post": {
+                "tags": [
+                    "pet"
+                ],
+                "summary": "Updates a pet in the store with form data",
+                "description": "",
+                "operationId": "updatePetWithForm",
+                "parameters": [
+                    {
+                        "name": "petId",
+                        "in": "path",
+                        "description": "ID of pet that needs to be updated",
+                        "required": true,
+                        "schema": {
+                            "type": "integer",
+                            "format": "int64"
+                        }
+                    }
+                ],
+                "responses": {
+
+                },
+                "security": [
+                    {
+                        "petstore_auth": [
+                            "write:pets",
+                            "read:pets"
+                        ]
+                    }
+                ],
+                "requestBody": {
+                    "content": {
+                        "application/x-www-form-urlencoded": {
+                            "schema": {
+                                "type": "object",
+                                "properties": {
+                                    "name": {
+                                        "description": "Updated name of the pet",
+                                        "type": "string"
+                                    },
+                                    "status": {
+                                        "description": "Updated status of the pet",
+                                        "type": "string"
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            },
+            "delete": {
+                "tags": [
+                    "pet"
+                ],
+                "summary": "Deletes a pet",
+                "description": "",
+                "operationId": "deletePet",
+                "parameters": [
+                    {
+                        "name": "api_key",
+                        "in": "header",
+                        "required": false,
+                        "schema": {
+                            "type": "string"
+                        }
+                    },
+                    {
+                        "name": "petId",
+                        "in": "path",
+                        "description": "Pet id to delete",
+                        "required": true,
+                        "schema": {
+                            "type": "integer",
+                            "format": "int64"
+                        }
+                    }
+                ],
+                "responses": {
+
+                },
+                "security": [
+                    {
+                        "petstore_auth": [
+                            "write:pets",
+                            "read:pets"
+                        ]
+                    }
+                ]
+            }
+        }
+    },
+    "externalDocs": {
+        "description": "See AsyncAPI example",
+        "url": "https://mermade.github.io/shins/asyncapi.html"
+    },
+    "components": {
+        "schemas": {
+            "Order": {
+                "type": "object",
+                "properties": {
+                    "id": {
+                        "type": "integer",
+                        "format": "int64"
+                    },
+                    "petId": {
+                        "type": "integer",
+                        "format": "int64"
+                    },
+                    "quantity": {
+                        "type": "integer",
+                        "format": "int32"
+                    },
+                    "shipDate": {
+                        "type": "string",
+                        "format": "date-time"
+                    },
+                    "status": {
+                        "type": "string",
+                        "description": "Order Status",
+                        "enum": [
+                            "placed",
+                            "approved",
+                            "delivered"
+                        ]
+                    },
+                    "complete": {
+                        "type": "boolean",
+                        "default": false
+                    }
+                },
+                "xml": {
+                    "name": "Order"
+                }
+            },
+            "Category": {
+                "type": "object",
+                "properties": {
+                    "id": {
+                        "type": "integer",
+                        "format": "int64"
+                    },
+                    "name": {
+                        "type": "string"
+                    }
+                },
+                "xml": {
+                    "name": "Category"
+                }
+            },
+            "User": {
+                "type": "object",
+                "properties": {
+                    "id": {
+                        "type": "integer",
+                        "format": "int64"
+                    },
+                    "username": {
+                        "type": "string"
+                    },
+                    "firstName": {
+                        "type": "string"
+                    },
+                    "lastName": {
+                        "type": "string"
+                    },
+                    "email": {
+                        "type": "string"
+                    },
+                    "password": {
+                        "type": "string"
+                    },
+                    "phone": {
+                        "type": "string"
+                    },
+                    "userStatus": {
+                        "type": "integer",
+                        "format": "int32",
+                        "description": "User Status"
+                    }
+                },
+                "xml": {
+                    "name": "User"
+                }
+            },
+            "Tag": {
+                "type": "object",
+                "properties": {
+                    "id": {
+                        "type": "integer",
+                        "format": "int64"
+                    },
+                    "name": {
+                        "type": "string"
+                    }
+                },
+                "xml": {
+                    "name": "Tag"
+                }
+            },
+            "Pet": {
+                "type": "object",
+                "required": [
+                    "name",
+                    "photoUrls"
+                ],
+                "properties": {
+                    "id": {
+                        "type": "integer",
+                        "format": "int64"
+                    },
+                    "category": {
+                        "$ref": "#/components/schemas/Category"
+                    },
+                    "name": {
+                        "type": "string",
+                        "example": "doggie"
+                    },
+                    "photoUrls": {
+                        "type": "array",
+                        "xml": {
+                            "name": "photoUrl",
+                            "wrapped": true
+                        },
+                        "items": {
+                            "type": "string"
+                        }
+                    },
+                    "tags": {
+                        "type": "array",
+                        "xml": {
+                            "name": "tag",
+                            "wrapped": true
+                        },
+                        "items": {
+                            "$ref": "#/components/schemas/Tag"
+                        }
+                    },
+                    "status": {
+                        "type": "string",
+                        "description": "pet status in the store",
+                        "enum": [
+                            "available",
+                            "pending",
+                            "sold"
+                        ]
+                    }
+                },
+                "xml": {
+                    "name": "Pet"
+                }
+            },
+            "ApiResponse": {
+                "type": "object",
+                "properties": {
+                    "code": {
+                        "type": "integer",
+                        "format": "int32"
+                    },
+                    "type": {
+                        "type": "string"
+                    },
+                    "message": {
+                        "type": "string"
+                    }
+                }
+            }
+        },
+        "requestBodies": {
+            "Pet": {
+                "content": {
+                    "application/json": {
+                        "schema": {
+                            "$ref": "#/components/schemas/Pet"
+                        }
+                    },
+                    "application/xml": {
+                        "schema": {
+                            "$ref": "#/components/schemas/Pet"
+                        }
+                    }
+                },
+                "description": "Pet object that needs to be added to the store",
+                "required": true
+            },
+            "UserArray": {
+                "content": {
+                    "application/json": {
+                        "schema": {
+                            "type": "array",
+                            "items": {
+                                "$ref": "#/components/schemas/User"
+                            }
+                        }
+                    }
+                },
+                "description": "List of user object",
+                "required": true
+            }
+        },
+        "securitySchemes": {
+            "petstore_auth": {
+                "type": "oauth2",
+                "flows": {
+                    "implicit": {
+                        "authorizationUrl": "http://petstore.swagger.io/oauth/dialog",
+                        "scopes": {
+                            "write:pets": "modify pets in your account",
+                            "read:pets": "read your pets"
+                        }
+                    }
+                }
+            },
+            "api_key": {
+                "type": "apiKey",
+                "name": "api_key",
+                "in": "header"
+            }
+        },
+        "links": {},
+        "callbacks": {}
+    },
+    "security": []
+}
+`
+
+	doc, err := NewSwaggerLoader().LoadSwaggerFromData([]byte(spec))
+	require.NoError(t, err)
+	err = doc.Validate(context.Background())
+	require.Error(t, err)
+}
diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go
index 4a5cefc..1cf7be3 100644
--- a/openapi3filter/req_resp_decoder_test.go
+++ b/openapi3filter/req_resp_decoder_test.go
@@ -924,7 +924,11 @@ func TestDecodeParameter(t *testing.T) {
 						Version: "0.1",
 					}
 					spec := &openapi3.Swagger{OpenAPI: "3.0.0", Info: info}
-					op := &openapi3.Operation{OperationID: "test", Parameters: []*openapi3.ParameterRef{{Value: tc.param}}, Responses: make(openapi3.Responses)}
+					op := &openapi3.Operation{
+						OperationID: "test",
+						Parameters:  []*openapi3.ParameterRef{{Value: tc.param}},
+						Responses:   openapi3.NewResponses(),
+					}
 					spec.AddOperation("/test"+path, http.MethodGet, op)
 					router := NewRouter()
 					require.NoError(t, router.AddSwagger(spec), "failed to create a router")
diff --git a/openapi3filter/router_test.go b/openapi3filter/router_test.go
index 4eba2c3..2865cb8 100644
--- a/openapi3filter/router_test.go
+++ b/openapi3filter/router_test.go
@@ -18,17 +18,17 @@ func TestRouter(t *testing.T) {
 	)
 
 	// Build swagger
-	helloCONNECT := &openapi3.Operation{Responses: make(openapi3.Responses)}
-	helloDELETE := &openapi3.Operation{Responses: make(openapi3.Responses)}
-	helloGET := &openapi3.Operation{Responses: make(openapi3.Responses)}
-	helloHEAD := &openapi3.Operation{Responses: make(openapi3.Responses)}
-	helloOPTIONS := &openapi3.Operation{Responses: make(openapi3.Responses)}
-	helloPATCH := &openapi3.Operation{Responses: make(openapi3.Responses)}
-	helloPOST := &openapi3.Operation{Responses: make(openapi3.Responses)}
-	helloPUT := &openapi3.Operation{Responses: make(openapi3.Responses)}
-	helloTRACE := &openapi3.Operation{Responses: make(openapi3.Responses)}
-	paramsGET := &openapi3.Operation{Responses: make(openapi3.Responses)}
-	partialGET := &openapi3.Operation{Responses: make(openapi3.Responses)}
+	helloCONNECT := &openapi3.Operation{Responses: openapi3.NewResponses()}
+	helloDELETE := &openapi3.Operation{Responses: openapi3.NewResponses()}
+	helloGET := &openapi3.Operation{Responses: openapi3.NewResponses()}
+	helloHEAD := &openapi3.Operation{Responses: openapi3.NewResponses()}
+	helloOPTIONS := &openapi3.Operation{Responses: openapi3.NewResponses()}
+	helloPATCH := &openapi3.Operation{Responses: openapi3.NewResponses()}
+	helloPOST := &openapi3.Operation{Responses: openapi3.NewResponses()}
+	helloPUT := &openapi3.Operation{Responses: openapi3.NewResponses()}
+	helloTRACE := &openapi3.Operation{Responses: openapi3.NewResponses()}
+	paramsGET := &openapi3.Operation{Responses: openapi3.NewResponses()}
+	partialGET := &openapi3.Operation{Responses: openapi3.NewResponses()}
 	swagger := &openapi3.Swagger{
 		OpenAPI: "3.0.0",
 		Info: &openapi3.Info{
diff --git a/openapi3filter/validation_test.go b/openapi3filter/validation_test.go
index 6ac05b3..c4e6e0f 100644
--- a/openapi3filter/validation_test.go
+++ b/openapi3filter/validation_test.go
@@ -133,13 +133,13 @@ func TestFilter(t *testing.T) {
 							},
 						},
 					},
-					Responses: make(openapi3.Responses),
+					Responses: openapi3.NewResponses(),
 				},
 			},
 
 			"/issue151": &openapi3.PathItem{
 				Get: &openapi3.Operation{
-					Responses: make(openapi3.Responses),
+					Responses: openapi3.NewResponses(),
 				},
 				Parameters: openapi3.Parameters{
 					{
@@ -557,21 +557,16 @@ func TestOperationOrSwaggerSecurity(t *testing.T) {
 	for _, tc := range tc {
 		var securityRequirements *openapi3.SecurityRequirements = nil
 		if tc.schemes != nil {
-			tempS := make(openapi3.SecurityRequirements, 0)
+			tempS := openapi3.NewSecurityRequirements()
 			for _, scheme := range *tc.schemes {
-				tempS = append(
-					tempS,
-					openapi3.SecurityRequirement{
-						scheme.Name: {},
-					},
-				)
+				tempS.With(openapi3.SecurityRequirement{scheme.Name: {}})
 			}
-			securityRequirements = &tempS
+			securityRequirements = tempS
 		}
 		swagger.Paths[tc.name] = &openapi3.PathItem{
 			Get: &openapi3.Operation{
 				Security:  securityRequirements,
-				Responses: make(openapi3.Responses),
+				Responses: openapi3.NewResponses(),
 			},
 		}
 	}
@@ -693,18 +688,16 @@ func TestAnySecurityRequirementMet(t *testing.T) {
 	// Add the paths to the swagger
 	for _, tc := range tc {
 		// Create the security requirements from the test cases's schemes
-		securityRequirements := make(openapi3.SecurityRequirements, len(tc.schemes))
-		for i, scheme := range tc.schemes {
-			securityRequirements[i] = openapi3.SecurityRequirement{
-				scheme: {},
-			}
+		securityRequirements := openapi3.NewSecurityRequirements()
+		for _, scheme := range tc.schemes {
+			securityRequirements.With(openapi3.SecurityRequirement{scheme: {}})
 		}
 
 		// Create the path with the security requirements
 		swagger.Paths[tc.name] = &openapi3.PathItem{
 			Get: &openapi3.Operation{
-				Security:  &securityRequirements,
-				Responses: make(openapi3.Responses),
+				Security:  securityRequirements,
+				Responses: openapi3.NewResponses(),
 			},
 		}
 	}
@@ -804,7 +797,7 @@ func TestAllSchemesMet(t *testing.T) {
 				Security: &openapi3.SecurityRequirements{
 					securityRequirement,
 				},
-				Responses: make(openapi3.Responses),
+				Responses: openapi3.NewResponses(),
 			},
 		}
 	}