From a2951b0639297cdfae0de3212a6df3ef58603316 Mon Sep 17 00:00:00 2001 From: Sam Calder-Mason Date: Tue, 11 Jun 2024 14:37:31 +1000 Subject: [PATCH] feat: Support RFC9110 (#167) * feat: Support RFC9110 * test: Add test cases for content type derivation and validation --- pkg/api/content_type.go | 40 ++++++++++------ pkg/api/content_type_test.go | 89 ++++++++++++++++++++++++++++++++++++ pkg/api/response.go | 2 +- 3 files changed, 115 insertions(+), 16 deletions(-) create mode 100644 pkg/api/content_type_test.go diff --git a/pkg/api/content_type.go b/pkg/api/content_type.go index ff3163c9..015100cd 100644 --- a/pkg/api/content_type.go +++ b/pkg/api/content_type.go @@ -1,6 +1,9 @@ package api -import "fmt" +import ( + "fmt" + "strings" +) type ContentType int @@ -20,27 +23,34 @@ func (c ContentType) String() string { case ContentTypeSSZ: return "application/octet-stream" case ContentTypeUnknown: - return "unknown" + return "application/unknown" } return "" } func DeriveContentType(accept string) ContentType { - switch accept { - case "application/json": - return ContentTypeJSON - case "*/*": - return ContentTypeJSON - case "application/yaml": - return ContentTypeYAML - case "application/octet-stream": - return ContentTypeSSZ - // TODO(sam.caldermason): HACK to support Nimbus - what should we do here? - case "application/octet-stream,application/json;q=0.9": - return ContentTypeSSZ + // Split the accept header by commas to handle multiple content types + acceptTypes := strings.Split(accept, ",") + for _, acceptType := range acceptTypes { + // Split each type by semicolon to handle q-values + parts := strings.Split(acceptType, ";") + contentType := strings.TrimSpace(parts[0]) + + switch contentType { + case "application/json": + return ContentTypeJSON + case "*/*": + return ContentTypeJSON + case "application/yaml": + return ContentTypeYAML + case "application/octet-stream": + return ContentTypeSSZ + } + } + // Default to JSON if they don't care what they get. - case "": + if accept == "" { return ContentTypeJSON } diff --git a/pkg/api/content_type_test.go b/pkg/api/content_type_test.go new file mode 100644 index 00000000..0eaf9125 --- /dev/null +++ b/pkg/api/content_type_test.go @@ -0,0 +1,89 @@ +package api_test + +import ( + "net/http" + "testing" + + "github.com/ethpandaops/checkpointz/pkg/api" + "github.com/stretchr/testify/assert" +) + +func TestDeriveContentType(t *testing.T) { + tests := []struct { + name string + accept string + expected api.ContentType + }{ + {"JSON", "application/json", api.ContentTypeJSON}, + {"Wildcard", "*/*", api.ContentTypeJSON}, + {"YAML", "application/yaml", api.ContentTypeYAML}, + {"SSZ", "application/octet-stream", api.ContentTypeSSZ}, + {"Unknown", "application/unknown", api.ContentTypeUnknown}, + {"Empty", "", api.ContentTypeJSON}, + {"QValue JSON", "application/json;q=0.8", api.ContentTypeJSON}, + {"QValue YAML", "application/yaml;q=0.5", api.ContentTypeYAML}, + {"QValue Multiple", "application/json;q=0.8, application/yaml;q=0.5", api.ContentTypeJSON}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := api.DeriveContentType(tt.accept) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestValidateContentType(t *testing.T) { + tests := []struct { + name string + contentType api.ContentType + accepting []api.ContentType + expectError bool + }{ + {"Valid JSON", api.ContentTypeJSON, []api.ContentType{api.ContentTypeJSON, api.ContentTypeYAML}, false}, + {"Invalid JSON", api.ContentTypeJSON, []api.ContentType{api.ContentTypeYAML}, true}, + {"Valid YAML", api.ContentTypeYAML, []api.ContentType{api.ContentTypeJSON, api.ContentTypeYAML}, false}, + {"Invalid YAML", api.ContentTypeYAML, []api.ContentType{api.ContentTypeJSON}, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := api.ValidateContentType(tt.contentType, tt.accepting) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestNewContentTypeFromRequest(t *testing.T) { + tests := []struct { + name string + accept string + expected api.ContentType + }{ + {"JSON", "application/json", api.ContentTypeJSON}, + {"Wildcard", "*/*", api.ContentTypeJSON}, + {"YAML", "application/yaml", api.ContentTypeYAML}, + {"SSZ", "application/octet-stream", api.ContentTypeSSZ}, + {"Unknown", "application/unknown", api.ContentTypeJSON}, + {"Empty", "", api.ContentTypeJSON}, + {"QValue JSON", "application/json;q=0.8", api.ContentTypeJSON}, + {"QValue YAML", "application/yaml;q=0.5", api.ContentTypeYAML}, + {"QValue Multiple", "application/json;q=0.8, application/yaml;q=0.5", api.ContentTypeJSON}, + {"Nimbus example", "application/octet-stream,application/json;q=0.9", api.ContentTypeSSZ}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, err := http.NewRequest("GET", "http://example.com", http.NoBody) + assert.NoError(t, err) + req.Header.Set("Accept", tt.accept) + + result := api.NewContentTypeFromRequest(req) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/pkg/api/response.go b/pkg/api/response.go index e7a71104..6af580b9 100644 --- a/pkg/api/response.go +++ b/pkg/api/response.go @@ -76,7 +76,7 @@ func NewBadRequestResponse(resolvers ContentTypeResolvers) *HTTPResponse { func NewUnsupportedMediaTypeResponse(resolvers ContentTypeResolvers) *HTTPResponse { return &HTTPResponse{ resolvers: resolvers, - StatusCode: http.StatusUnsupportedMediaType, + StatusCode: http.StatusNotAcceptable, Headers: make(map[string]string), ExtraData: make(map[string]interface{}), }