From 68c4710d0facf945f5ba181f41d7ed5dd5568ff2 Mon Sep 17 00:00:00 2001 From: nrevelant <62693975+nrevelant@users.noreply.github.com> Date: Fri, 18 Nov 2022 17:03:00 +0100 Subject: [PATCH] fix(handler): requested format with Accept header and suffix (#134) --- internal/api/net.go | 83 +++++---- internal/service/handler.go | 104 +++++------ .../service/mock_test/handler_get_test.go | 168 +++++++++++++++--- .../service/mock_test/runner_mock_test.go | 43 ++++- internal/service/util.go | 14 +- 5 files changed, 293 insertions(+), 119 deletions(-) diff --git a/internal/api/net.go b/internal/api/net.go index 23e92c56..5f518732 100644 --- a/internal/api/net.go +++ b/internal/api/net.go @@ -75,23 +75,10 @@ func RequestedFormat(r *http.Request) string { // first check explicit path path := r.URL.EscapedPath() - // Accept header value - hdrAcceptValue := r.Header.Get("Accept") - // Extension value - splittedPath := strings.Split(path, "/") - pathEnd := splittedPath[len(splittedPath)-1] - extension := "" - pos := strings.LastIndex(pathEnd, ".") - if pos != -1 { - extension = pathEnd[pos+1:] - } - - // TODO: case when extension and header Accept are provided at the same time - // -> Bad Request ? - - if extension != "" && hdrAcceptValue == "" { - switch extension { + suffix := PathSuffix(path) + if suffix != "" { + switch suffix { case "json": return FormatJSON case "html": @@ -101,23 +88,38 @@ func RequestedFormat(r *http.Request) string { case "svg": return FormatSVG default: - return extension + return suffix } - } - - if hdrAcceptValue != "" { - switch hdrAcceptValue { - case ContentTypeJSON: - return FormatJSON - case ContentTypeSchemaJSON, ContentTypeSchemaPatchJSON: - return FormatSchemaJSON - case ContentTypeHTML: - return FormatHTML - case ContentTypeText: - return FormatText - case ContentTypeSVG: - return FormatSVG - default: + } else { + // Accept header value + hdrAcceptValue := r.Header.Get("Accept") + if hdrAcceptValue != "" { + // Accept header fields preferences: + // -> https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.1 + + // Examples: + // "Accept: application/json" + // "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8" + preferredFormats := strings.Split(hdrAcceptValue, ",") + for _, value := range preferredFormats { + mediaTypeValue := value + lastSemicolon := strings.LastIndex(value, ";") + if lastSemicolon > 0 { + mediaTypeValue = value[:lastSemicolon] // 'q' quality parameter not used + } + switch mediaTypeValue { + case ContentTypeJSON: + return FormatJSON + case ContentTypeSchemaJSON, ContentTypeSchemaPatchJSON: + return FormatSchemaJSON + case ContentTypeHTML: + return FormatHTML + case ContentTypeText: + return FormatText + case ContentTypeSVG: + return FormatSVG + } + } return hdrAcceptValue } } @@ -139,12 +141,25 @@ func SentDataFormat(r *http.Request) string { // PathStripFormat removes a format extension from a path func PathStripFormat(path string) string { - if strings.HasSuffix(path, ".html") || strings.HasSuffix(path, ".json") { - return path[0 : len(path)-5] + pos := strings.LastIndex(path, ".") + if pos != -1 { + path = path[:pos] } return path } +// PathSuffix returns the format extension from a path following a dot character +func PathSuffix(path string) string { + splittedPath := strings.Split(path, "/") + pathEnd := splittedPath[len(splittedPath)-1] + pos := strings.LastIndex(pathEnd, ".") + if pos != -1 { + return pathEnd[pos+1:] + } else { + return "" + } +} + // URLQuery gets the query part of a URL func URLQuery(url *url.URL) string { uri := url.RequestURI() diff --git a/internal/service/handler.go b/internal/service/handler.go index fa74ff10..cefe0f75 100644 --- a/internal/service/handler.go +++ b/internal/service/handler.go @@ -40,9 +40,10 @@ import ( ) const ( - routeVarID = "id" - routeVarFeatureID = "fid" - routeVarStrongEtag = "etag" + routeVarCollectionID = "cid" + routeVarFeatureID = "fid" + routeVarFunctionID = "funid" + routeVarStrongEtag = "etag" ) func InitRouter(basePath string) *mux.Router { @@ -52,9 +53,13 @@ func InitRouter(basePath string) *mux.Router { Subrouter() addRoute(router, "/", handleRoot) - addRoute(router, "/home{.fmt}", handleRoot) + + addRoute(router, "/home", handleRoot) + addRoute(router, "/home.{fmt}", handleRoot) + // consistent with pg_tileserv - addRoute(router, "/index{.fmt}", handleRoot) + addRoute(router, "/index", handleRoot) + addRoute(router, "/index.{fmt}", handleRoot) addRoute(router, "/etags/decodestrong/{etag}", handleDecodeStrongEtag) @@ -67,36 +72,28 @@ func InitRouter(basePath string) *mux.Router { addRoute(router, "/collections", handleCollections) addRoute(router, "/collections.{fmt}", handleCollections) - addRoute(router, "/collections/{id}", handleCollection) - addRoute(router, "/collections/{id}.{fmt}", handleCollection) + addRoute(router, "/collections/{cid}", handleCollection) - addRoute(router, "/collections/{id}/items", handleCollectionItems) - addRoute(router, "/collections/{id}/items.{fmt}", handleCollectionItems) + addRoute(router, "/collections/{cid}/items", handleCollectionItems) + addRoute(router, "/collections/{cid}/items.{fmt}", handleCollectionItems) if conf.Configuration.Database.AllowWrite { - addRouteWithMethod(router, "/collections/{id}/items", handleCreateCollectionItem, "POST") - addRouteWithMethod(router, "/collections/{id}/items/{fid}", handleDeleteCollectionItem, "DELETE") + addRouteWithMethod(router, "/collections/{cid}/items", handleCreateCollectionItem, "POST") + addRouteWithMethod(router, "/collections/{cid}/items/{fid}", handleDeleteCollectionItem, "DELETE") + addRouteWithMethod(router, "/collections/{cid}/items/{fid}", handlePartialUpdateItem, "PATCH") + addRouteWithMethod(router, "/collections/{cid}/items/{fid}", handleReplaceItem, "PUT") - addRoute(router, "/collections/{id}/schema", handleCollectionSchemas) + addRoute(router, "/collections/{cid}/schema", handleCollectionSchemas) } - addRoute(router, "/collections/{id}/items/{fid}", handleItem) - addRoute(router, "/collections/{id}/items/{fid}.{fmt}", handleItem) - - addRouteWithMethod(router, "/collections/{id}/items/{fid}", handlePartialUpdateItem, "PATCH") - addRouteWithMethod(router, "/collections/{id}/items/{fid}.{fmt}", handlePartialUpdateItem, "PATCH") - - addRouteWithMethod(router, "/collections/{id}/items/{fid}", handleReplaceItem, "PUT") - addRouteWithMethod(router, "/collections/{id}/items/{fid}.{fmt}", handleReplaceItem, "PUT") + addRoute(router, "/collections/{cid}/items/{fid}", handleItem) addRoute(router, "/functions", handleFunctions) addRoute(router, "/functions.{fmt}", handleFunctions) - addRoute(router, "/functions/{id}", handleFunction) - addRoute(router, "/functions/{id}.{fmt}", handleFunction) + addRoute(router, "/functions/{funid}", handleFunction) - addRoute(router, "/functions/{id}/items", handleFunctionItems) - addRoute(router, "/functions/{id}/items.{fmt}", handleFunctionItems) + addRoute(router, "/functions/{funid}/items", handleFunctionItems) return router } @@ -178,7 +175,7 @@ func linkAlt(urlBase string, path string, desc string) *api.Link { func handleDecodeStrongEtag(w http.ResponseWriter, r *http.Request) *appError { //--- extract request parameters - etag := getRequestVar(routeVarStrongEtag, r) + etag := getRequestVarStrip(routeVarStrongEtag, r) decodedEtag, err := api.DecodeStrongEtag(etag) if err != nil { return appErrorBadRequest(err, "Malformed etag") @@ -271,7 +268,9 @@ func handleCollection(w http.ResponseWriter, r *http.Request) *appError { format := api.RequestedFormat(r) urlBase := serveURLBase(r) - name := getRequestVar(routeVarID, r) + // the collection is at the end of the URL, this is why we strip the extension + // it may be an issue if the schema name is provided here + name := getRequestVarStrip(routeVarCollectionID, r) tbl, err := catalogInstance.TableByName(name) if tbl == nil && err == nil { @@ -309,7 +308,7 @@ func handleCollectionSchemas(w http.ResponseWriter, r *http.Request) *appError { format := api.RequestedFormat(r) //--- extract request parameters - name := getRequestVar(routeVarID, r) + name := getRequestVar(routeVarCollectionID, r) tbl, err1 := catalogInstance.TableByName(name) if err1 != nil { return appErrorInternal(err1, api.ErrMsgCollectionAccess, name) @@ -349,7 +348,7 @@ func handleCreateCollectionItem(w http.ResponseWriter, r *http.Request) *appErro urlBase := serveURLBase(r) //--- extract request parameters - name := getRequestVar(routeVarID, r) + name := getRequestVar(routeVarCollectionID, r) //--- check query parameters queryValues := r.URL.Query() @@ -398,8 +397,8 @@ func handleCreateCollectionItem(w http.ResponseWriter, r *http.Request) *appErro func handleDeleteCollectionItem(w http.ResponseWriter, r *http.Request) *appError { //--- extract request parameters - name := getRequestVar(routeVarID, r) - fid := getRequestVar(routeVarFeatureID, r) + name := getRequestVar(routeVarCollectionID, r) + fid := getRequestVarStrip(routeVarFeatureID, r) //--- check request parameters index, err := strconv.Atoi(fid) @@ -433,13 +432,14 @@ func handleDeleteCollectionItem(w http.ResponseWriter, r *http.Request) *appErro } func handleCollectionItems(w http.ResponseWriter, r *http.Request) *appError { + // "/collections/{id}/items" // TODO: determine content from request header? format := api.RequestedFormat(r) urlBase := serveURLBase(r) query := api.URLQuery(r.URL) //--- extract request parameters - name := getRequestVar(routeVarID, r) + name := getRequestVar(routeVarCollectionID, r) reqParam, err := parseRequestParams(r) if err != nil { return appErrorBadRequest(err, err.Error()) @@ -452,20 +452,22 @@ func handleCollectionItems(w http.ResponseWriter, r *http.Request) *appError { if tbl == nil { return appErrorNotFound(err1, api.ErrMsgCollectionNotFound, name) } - param, err := createQueryParams(&reqParam, tbl.Columns, tbl.Srid) - if err != nil { - return appErrorBadRequest(err, err.Error()) - } + param, errQuery := createQueryParams(&reqParam, tbl.Columns, tbl.Srid) param.Filter = parseFilter(reqParam.Values, tbl.DbTypes) - ctx := r.Context() - switch format { - case api.FormatJSON: - return writeItemsJSON(ctx, w, name, param, urlBase) - case api.FormatHTML: - return writeItemsHTML(w, tbl, name, query, urlBase) + if errQuery == nil { + ctx := r.Context() + switch format { + case api.FormatJSON: + return writeItemsJSON(ctx, w, name, param, urlBase) + case api.FormatHTML: + return writeItemsHTML(w, tbl, name, query, urlBase) + default: + return appErrorNotAcceptable(nil, api.ErrMsgNotSupportedFormat, format) + } + } else { + return appErrorBadRequest(errQuery, api.ErrMsgInvalidQuery) } - return nil } func writeCreateItemSchemaJSON(ctx context.Context, w http.ResponseWriter, table *api.Table) *appError { @@ -639,8 +641,8 @@ func handleItem(w http.ResponseWriter, r *http.Request) *appError { query := api.URLQuery(r.URL) //--- extract request parameters - name := getRequestVar(routeVarID, r) - fid := getRequestVar(routeVarFeatureID, r) + name := getRequestVar(routeVarCollectionID, r) + fid := getRequestVarStrip(routeVarFeatureID, r) reqParam, err := parseRequestParams(r) if err != nil { return appErrorBadRequest(err, err.Error()) @@ -697,8 +699,8 @@ func handleItem(w http.ResponseWriter, r *http.Request) *appError { func handlePartialUpdateItem(w http.ResponseWriter, r *http.Request) *appError { // extract request parameters - name := getRequestVar(routeVarID, r) - fid := getRequestVar(routeVarFeatureID, r) + name := getRequestVar(routeVarCollectionID, r) + fid := getRequestVarStrip(routeVarFeatureID, r) // check query parameters queryValues := r.URL.Query() @@ -752,8 +754,8 @@ func handlePartialUpdateItem(w http.ResponseWriter, r *http.Request) *appError { func handleReplaceItem(w http.ResponseWriter, r *http.Request) *appError { // extract request parameters - name := getRequestVar(routeVarID, r) - fid := getRequestVar(routeVarFeatureID, r) + name := getRequestVar(routeVarCollectionID, r) + fid := getRequestVarStrip(routeVarFeatureID, r) // check query parameters queryValues := r.URL.Query() @@ -976,7 +978,7 @@ func handleFunction(w http.ResponseWriter, r *http.Request) *appError { format := api.RequestedFormat(r) urlBase := serveURLBase(r) - shortName := getRequestVar(routeVarID, r) + shortName := getRequestVarStrip(routeVarFunctionID, r) name := data.FunctionQualifiedId(shortName) fn, err := catalogInstance.FunctionByName(name) @@ -1017,7 +1019,7 @@ func handleFunctionItems(w http.ResponseWriter, r *http.Request) *appError { urlBase := serveURLBase(r) //--- extract request parameters - name := data.FunctionQualifiedId(getRequestVar(routeVarID, r)) + name := data.FunctionQualifiedId(getRequestVarStrip(routeVarFunctionID, r)) reqParam, err := parseRequestParams(r) if err != nil { return appErrorBadRequest(err, err.Error()) @@ -1033,7 +1035,7 @@ func handleFunctionItems(w http.ResponseWriter, r *http.Request) *appError { return appErrorBadRequest(err, err.Error()) } fnArgs := restrict(reqParam.Values, fn.InNames) - //log.Debugf("Function request args: %v ", fnArgs) + // log.Debugf("Function request args: %v ", fnArgs) ctx := r.Context() switch format { diff --git a/internal/service/mock_test/handler_get_test.go b/internal/service/mock_test/handler_get_test.go index 95abc0a9..a694d3ca 100644 --- a/internal/service/mock_test/handler_get_test.go +++ b/internal/service/mock_test/handler_get_test.go @@ -1,7 +1,7 @@ package mock_test /* - Copyright 2019 Crunchy Data Solutions, Inc. + Copyright 2022 Crunchy Data Solutions, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -11,6 +11,11 @@ package mock_test WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + + Date : October 2022 + Authors : Benoit De Mezzo (benoit dot de dot mezzo at oslandia dot com) + Jean-philippe Bazonnais (jean-philippe dot bazonnais at ign dot fr) + Nicolas Revelant (nicolas dot revelant at ign dot fr) */ import ( @@ -43,6 +48,140 @@ func (t *MockTests) TestRoot() { }) } +func (t *MockTests) TestGetFormatHandlingWithAcceptHeader() { + t.Test.Run("TestGetFormatHandlingWithAcceptHeader", func(t *testing.T) { + // This test targets the RequestedFormat() function from the net.go file + + // route / + "Accept: application/json" + jsonBody := checkRouteWithAcceptHeader(t, "/", "application/json", http.StatusOK, api.ContentTypeJSON) + jsonMap := new(map[string]interface{}) + errUnMarsh := json.Unmarshal(jsonBody, &jsonMap) + util.Assert(t, errUnMarsh == nil, fmt.Sprintf("%v", errUnMarsh)) + + checkRouteWithAcceptHeader(t, "/", "text/html", http.StatusOK, api.ContentTypeHTML) + + checkRouteWithAcceptHeader(t, "/api", "*/*", http.StatusOK, api.ContentTypeJSON) + + // Browser tests + // ------------- + // route /api + default Accept header from Firefox 92 + firefoxAcceptHdr := "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8" + checkRouteWithAcceptHeader(t, "/api", firefoxAcceptHdr, http.StatusOK, "") + + // route /api + default Accept header from Safari/Chrome + chromeAcceptHdr := "text/html, application/xhtml+xml, image/jxr, */*" + checkRouteWithAcceptHeader(t, "/api", chromeAcceptHdr, http.StatusOK, "") + + // route /api + default Accept header from Opera + operaAcceptHdr := "text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/webp, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1" + checkRouteWithAcceptHeader(t, "/api", operaAcceptHdr, http.StatusOK, "") + + // route /api + Accept header with a supported format present in the middle of the value + messyHtmlAcceptHdr := "application/xhtml+xml,application/xml;q=0.9,image/avif,text/html,image/webp,*/*;q=0.8" + checkRouteWithAcceptHeader(t, "/api", messyHtmlAcceptHdr, http.StatusOK, "") + }) +} + +func (t *MockTests) TestGetFormatHandlingSuffix() { + t.Test.Run("TestGetFormatHandlingSuffix", func(t *testing.T) { + + // checking supported suffixes HTML and JSON, and missing suffix + checkRouteResponseFormat(t, "/home", api.ContentTypeJSON) + checkRouteResponseFormat(t, "/home.html", api.ContentTypeHTML) + checkRouteResponseFormat(t, "/home.json", api.ContentTypeJSON) + checkRouteResponseFormat(t, "/index", api.ContentTypeJSON) + checkRouteResponseFormat(t, "/index.html", api.ContentTypeHTML) + checkRouteResponseFormat(t, "/index.json", api.ContentTypeJSON) + checkRouteResponseFormat(t, "/api", api.ContentTypeJSON) + checkRouteResponseFormat(t, "/api.html", api.ContentTypeHTML) + checkRouteResponseFormat(t, "/api.json", api.ContentTypeJSON) + checkRouteResponseFormat(t, "/collections", api.ContentTypeJSON) + checkRouteResponseFormat(t, "/collections/mock_a", api.ContentTypeJSON) + checkRouteResponseFormat(t, "/collections/mock_a.html", api.ContentTypeHTML) + checkRouteResponseFormat(t, "/collections/mock_a.json", api.ContentTypeJSON) + checkRouteResponseFormat(t, "/collections/mock_a/items", api.ContentTypeGeoJSON) + checkRouteResponseFormat(t, "/collections/mock_a/items.html", api.ContentTypeHTML) + checkRouteResponseFormat(t, "/collections/mock_a/items.json", api.ContentTypeGeoJSON) + checkRouteResponseFormat(t, "/collections/mock_a/items/1", api.ContentTypeGeoJSON) + checkRouteResponseFormat(t, "/collections/mock_a/items/1.html", api.ContentTypeHTML) + checkRouteResponseFormat(t, "/collections/mock_a/items/1.json", api.ContentTypeGeoJSON) + checkRouteResponseFormat(t, "/functions", api.ContentTypeJSON) + checkRouteResponseFormat(t, "/functions.html", api.ContentTypeHTML) + checkRouteResponseFormat(t, "/functions.json", api.ContentTypeJSON) + checkRouteResponseFormat(t, "/functions/fun_a", api.ContentTypeJSON) + checkRouteResponseFormat(t, "/functions/fun_a.html", api.ContentTypeHTML) + checkRouteResponseFormat(t, "/functions/fun_a.json", api.ContentTypeJSON) + // TODO : /functions/{id}/items + checkRouteResponseFormat(t, "/conformance", api.ContentTypeJSON) + checkRouteResponseFormat(t, "/conformance.html", api.ContentTypeHTML) + checkRouteResponseFormat(t, "/conformance.json", api.ContentTypeJSON) + + }) +} + +func (t *MockTests) TestGetFormatHeaderAcceptUnsupportedMimeType() { + t.Test.Run("TestGetFormatHeaderAcceptUnsupportedMimeType", func(t *testing.T) { + + gifAccept := "image/gif" + xmlAccept := "application/xml" + var dummyAcceptHdr = make(http.Header) + dummyAcceptHdr.Add("Accept", "dummy/format") + // Root + checkRouteWithAcceptHeader(t, "/", gifAccept, http.StatusOK, api.ContentTypeJSON) + checkRouteWithAcceptHeader(t, "/", xmlAccept, http.StatusOK, api.ContentTypeJSON) + + // Api + checkRouteWithAcceptHeader(t, "/api", gifAccept, http.StatusOK, api.ContentTypeJSON) + + // Collections + checkRouteWithAcceptHeader(t, "/collections", gifAccept, http.StatusOK, api.ContentTypeJSON) + checkRouteWithAcceptHeader(t, "/collections/mock_a", gifAccept, http.StatusOK, api.ContentTypeJSON) + + // GET item(s) + hTest.DoRequestMethodStatus(t, "GET", "/collections/mock_a/items", nil, dummyAcceptHdr, http.StatusNotAcceptable) + hTest.DoRequestMethodStatus(t, "GET", "/collections/mock_a/items/1", nil, dummyAcceptHdr, http.StatusNotAcceptable) + + }) +} + +func (t *MockTests) TestGetFormatSuffixSupersedesAcceptHeader() { + t.Test.Run("TestGetFormatSuffixSupersedesAcceptHeader", func(t *testing.T) { + + htmlAccept := "text/html" + checkRouteWithAcceptHeader(t, "/api", htmlAccept, http.StatusOK, api.ContentTypeHTML) + checkRouteWithAcceptHeader(t, "/api.json", htmlAccept, http.StatusOK, api.ContentTypeJSON) + checkRouteWithAcceptHeader(t, "/api.html", htmlAccept, http.StatusOK, api.ContentTypeHTML) + + }) +} + +func (t *MockTests) TestFeatureFormats() { + t.Test.Run("TestFeatureFormats", func(t *testing.T) { + + hTest.DoRequestStatus(t, "/collections/mock_a/items/1.dummyformat", http.StatusNotAcceptable) + + path := "/collections/mock_a/items/1" + + // From header Accept + var header = make(http.Header) + header.Add("Accept", "json") + resp := hTest.DoRequestMethodStatus(t, "GET", path, nil, header, http.StatusOK) + var geoJsonStruct api.GeojsonFeatureData + errUnMarsh := json.Unmarshal(hTest.ReadBody(resp), &geoJsonStruct) + util.Assert(t, errUnMarsh == nil, fmt.Sprintf("%v", errUnMarsh)) + + // TODO HTML + // TODO SVG + // TODO TEXT + + // From URL extension + resp2 := hTest.DoRequestStatus(t, "/collections/mock_a/items/1.json", http.StatusOK) + errUnMarsh2 := json.Unmarshal(hTest.ReadBody(resp2), &geoJsonStruct) + util.Assert(t, errUnMarsh == nil, fmt.Sprintf("%v", errUnMarsh2)) + + }) +} + func (t *MockTests) TestCollectionsResponse() { t.Test.Run("TestCollectionsResponse", func(t *testing.T) { path := "/collections" @@ -115,33 +254,6 @@ func (t *MockTests) TestCollectionItem() { }) } -func (t *MockTests) TestFeatureFormats() { - t.Test.Run("TestFeatureFormats", func(t *testing.T) { - - hTest.DoRequestStatus(t, "/collections/mock_a/items/1.dummyformat", http.StatusNotAcceptable) - - path := "/collections/mock_a/items/1" - - // From header Accept - var header = make(http.Header) - header.Add("Accept", "json") - resp := hTest.DoRequestMethodStatus(t, "GET", path, nil, header, http.StatusOK) - var geoJsonStruct api.GeojsonFeatureData - errUnMarsh := json.Unmarshal(hTest.ReadBody(resp), &geoJsonStruct) - util.Assert(t, errUnMarsh == nil, fmt.Sprintf("%v", errUnMarsh)) - - // TODO HTML - // TODO SVG - // TODO TEXT - - // From URL extension - resp2 := hTest.DoRequestStatus(t, "/collections/mock_a/items/1.json", http.StatusOK) - errUnMarsh2 := json.Unmarshal(hTest.ReadBody(resp2), &geoJsonStruct) - util.Assert(t, errUnMarsh == nil, fmt.Sprintf("%v", errUnMarsh2)) - - }) -} - func (t *MockTests) TestCollectionItemPropertiesEmpty() { t.Test.Run("TestCollectionItemPropertiesEmpty", func(t *testing.T) { rr := hTest.DoRequest(t, "/collections/mock_a/items/1?properties=") diff --git a/internal/service/mock_test/runner_mock_test.go b/internal/service/mock_test/runner_mock_test.go index e3579ce5..bea2b322 100644 --- a/internal/service/mock_test/runner_mock_test.go +++ b/internal/service/mock_test/runner_mock_test.go @@ -1,7 +1,7 @@ package mock_test /* - Copyright 2019 Crunchy Data Solutions, Inc. + Copyright 2022 Crunchy Data Solutions, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -11,14 +11,19 @@ package mock_test WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + Date : October 2022 + Authors : Jean-philippe Bazonnais (jean-philippe dot bazonnais at ign dot fr) + Nicolas Revelant (nicolas dot revelant at ign dot fr) */ import ( "encoding/json" "fmt" "io/ioutil" + "net/http" "os" "strconv" + "strings" "testing" "github.com/CrunchyData/pg_featureserv/internal/api" @@ -55,6 +60,12 @@ func TestRunnerHandlerMock(t *testing.T) { t.Run("GET", func(t *testing.T) { m := MockTests{Test: t} + m.TestRoot() + m.TestGetFormatHandlingWithAcceptHeader() + m.TestGetFormatHandlingSuffix() + m.TestGetFormatHeaderAcceptUnsupportedMimeType() + m.TestGetFormatSuffixSupersedesAcceptHeader() + m.TestFeatureFormats() m.TestCollectionItem() m.TestCollectionItemsResponse() m.TestCollectionMissingItemsNotFound() @@ -63,7 +74,6 @@ func TestRunnerHandlerMock(t *testing.T) { m.TestCollectionResponse() m.TestCollectionsResponse() m.TestFeatureNotFound() - m.TestFeatureFormats() }) t.Run("CACHE AND ETAGS", func(t *testing.T) { m := MockTests{Test: t} @@ -295,3 +305,32 @@ func checkItem(t *testing.T, id int) []byte { return body } + +// sends a GET request and checks the expected format (Content-Type header) from the response +func checkRouteResponseFormat(t *testing.T, url string, expectedContentType string) { + + resp := hTest.DoRequestStatus(t, url, http.StatusOK) + respContentType := resp.Result().Header["Content-Type"][0] + util.Assert(t, respContentType == expectedContentType, fmt.Sprintf("wrong Content-Type: %s", respContentType)) +} + +// sends a GET request with the specific Accept header provided, and checks the response received for : +// - the expected status +// - the expected format provided, or according to the initial Accept header (Content-Type header) +func checkRouteWithAcceptHeader(t *testing.T, url string, acceptValue string, expectedStatus int, expectedFormat string) []byte { + + var acceptHeader = make(http.Header) + acceptHeader.Add("Accept", acceptValue) + + resp := hTest.DoRequestMethodStatus(t, "GET", url, nil, acceptHeader, expectedStatus) + contentType := resp.Result().Header["Content-Type"][0] + + if expectedFormat != "" { + util.Assert(t, contentType == expectedFormat, fmt.Sprintf("Content-Type: %s", contentType)) + } else { + util.Assert(t, strings.Contains(acceptValue, contentType), fmt.Sprintf("Content-Type: %s", contentType)) + } + + body, _ := ioutil.ReadAll(resp.Body) + return body +} diff --git a/internal/service/util.go b/internal/service/util.go index d762ccfa..cb6a7d17 100644 --- a/internal/service/util.go +++ b/internal/service/util.go @@ -190,11 +190,17 @@ func serveURLBase(r *http.Request) string { return fmt.Sprintf("%v://%v%v/", ps, ph, path) } -func getRequestVar(varname string, r *http.Request) string { +// Return value for the requested path route variable +func getRequestVar(varName string, r *http.Request) string { vars := mux.Vars(r) - nameFull := vars[varname] - name := api.PathStripFormat(nameFull) - return name + return vars[varName] +} + +// Return value for the requested path route variable while stripping te extension +func getRequestVarStrip(varName string, r *http.Request) string { + vars := mux.Vars(r) + value := vars[varName] + return api.PathStripFormat(value) } // urlPathFormat provides a URL for the given base and path