-
Notifications
You must be signed in to change notification settings - Fork 110
feat(validation): implement Rule 47 for response content-type and schema #754
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -646,3 +646,51 @@ func collectParamNames(item *openapi3.PathItem, op *openapi3.Operation) map[stri | |||||
| } | ||||||
| return names | ||||||
| } | ||||||
|
|
||||||
| // --- Rule 47: 2xx responses (except 204) must have content and schema --- | ||||||
|
|
||||||
| func checkRule47(filePath string, doc *openapi3.T, opts AuditOptions) []Violation { | ||||||
| if doc == nil || doc.Paths == nil { | ||||||
| return nil | ||||||
| } | ||||||
| var out []Violation | ||||||
| for path, item := range doc.Paths.Map() { | ||||||
| for _, method := range httpMethods { | ||||||
| op := getOperation(item, method) | ||||||
| if op == nil || op.Responses == nil { | ||||||
| continue | ||||||
| } | ||||||
| label := fmt.Sprintf("%s %s", strings.ToUpper(method), path) | ||||||
| for code, respRef := range op.Responses.Map() { | ||||||
| // Only interested in 2xx responses, excluding 204 (No Content). | ||||||
| if !strings.HasPrefix(code, "2") || code == "204" { | ||||||
|
||||||
| if !strings.HasPrefix(code, "2") || code == "204" { | |
| if !strings.HasPrefix(code, "2") || code == "204" || code == "205" { |
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updating the violation message to include 205 in the list of excluded status codes for clarity.
| Message: fmt.Sprintf("%s — response %s is missing a `content` block. All 2xx responses (except 204) must declare a content-type.", label, code), | |
| Message: fmt.Sprintf("%s — response %s is missing a content block. All 2xx responses (except 204 and 205) must declare a content-type.", label, code), |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -775,3 +775,100 @@ func TestCheckRule28_DeleteCollectionPathIgnored(t *testing.T) { | |
| t.Errorf("expected 0 violations for collection DELETE, got %d", len(vs)) | ||
| } | ||
| } | ||
| // --------------------------------------------------------------------------- | ||
| // Rule 47: 2xx responses (except 204) must have content and schema | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| func TestCheckRule47_MissingContent(t *testing.T) { | ||
| doc := &openapi3.T{ | ||
| OpenAPI: "3.0.0", | ||
| Info: &openapi3.Info{Title: "Test", Version: "v1"}, | ||
| Paths: openapi3.NewPaths(), | ||
| } | ||
| responses := openapi3.NewResponses() | ||
| desc := "Success" | ||
| responses.Set("200", &openapi3.ResponseRef{Value: &openapi3.Response{Description: &desc}}) | ||
| doc.Paths.Set("/api/test", &openapi3.PathItem{ | ||
| Get: &openapi3.Operation{Responses: responses}, | ||
| }) | ||
|
|
||
| vs := checkRule47("api.yml", doc, AuditOptions{}) | ||
| if len(vs) != 1 { | ||
| t.Fatalf("expected 1 violation for missing content on 200, got %d", len(vs)) | ||
| } | ||
| if !strings.Contains(vs[0].Message, "missing a `content` block") { | ||
| t.Errorf("expected message about missing content, got %q", vs[0].Message) | ||
| } | ||
| } | ||
|
|
||
| func TestCheckRule47_MissingSchema(t *testing.T) { | ||
| doc := &openapi3.T{ | ||
| OpenAPI: "3.0.0", | ||
| Info: &openapi3.Info{Title: "Test", Version: "v1"}, | ||
| Paths: openapi3.NewPaths(), | ||
| } | ||
| responses := openapi3.NewResponses() | ||
| desc := "Success" | ||
| responses.Set("201", &openapi3.ResponseRef{Value: &openapi3.Response{ | ||
| Description: &desc, | ||
| Content: openapi3.Content{ | ||
| "application/json": &openapi3.MediaType{}, // Missing Schema | ||
| }, | ||
| }}) | ||
| doc.Paths.Set("/api/test", &openapi3.PathItem{ | ||
| Post: &openapi3.Operation{Responses: responses}, | ||
| }) | ||
|
|
||
| vs := checkRule47("api.yml", doc, AuditOptions{}) | ||
| if len(vs) != 1 { | ||
| t.Fatalf("expected 1 violation for missing schema, got %d", len(vs)) | ||
| } | ||
| if !strings.Contains(vs[0].Message, "missing a `schema`") { | ||
| t.Errorf("expected message about missing schema, got %q", vs[0].Message) | ||
| } | ||
| } | ||
|
|
||
| func TestCheckRule47_Valid204(t *testing.T) { | ||
| doc := &openapi3.T{ | ||
| OpenAPI: "3.0.0", | ||
| Info: &openapi3.Info{Title: "Test", Version: "v1"}, | ||
| Paths: openapi3.NewPaths(), | ||
| } | ||
| responses := openapi3.NewResponses() | ||
| desc := "No Content" | ||
| responses.Set("204", &openapi3.ResponseRef{Value: &openapi3.Response{Description: &desc}}) | ||
| doc.Paths.Set("/api/test", &openapi3.PathItem{ | ||
| Delete: &openapi3.Operation{Responses: responses}, | ||
| }) | ||
|
|
||
| vs := checkRule47("api.yml", doc, AuditOptions{}) | ||
| if len(vs) != 0 { | ||
| t.Errorf("expected 0 violations for 204 response without content, got %d", len(vs)) | ||
| } | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding a test case for the }
func TestCheckRule47_Valid205(t *testing.T) {
doc := &openapi3.T{
OpenAPI: "3.0.0",
Info: &openapi3.Info{Title: "Test", Version: "v1"},
Paths: openapi3.NewPaths(),
}
responses := openapi3.NewResponses()
desc := "Reset Content"
responses.Set("205", &openapi3.ResponseRef{Value: &openapi3.Response{Description: &desc}})
doc.Paths.Set("/api/test", &openapi3.PathItem{
Post: &openapi3.Operation{Responses: responses},
})
vs := checkRule47("api.yml", doc, AuditOptions{})
if len(vs) != 0 {
t.Errorf("expected 0 violations for 205 response without content, got %d", len(vs))
}
} |
||
|
|
||
| func TestCheckRule47_Valid200(t *testing.T) { | ||
| doc := &openapi3.T{ | ||
| OpenAPI: "3.0.0", | ||
| Info: &openapi3.Info{Title: "Test", Version: "v1"}, | ||
| Paths: openapi3.NewPaths(), | ||
| } | ||
| responses := openapi3.NewResponses() | ||
| desc := "Success" | ||
| responses.Set("200", &openapi3.ResponseRef{Value: &openapi3.Response{ | ||
| Description: &desc, | ||
| Content: openapi3.Content{ | ||
| "application/json": &openapi3.MediaType{ | ||
| Schema: &openapi3.SchemaRef{Value: openapi3.NewObjectSchema()}, | ||
| }, | ||
| }, | ||
| }}) | ||
| doc.Paths.Set("/api/test", &openapi3.PathItem{ | ||
| Get: &openapi3.Operation{Responses: responses}, | ||
| }) | ||
|
|
||
| vs := checkRule47("api.yml", doc, AuditOptions{}) | ||
| if len(vs) != 0 { | ||
| t.Errorf("expected 0 violations for valid 200 response, got %d", len(vs)) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rule 47 should also exclude the
205 Reset Contentstatus code. According to RFC 9110, a 205 response MUST NOT include a content part, similar to 204. I've updated the comment to reflect this.