diff --git a/internal/api/net.go b/internal/api/net.go index 23e92c56..19b0a695 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,24 +88,41 @@ 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: - return hdrAcceptValue + } 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 + default: + return hdrAcceptValue + } + } } } return FormatJSON @@ -139,12 +143,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 acf1bd9d..839826d7 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,33 +72,27 @@ 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) - addRouteWithMethod(router, "/collections/{id}/items", handleCreateCollectionItem, "POST") - addRouteWithMethod(router, "/collections/{id}/items/{fid}", handleDeleteCollectionItem, "DELETE") + addRoute(router, "/collections/{cid}/items", handleCollectionItems) + addRoute(router, "/collections/{cid}/items.{fmt}", handleCollectionItems) + addRouteWithMethod(router, "/collections/{cid}/items", handleCreateCollectionItem, "POST") + addRouteWithMethod(router, "/collections/{cid}/items/{fid}", handleDeleteCollectionItem, "DELETE") - 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) + addRoute(router, "/collections/{cid}/items/{fid}", handleItem) - addRouteWithMethod(router, "/collections/{id}/items/{fid}", handlePartialUpdateItem, "PATCH") - addRouteWithMethod(router, "/collections/{id}/items/{fid}.{fmt}", handlePartialUpdateItem, "PATCH") + addRouteWithMethod(router, "/collections/{cid}/items/{fid}", handlePartialUpdateItem, "PATCH") - addRouteWithMethod(router, "/collections/{id}/items/{fid}", handleReplaceItem, "PUT") - addRouteWithMethod(router, "/collections/{id}/items/{fid}.{fmt}", handleReplaceItem, "PUT") + addRouteWithMethod(router, "/collections/{cid}/items/{fid}", handleReplaceItem, "PUT") 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 } @@ -175,7 +174,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") @@ -268,7 +267,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 { @@ -306,7 +307,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) @@ -346,7 +347,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() @@ -395,8 +396,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) @@ -430,13 +431,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()) @@ -449,20 +451,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 { @@ -636,8 +640,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()) @@ -694,8 +698,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() @@ -749,8 +753,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() @@ -973,7 +977,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) @@ -1014,7 +1018,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()) @@ -1030,7 +1034,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 66f1255d..9ea28451 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,136 @@ 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, "") + }) +} + +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 +250,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 c5ee6a72..0ecc7fdc 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" @@ -53,6 +58,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() @@ -61,7 +72,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} @@ -293,3 +303,29 @@ func checkItem(t *testing.T, id int) []byte { return body } + +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)) + +} + +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