github.com/go-spatial/go-wfs@v0.1.4-0.20190401000911-c9fba2bb5188/server/handlers_internal_test.go (about)

     1  ///////////////////////////////////////////////////////////////////////////////
     2  //
     3  // The MIT License (MIT)
     4  // Copyright (c) 2018 Jivan Amara
     5  //
     6  // Permission is hereby granted, free of charge, to any person obtaining a copy
     7  // of this software and associated documentation files (the "Software"), to
     8  // deal in the Software without restriction, including without limitation the
     9  // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    10  // sell copies of the Software, and to permit persons to whom the Software is
    11  // furnished to do so, subject to the following conditions:
    12  //
    13  // The above copyright notice and this permission notice shall be included in
    14  // all copies or substantial portions of the Software.
    15  //
    16  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    17  // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    18  // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
    19  // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
    20  // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
    21  // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
    22  // USE OR OTHER DEALINGS IN THE SOFTWARE.
    23  //
    24  ///////////////////////////////////////////////////////////////////////////////
    25  
    26  // jivan project handlers_internal_test.go
    27  
    28  // TODO: The package var serveAddress from server.go is used extensively here.  Update
    29  //	for safe test parallelism.
    30  
    31  package server
    32  
    33  import (
    34  	"bytes"
    35  	"context"
    36  	"encoding/json"
    37  	"fmt"
    38  	"io/ioutil"
    39  	"net/http"
    40  	"net/http/httptest"
    41  	"net/url"
    42  	"path"
    43  	"runtime"
    44  	"strings"
    45  	"testing"
    46  
    47  	"github.com/go-spatial/geom"
    48  	"github.com/go-spatial/geom/encoding/geojson"
    49  	"github.com/go-spatial/jivan/config"
    50  	"github.com/go-spatial/jivan/data_provider"
    51  	"github.com/go-spatial/jivan/wfs3"
    52  	"github.com/go-spatial/tegola/provider/gpkg"
    53  	"github.com/julienschmidt/httprouter"
    54  )
    55  
    56  var testingProvider data_provider.Provider
    57  
    58  func init() {
    59  	// Instantiate a provider from the codebase's testing gpkg.
    60  	_, thisFilePath, _, _ := runtime.Caller(0)
    61  	gpkgPath := path.Join(path.Dir(thisFilePath), "..", "test_data/athens-osm-20170921.gpkg")
    62  	gpkgConfig, err := gpkg.AutoConfig(gpkgPath)
    63  	if err != nil {
    64  		panic(err.Error())
    65  	}
    66  	gpkgTiler, err := gpkg.NewTileProvider(gpkgConfig)
    67  	if err != nil {
    68  		panic(err.Error())
    69  	}
    70  	testingProvider = data_provider.Provider{Tiler: gpkgTiler}
    71  
    72  	// This is the provider the server will use for data
    73  	Provider = testingProvider
    74  }
    75  
    76  func TestServeSchemeHostPortBase(t *testing.T) {
    77  	type TestCase struct {
    78  		requestScheme               string
    79  		requestHostPort             string
    80  		configURLHostPort           string
    81  		configURLScheme             string
    82  		configURLBasePath           string
    83  		expectedServeSchemeHostPort string
    84  	}
    85  
    86  	testCases := []TestCase{
    87  		// Check that w/ no config settings, everything is pulled from the request
    88  		{
    89  			requestScheme:               "http",
    90  			requestHostPort:             "someplace.com",
    91  			expectedServeSchemeHostPort: "http://someplace.com",
    92  		},
    93  		// Check that things work w/ an alternate port
    94  		{
    95  			requestScheme:               "http",
    96  			requestHostPort:             "someplace.com:7777",
    97  			expectedServeSchemeHostPort: "http://someplace.com:7777",
    98  		},
    99  		// Check that scheme setting works
   100  		{
   101  			requestScheme:               "https",
   102  			requestHostPort:             "someplace.com",
   103  			configURLHostPort:           "otherplace.com",
   104  			configURLScheme:             "https",
   105  			expectedServeSchemeHostPort: "https://otherplace.com",
   106  		},
   107  		// Check base path setting works
   108  		{
   109  			requestScheme:               "http",
   110  			requestHostPort:             "someplace.com",
   111  			configURLBasePath:           "/testdir",
   112  			configURLHostPort:           "otherplace.com",
   113  			expectedServeSchemeHostPort: "https://otherplace.com/testdir",
   114  		},
   115  		// Check base path w/ trailing slash
   116  		{
   117  			requestScheme:               "http",
   118  			requestHostPort:             "someplace.com",
   119  			configURLBasePath:           "/testdir/",
   120  			configURLHostPort:           "otherplace.com",
   121  			expectedServeSchemeHostPort: "https://otherplace.com/testdir",
   122  		},
   123  	}
   124  
   125  	originalURLScheme := config.Configuration.Server.URLScheme
   126  	originalURLHostPort := config.Configuration.Server.URLHostPort
   127  	originalURLBasePath := config.Configuration.Server.URLBasePath
   128  
   129  	defer func(ous, ohp, obp string) {
   130  		config.Configuration.Server.URLScheme = ous
   131  		config.Configuration.Server.URLHostPort = ohp
   132  		config.Configuration.Server.URLBasePath = obp
   133  	}(originalURLScheme, originalURLHostPort, originalURLBasePath)
   134  
   135  	for i, tc := range testCases {
   136  		url := fmt.Sprintf("%v://%v", tc.requestScheme, tc.requestHostPort)
   137  		req := httptest.NewRequest("GET", url, bytes.NewReader([]byte{}))
   138  
   139  		if tc.configURLScheme != "" {
   140  			config.Configuration.Server.URLScheme = tc.configURLScheme
   141  		}
   142  		if tc.configURLHostPort != "" {
   143  			config.Configuration.Server.URLHostPort = tc.configURLHostPort
   144  		}
   145  		if tc.configURLBasePath != "" {
   146  			config.Configuration.Server.URLBasePath = tc.configURLBasePath
   147  		}
   148  
   149  		sa := serveSchemeHostPortBase(req)
   150  		if sa != tc.expectedServeSchemeHostPort {
   151  			t.Errorf("[%v] serve address %v != %v", i, sa, tc.expectedServeSchemeHostPort)
   152  		}
   153  	}
   154  }
   155  
   156  func TestRoot(t *testing.T) {
   157  	serveAddress := "test.com"
   158  	rootUrl := fmt.Sprintf("http://%v/", serveAddress)
   159  
   160  	type TestCase struct {
   161  		requestMethod      string
   162  		goContent          interface{}
   163  		overrideContent    interface{}
   164  		contentType        string
   165  		expectedETag       string
   166  		expectedStatusCode int
   167  	}
   168  
   169  	testCases := []TestCase{
   170  		// Happy path GET test case
   171  		{
   172  			requestMethod: HTTPMethodGET,
   173  			goContent: &wfs3.RootContent{
   174  				Links: []*wfs3.Link{
   175  					{
   176  						Href: fmt.Sprintf("http://%v/", serveAddress),
   177  						Rel:  "self",
   178  						Type: "application/json",
   179  					},
   180  					{
   181  						Href: fmt.Sprintf("http://%v/?f=text%%2Fhtml", serveAddress),
   182  						Rel:  "alternate",
   183  						Type: "text/html",
   184  					},
   185  					{
   186  						Href: fmt.Sprintf("http://%v/api", serveAddress),
   187  						Rel:  "service",
   188  						Type: "application/json",
   189  					},
   190  					{
   191  						Href: fmt.Sprintf("http://%v/conformance", serveAddress),
   192  						Rel:  "conformance",
   193  						Type: "application/json",
   194  					},
   195  					{
   196  						Href: fmt.Sprintf("http://%v/collections", serveAddress),
   197  						Rel:  "data",
   198  						Type: "application/json",
   199  					},
   200  				},
   201  			},
   202  			contentType:        config.JSONContentType,
   203  			expectedETag:       "temp_content_id",
   204  			expectedStatusCode: 200,
   205  		},
   206  		// Happy path HEAD test case
   207  		{
   208  			requestMethod:      HTTPMethodHEAD,
   209  			goContent:          nil,
   210  			contentType:        "",
   211  			expectedETag:       "temp_content_id",
   212  			expectedStatusCode: 200,
   213  		},
   214  		// Schema error, Links type as []string instead of []wfs3.Link
   215  		{
   216  			requestMethod:      HTTPMethodGET,
   217  			goContent:          &HandlerError{Code: "NoApplicableCode", Description: "response doesn't match schema"},
   218  			overrideContent:    `{ links: ["http://doesntmatter.com"] }`,
   219  			expectedStatusCode: 500,
   220  		},
   221  	}
   222  
   223  	for i, tc := range testCases {
   224  		var expectedContent []byte
   225  		var err error
   226  		// --- Collect expected response body
   227  		switch gc := tc.goContent.(type) {
   228  		case *wfs3.RootContent:
   229  			expectedContent, err = json.Marshal(gc)
   230  			if err != nil {
   231  				t.Errorf("Problem marshalling expected content: %v", err)
   232  			}
   233  		case *HandlerError:
   234  			expectedContent, err = json.Marshal(gc)
   235  			if err != nil {
   236  				t.Errorf("Problem marshalling expected content: %v", err)
   237  			}
   238  		case nil:
   239  			expectedContent = []byte{}
   240  		default:
   241  			t.Errorf("[%v] Unexpected type in tc.goContent: %T", i, tc.goContent)
   242  		}
   243  
   244  		// --- override the content produced in the handler if requested by this test case
   245  		ctx := context.TODO()
   246  		if tc.overrideContent != nil {
   247  			oc, err := json.Marshal(tc.overrideContent)
   248  			if err != nil {
   249  				t.Errorf("[%v] Problem marshalling overrideContent: %v", i, err)
   250  			}
   251  			ctx = context.WithValue(ctx, "overrideContent", oc)
   252  		}
   253  
   254  		// --- perform the request & get the response
   255  		responseWriter := httptest.NewRecorder()
   256  		request := httptest.NewRequest(tc.requestMethod, rootUrl, bytes.NewBufferString("")).WithContext(ctx)
   257  
   258  		root(responseWriter, request)
   259  		resp := responseWriter.Result()
   260  
   261  		// --- check that the results match expected
   262  		if resp.StatusCode != tc.expectedStatusCode {
   263  			t.Errorf("[%v]: status code %v != %v", i, resp.StatusCode, tc.expectedStatusCode)
   264  		}
   265  
   266  		if tc.expectedETag != "" && (resp.Header.Get("ETag") != tc.expectedETag) {
   267  			t.Errorf("[%v]: ETag %v != %v", i, resp.Header.Get("ETag"), tc.expectedETag)
   268  		}
   269  
   270  		body, _ := ioutil.ReadAll(resp.Body)
   271  		if string(body) != string(expectedContent) {
   272  			t.Errorf("[%v] response body doesn't match expected", i)
   273  			reducedOutputError(t, body, expectedContent)
   274  		}
   275  	}
   276  }
   277  
   278  func TestApi(t *testing.T) {
   279  	// TODO: This is pretty circular logic, as the /api endpoint simply returns openapiSpecJson.
   280  	//	Make a better test plan.
   281  
   282  	serveAddress := "unittest.net"
   283  	apiUrl := fmt.Sprintf("http://%v/api", serveAddress)
   284  
   285  	type TestCase struct {
   286  		requestMethod      string
   287  		goContent          interface{}
   288  		overrideContent    interface{}
   289  		contentType        string
   290  		expectedETag       string
   291  		expectedStatusCode int
   292  	}
   293  
   294  	testCases := []TestCase{
   295  		// Happy-path GET request
   296  		{
   297  			requestMethod:      HTTPMethodGET,
   298  			goContent:          wfs3.OpenAPI3Schema(),
   299  			overrideContent:    nil,
   300  			contentType:        config.JSONContentType,
   301  			expectedETag:       "9594694f73aedc17",
   302  			expectedStatusCode: 200,
   303  		},
   304  		// Happy-path HEAD request
   305  		{
   306  			requestMethod:      HTTPMethodHEAD,
   307  			goContent:          nil,
   308  			overrideContent:    nil,
   309  			expectedETag:       "9594694f73aedc17",
   310  			expectedStatusCode: 200,
   311  		},
   312  	}
   313  
   314  	for i, tc := range testCases {
   315  		var expectedContent []byte
   316  		var err error
   317  		switch tc.contentType {
   318  		case config.JSONContentType:
   319  			expectedContent, err = json.Marshal(tc.goContent)
   320  			if err != nil {
   321  				t.Errorf("[%v] problem marshalling tc.goContent to JSON: %v", i, err)
   322  				return
   323  			}
   324  		case "":
   325  			expectedContent = []byte{}
   326  		default:
   327  			t.Errorf("[%v] unsupported content type: '%v'", i, tc.contentType)
   328  			return
   329  		}
   330  
   331  		responseWriter := httptest.NewRecorder()
   332  		rctx := context.WithValue(context.TODO(), "overrideContent", tc.overrideContent)
   333  		request := httptest.NewRequest(tc.requestMethod, apiUrl, bytes.NewBufferString("")).WithContext(rctx)
   334  		openapi(responseWriter, request)
   335  		resp := responseWriter.Result()
   336  
   337  		if resp.StatusCode != tc.expectedStatusCode {
   338  			t.Errorf("[%v] status code %v != %v", i, resp.StatusCode, tc.expectedStatusCode)
   339  		}
   340  
   341  		if tc.expectedETag != "" && (resp.Header.Get("ETag") != tc.expectedETag) {
   342  			t.Errorf("[%v] ETag %v != %v", i, resp.Header.Get("ETag"), tc.expectedETag)
   343  		}
   344  		body, _ := ioutil.ReadAll(resp.Body)
   345  		if string(body) != string(expectedContent) {
   346  			t.Errorf("[%v] response content doesn't match expected:", i)
   347  			reducedOutputError(t, body, expectedContent)
   348  		}
   349  	}
   350  }
   351  
   352  func TestConformance(t *testing.T) {
   353  	serveAddress := "tdd.uk"
   354  	conformanceUrl := fmt.Sprintf("http://%v/conformance", serveAddress)
   355  
   356  	type TestCase struct {
   357  		requestMethod      string
   358  		goContent          interface{}
   359  		overrideContent    interface{}
   360  		contentType        string
   361  		expectedETag       string
   362  		expectedStatusCode int
   363  	}
   364  
   365  	testCases := []TestCase{
   366  		// Happy-path GET request
   367  		{
   368  			requestMethod: HTTPMethodGET,
   369  			goContent: wfs3.ConformanceClasses{
   370  				ConformsTo: []string{
   371  					"http://www.opengis.net/spec/wfs-1/3.0/req/core",
   372  					"http://www.opengis.net/spec/wfs-1/3.0/req/geojson",
   373  				},
   374  			},
   375  			overrideContent:    nil,
   376  			contentType:        config.JSONContentType,
   377  			expectedETag:       "4385e7a21a681d7d",
   378  			expectedStatusCode: 200,
   379  		},
   380  		// Happy-path HEAD request
   381  		{
   382  			requestMethod:      HTTPMethodHEAD,
   383  			goContent:          nil,
   384  			overrideContent:    nil,
   385  			expectedETag:       "4385e7a21a681d7d",
   386  			expectedStatusCode: 200,
   387  		},
   388  	}
   389  
   390  	for i, tc := range testCases {
   391  		var expectedContent []byte
   392  		var err error
   393  		switch tc.contentType {
   394  		case config.JSONContentType:
   395  			expectedContent, err = json.Marshal(tc.goContent)
   396  			if err != nil {
   397  				t.Errorf("[%v] problem marshalling expected content to json: %v", i, err)
   398  				return
   399  			}
   400  		case "":
   401  			expectedContent = []byte{}
   402  		default:
   403  			t.Errorf("[%v] unexpected content type: %v", i, tc.contentType)
   404  			return
   405  		}
   406  
   407  		responseWriter := httptest.NewRecorder()
   408  		rctx := context.WithValue(context.TODO(), "overrideContent", tc.overrideContent)
   409  		request := httptest.NewRequest(tc.requestMethod, conformanceUrl, bytes.NewBufferString("")).WithContext(rctx)
   410  		conformance(responseWriter, request)
   411  		resp := responseWriter.Result()
   412  
   413  		if resp.StatusCode != tc.expectedStatusCode {
   414  			t.Errorf("status code %v != %v", resp.StatusCode, tc.expectedStatusCode)
   415  		}
   416  
   417  		if resp.Header.Get("ETag") != "" && (resp.Header.Get("ETag") != tc.expectedETag) {
   418  			t.Errorf("[%v] ETag %v != %v", i, resp.Header.Get("ETag"), tc.expectedETag)
   419  		}
   420  
   421  		body, err := ioutil.ReadAll(resp.Body)
   422  		if err != nil {
   423  			t.Errorf("Problem reading response: %v", err)
   424  		}
   425  
   426  		if string(body) != string(expectedContent) {
   427  			t.Errorf("[%v] response content doesn't match expected:", i)
   428  			reducedOutputError(t, body, expectedContent)
   429  		}
   430  	}
   431  }
   432  
   433  func TestCollectionsMetaData(t *testing.T) {
   434  	serveAddress := "extratesting.org:77"
   435  	collectionsUrl := fmt.Sprintf("http://%v/collections", serveAddress)
   436  
   437  	// Build the expected result
   438  	cNames, err := testingProvider.CollectionNames()
   439  	if err != nil {
   440  		t.Errorf("Problem getting collection names: %v", err)
   441  	}
   442  
   443  	csInfo := wfs3.CollectionsInfo{Links: []*wfs3.Link{}, Collections: []*wfs3.CollectionInfo{}}
   444  	// Set the self & alternate links
   445  	csInfo.Links = append(csInfo.Links, &wfs3.Link{Rel: "self", Href: collectionsUrl, Type: config.JSONContentType})
   446  	cURL, err := url.Parse(collectionsUrl)
   447  	if err != nil {
   448  		t.Errorf("Problem parsing collections URL: %v", err)
   449  	}
   450  	for _, sct := range config.SupportedContentTypes {
   451  		if sct == config.JSONContentType {
   452  			continue
   453  		}
   454  		url := cURL
   455  		q := url.Query()
   456  		q.Set("f", sct)
   457  		url.RawQuery = q.Encode()
   458  		csInfo.Links = append(csInfo.Links, &wfs3.Link{Rel: "alternate", Href: url.String(), Type: sct})
   459  	}
   460  	// Set the item links
   461  	for _, cn := range cNames {
   462  		basehref := fmt.Sprintf("%v/%v", collectionsUrl, cn)
   463  		csInfo.Links = append(csInfo.Links, &wfs3.Link{Rel: "item", Href: basehref, Type: "application/json"})
   464  		for _, sct := range []string{config.HTMLContentType} {
   465  			// Converting from a string to a URL then back to string correctly/consistently encodes elements.
   466  			ihref := fmt.Sprintf("%v?f=%v", basehref, sct)
   467  			iurl, err := url.Parse(ihref)
   468  			if err != nil {
   469  				t.Errorf("Unable to parase url string: '%v'", ihref)
   470  			}
   471  			iurl.RawQuery = iurl.Query().Encode()
   472  			csInfo.Links = append(csInfo.Links, &wfs3.Link{Rel: "item", Href: iurl.String(), Type: sct})
   473  		}
   474  	}
   475  
   476  	// Fill in the Collections property
   477  	for _, cn := range cNames {
   478  		collectionUrl := fmt.Sprintf("http://%v/collections/%v", serveAddress, cn)
   479  		collectionUrlHtml := fmt.Sprintf("http://%v/collections/%v?f=text%%2Fhtml", serveAddress, cn)
   480  		itemUrl := fmt.Sprintf("http://%v/collections/%v/items", serveAddress, cn)
   481  		itemUrlHtml := fmt.Sprintf("http://%v/collections/%v/items?f=text%%2Fhtml", serveAddress, cn)
   482  		cInfo := wfs3.CollectionInfo{Name: cn, Title: cn, Links: []*wfs3.Link{
   483  			{Rel: "self", Href: collectionUrl, Type: config.JSONContentType},
   484  			{Rel: "alternate", Href: collectionUrlHtml, Type: config.HTMLContentType},
   485  			{Rel: "item", Href: itemUrl, Type: config.JSONContentType},
   486  			{Rel: "item", Href: itemUrlHtml, Type: config.HTMLContentType},
   487  		}}
   488  
   489  		csInfo.Collections = append(csInfo.Collections, &cInfo)
   490  	}
   491  
   492  	type TestCase struct {
   493  		requestMethod      string
   494  		goContent          interface{}
   495  		overrideContent    interface{}
   496  		contentType        string
   497  		expectedETag       string
   498  		expectedStatusCode int
   499  	}
   500  
   501  	testCases := []TestCase{
   502  		// Happy-path GET request
   503  		{
   504  			requestMethod:      HTTPMethodGET,
   505  			goContent:          csInfo,
   506  			overrideContent:    nil,
   507  			contentType:        config.JSONContentType,
   508  			expectedETag:       "86c51f1263aa1e87",
   509  			expectedStatusCode: 200,
   510  		},
   511  		// Happy-path HEAD request
   512  		{
   513  			requestMethod:      HTTPMethodHEAD,
   514  			goContent:          nil,
   515  			overrideContent:    nil,
   516  			expectedETag:       "86c51f1263aa1e87",
   517  			expectedStatusCode: 200,
   518  		},
   519  	}
   520  
   521  	for i, tc := range testCases {
   522  		var expectedContent []byte
   523  		var err error
   524  		switch tc.contentType {
   525  		case config.JSONContentType:
   526  			expectedContent, err = json.Marshal(csInfo)
   527  			if err != nil {
   528  				t.Errorf("[%v] problem marshalling expected collections info to json: %v", i, err)
   529  				return
   530  			}
   531  		case "":
   532  			expectedContent = []byte{}
   533  		default:
   534  			t.Errorf("[%v] unsupported content type: %v", i, tc.contentType)
   535  			return
   536  		}
   537  
   538  		responseWriter := httptest.NewRecorder()
   539  		rctx := context.WithValue(context.TODO(), "overrideContent", tc.overrideContent)
   540  		request := httptest.NewRequest(tc.requestMethod, collectionsUrl, bytes.NewBufferString("")).WithContext(rctx)
   541  		collectionsMetaData(responseWriter, request)
   542  
   543  		resp := responseWriter.Result()
   544  		body, err := ioutil.ReadAll(resp.Body)
   545  		if err != nil {
   546  			t.Errorf("[%v] Problem reading response body: %v", i, err)
   547  		}
   548  
   549  		if tc.expectedETag != "" && (resp.Header.Get("ETag") != tc.expectedETag) {
   550  			t.Errorf("[%v] ETag %v != %v", i, resp.Header.Get("ETag"), tc.expectedETag)
   551  		}
   552  
   553  		if resp.StatusCode != tc.expectedStatusCode {
   554  			t.Errorf("[%v] Status code %v != %v", i, resp.StatusCode, tc.expectedStatusCode)
   555  		}
   556  
   557  		if string(body) != string(expectedContent) {
   558  			// These are nice if the reduced output doesn't give you enough context
   559  			// t.Logf("---")
   560  			// t.Logf("%v", string(body))
   561  			// t.Logf("---")
   562  			// t.Logf("%v", string(expectedContent))
   563  			// t.Logf("---")
   564  
   565  			t.Errorf("[%v] response content doesn't match expected", i)
   566  
   567  			reducedOutputError(t, body, expectedContent)
   568  		}
   569  	}
   570  }
   571  
   572  func TestSingleCollectionMetaData(t *testing.T) {
   573  	serveAddress := "testthis.com"
   574  
   575  	type TestCase struct {
   576  		requestMethod      string
   577  		goContent          interface{}
   578  		contentOverride    interface{}
   579  		contentType        string
   580  		expectedETag       string
   581  		expectedStatusCode int
   582  		urlParams          map[string]string
   583  	}
   584  
   585  	testCases := []TestCase{
   586  		// Happy-path GET request
   587  		{
   588  			requestMethod: HTTPMethodGET,
   589  			goContent: wfs3.CollectionInfo{
   590  				Name:  "roads_lines",
   591  				Title: "roads_lines",
   592  				Links: []*wfs3.Link{
   593  					{
   594  						Rel:  "self",
   595  						Href: fmt.Sprintf("http://%v/collections/%v", serveAddress, "roads_lines"),
   596  						Type: config.JSONContentType,
   597  					}, {
   598  						Rel:  "alternate",
   599  						Href: fmt.Sprintf("http://%v/collections/%v?f=text%%2Fhtml", serveAddress, "roads_lines"),
   600  						Type: config.HTMLContentType,
   601  					},
   602  					{
   603  						Rel:  "item",
   604  						Href: fmt.Sprintf("http://%v/collections/%v/items", serveAddress, "roads_lines"),
   605  						Type: config.JSONContentType,
   606  					}, {
   607  						Rel:  "item",
   608  						Href: fmt.Sprintf("http://%v/collections/%v/items?f=text%%2Fhtml", serveAddress, "roads_lines"),
   609  						Type: config.HTMLContentType,
   610  					},
   611  				},
   612  			},
   613  			contentOverride:    nil,
   614  			contentType:        config.JSONContentType,
   615  			expectedETag:       "a3020c6917d284ef",
   616  			expectedStatusCode: 200,
   617  			urlParams:          map[string]string{"name": "roads_lines"},
   618  		},
   619  		// Happy-path HEAD request
   620  		{
   621  			requestMethod:      HTTPMethodHEAD,
   622  			goContent:          nil,
   623  			contentOverride:    nil,
   624  			expectedETag:       "a3020c6917d284ef",
   625  			expectedStatusCode: 200,
   626  			urlParams:          map[string]string{"name": "roads_lines"},
   627  		},
   628  	}
   629  
   630  	for i, tc := range testCases {
   631  		url := fmt.Sprintf("http://%v/collections/%v", serveAddress, tc.urlParams["name"])
   632  
   633  		var expectedContent []byte
   634  		var err error
   635  		switch tc.contentType {
   636  		case config.JSONContentType:
   637  			expectedContent, err = json.Marshal(tc.goContent)
   638  			if err != nil {
   639  				t.Errorf("[%v] Problem marshalling expected collection info: %v", i, err)
   640  				return
   641  			}
   642  		case "":
   643  			expectedContent = []byte{}
   644  		default:
   645  			t.Errorf("[%v] Unexpected content type: %v", err, tc.contentType)
   646  			return
   647  		}
   648  
   649  		responseWriter := httptest.NewRecorder()
   650  		hrParams := make(httprouter.Params, 0, len(tc.urlParams))
   651  		for k, v := range tc.urlParams {
   652  			hrParams = append(hrParams, httprouter.Param{Key: k, Value: v})
   653  		}
   654  
   655  		request := httptest.NewRequest(tc.requestMethod, url, bytes.NewBufferString(""))
   656  		rctx := context.WithValue(request.Context(), httprouter.ParamsKey, hrParams)
   657  		rctx = context.WithValue(rctx, "contentOverride", tc.contentOverride)
   658  		request = request.WithContext(rctx)
   659  
   660  		collectionMetaData(responseWriter, request)
   661  		resp := responseWriter.Result()
   662  		body, err := ioutil.ReadAll(resp.Body)
   663  		if err != nil {
   664  			t.Errorf("[%v] Problem reading response body: %v", i, err)
   665  		}
   666  
   667  		if tc.expectedETag != "" && (resp.Header.Get("ETag") != tc.expectedETag) {
   668  			t.Errorf("[%v] ETag %v != %v", i, resp.Header.Get("ETag"), tc.expectedETag)
   669  		}
   670  		if resp.StatusCode != tc.expectedStatusCode {
   671  			t.Errorf("[%v] Status code %v != %v", i, resp.StatusCode, tc.expectedStatusCode)
   672  		}
   673  		if string(body) != string(expectedContent) {
   674  			t.Errorf("[%v] result content doesn't match expected", i)
   675  			reducedOutputError(t, body, expectedContent)
   676  		}
   677  	}
   678  }
   679  
   680  func uint64ptr(i uint64) *uint64 {
   681  	return &i
   682  }
   683  
   684  func TestCollectionFeatures(t *testing.T) {
   685  	serveAddress := "test.com"
   686  
   687  	type TestCase struct {
   688  		requestMethod      string
   689  		goContent          interface{}
   690  		contentOverride    interface{}
   691  		contentType        string
   692  		expectedETag       string
   693  		expectedStatusCode int
   694  		urlParams          map[string]string
   695  		queryParams        map[string]string
   696  	}
   697  
   698  	testCases := []TestCase{
   699  		// Happy-path GET request
   700  		{
   701  			requestMethod: HTTPMethodGET,
   702  			goContent: wfs3.FeatureCollection{
   703  				Links: []*wfs3.Link{
   704  					{Rel: "self", Href: fmt.Sprintf("http://%v/collections/aviation_polygons/items?limit=3&page=1", serveAddress), Type: "application/json"},
   705  					{Rel: "alternate", Href: fmt.Sprintf("http://%v/collections/aviation_polygons/items?f=text%%2Fhtml&limit=3&page=1", serveAddress), Type: "text/html"},
   706  					{Rel: "prev", Href: fmt.Sprintf("http://%v/collections/aviation_polygons/items?limit=3&page=0", serveAddress), Type: "application/json"},
   707  					{Rel: "next", Href: fmt.Sprintf("http://%v/collections/aviation_polygons/items?limit=3&page=2", serveAddress), Type: "application/json"},
   708  				},
   709  				NumberMatched:  8,
   710  				NumberReturned: 3,
   711  				// Populate the embedded geojson FeatureCollection
   712  				FeatureCollection: geojson.FeatureCollection{
   713  					Features: []geojson.Feature{
   714  						{
   715  							ID: uint64ptr(4),
   716  							Geometry: geojson.Geometry{
   717  								Geometry: geom.Polygon{
   718  									{
   719  										{23.7393297, 37.8862976},
   720  										{23.7392296, 37.8862617},
   721  										{23.7392581, 37.8862122},
   722  										{23.7385715, 37.8859662},
   723  										{23.7384902, 37.8861076},
   724  										{23.7391751, 37.8863529},
   725  										{23.7391999, 37.8863097},
   726  										{23.7393018, 37.8863462},
   727  										{23.7393297, 37.8862976},
   728  									},
   729  								},
   730  							},
   731  							Properties: map[string]interface{}{
   732  								"aeroway":    "terminal",
   733  								"building":   "yes",
   734  								"osm_way_id": "191315126",
   735  							},
   736  						},
   737  						{
   738  							ID: uint64ptr(5),
   739  							Geometry: geojson.Geometry{
   740  								Geometry: geom.Polygon{
   741  									{
   742  										{23.7400581, 37.8850307},
   743  										{23.7400919, 37.884972},
   744  										{23.7399529, 37.8849222},
   745  										{23.739979, 37.8848768},
   746  										{23.739275, 37.8846247},
   747  										{23.7391938, 37.884766},
   748  										{23.73991, 37.8850225},
   749  										{23.7399314, 37.8849853},
   750  										{23.7400581, 37.8850307},
   751  									},
   752  								},
   753  							},
   754  							Properties: map[string]interface{}{
   755  								"aeroway":    "terminal",
   756  								"building":   "yes",
   757  								"osm_way_id": "191315130",
   758  							},
   759  						},
   760  						{
   761  							ID: uint64ptr(6),
   762  							Geometry: geojson.Geometry{
   763  								Geometry: geom.Polygon{
   764  									{
   765  										{23.739719, 37.8856206},
   766  										{23.7396799, 37.8856886},
   767  										{23.739478, 37.8860396},
   768  										{23.7398555, 37.8861748},
   769  										{23.7398922, 37.886111},
   770  										{23.7402413, 37.8855038},
   771  										{23.7402659, 37.8854609},
   772  										{23.7402042, 37.8854388},
   773  										{23.7398885, 37.8853257},
   774  										{23.739719, 37.8856206},
   775  									},
   776  								},
   777  							},
   778  							Properties: map[string]interface{}{
   779  								"aeroway":    "terminal",
   780  								"building":   "yes",
   781  								"osm_way_id": "191315133",
   782  							},
   783  						},
   784  					},
   785  				},
   786  			},
   787  			contentOverride:    nil,
   788  			contentType:        config.JSONContentType,
   789  			expectedETag:       "953ff7048ec325ce",
   790  			expectedStatusCode: 200,
   791  			urlParams: map[string]string{
   792  				"name": "aviation_polygons",
   793  			},
   794  			queryParams: map[string]string{
   795  				"page":  "1",
   796  				"limit": "3",
   797  			},
   798  		},
   799  		// Happy-path GET request w/ full timestamp filter (date/time/timezone)
   800  		// TODO: The athens test gpkg doesn't have any time data so this only checks if the collection
   801  		//	and interpretation of time values is working, no features will be filtered out.
   802  		{
   803  			requestMethod: HTTPMethodGET,
   804  			goContent: wfs3.FeatureCollection{
   805  				Links: []*wfs3.Link{
   806  					{Rel: "self", Href: fmt.Sprintf("http://%v/collections/aviation_polygons/items?limit=3&page=1", serveAddress), Type: "application/json"},
   807  					{Rel: "alternate", Href: fmt.Sprintf("http://%v/collections/aviation_polygons/items?f=text%%2Fhtml&limit=3&page=1", serveAddress), Type: "text/html"},
   808  					{Rel: "prev", Href: fmt.Sprintf("http://%v/collections/aviation_polygons/items?limit=3&page=0", serveAddress), Type: "application/json"},
   809  					{Rel: "next", Href: fmt.Sprintf("http://%v/collections/aviation_polygons/items?limit=3&page=2", serveAddress), Type: "application/json"},
   810  				},
   811  				NumberMatched:  8,
   812  				NumberReturned: 3,
   813  				// Populate the embedded geojson FeatureCollection
   814  				FeatureCollection: geojson.FeatureCollection{
   815  					Features: []geojson.Feature{
   816  						{
   817  							ID: uint64ptr(4),
   818  							Geometry: geojson.Geometry{
   819  								Geometry: geom.Polygon{
   820  									{
   821  										{23.7393297, 37.8862976},
   822  										{23.7392296, 37.8862617},
   823  										{23.7392581, 37.8862122},
   824  										{23.7385715, 37.8859662},
   825  										{23.7384902, 37.8861076},
   826  										{23.7391751, 37.8863529},
   827  										{23.7391999, 37.8863097},
   828  										{23.7393018, 37.8863462},
   829  										{23.7393297, 37.8862976},
   830  									},
   831  								},
   832  							},
   833  							Properties: map[string]interface{}{
   834  								"aeroway":    "terminal",
   835  								"building":   "yes",
   836  								"osm_way_id": "191315126",
   837  							},
   838  						},
   839  						{
   840  							ID: uint64ptr(5),
   841  							Geometry: geojson.Geometry{
   842  								Geometry: geom.Polygon{
   843  									{
   844  										{23.7400581, 37.8850307},
   845  										{23.7400919, 37.884972},
   846  										{23.7399529, 37.8849222},
   847  										{23.739979, 37.8848768},
   848  										{23.739275, 37.8846247},
   849  										{23.7391938, 37.884766},
   850  										{23.73991, 37.8850225},
   851  										{23.7399314, 37.8849853},
   852  										{23.7400581, 37.8850307},
   853  									},
   854  								},
   855  							},
   856  							Properties: map[string]interface{}{
   857  								"aeroway":    "terminal",
   858  								"building":   "yes",
   859  								"osm_way_id": "191315130",
   860  							},
   861  						},
   862  						{
   863  							ID: uint64ptr(6),
   864  							Geometry: geojson.Geometry{
   865  								Geometry: geom.Polygon{
   866  									{
   867  										{23.739719, 37.8856206},
   868  										{23.7396799, 37.8856886},
   869  										{23.739478, 37.8860396},
   870  										{23.7398555, 37.8861748},
   871  										{23.7398922, 37.886111},
   872  										{23.7402413, 37.8855038},
   873  										{23.7402659, 37.8854609},
   874  										{23.7402042, 37.8854388},
   875  										{23.7398885, 37.8853257},
   876  										{23.739719, 37.8856206},
   877  									},
   878  								},
   879  							},
   880  							Properties: map[string]interface{}{
   881  								"aeroway":    "terminal",
   882  								"building":   "yes",
   883  								"osm_way_id": "191315133",
   884  							},
   885  						},
   886  					},
   887  				},
   888  			},
   889  			contentOverride:    nil,
   890  			contentType:        config.JSONContentType,
   891  			expectedETag:       "953ff7048ec325ce",
   892  			expectedStatusCode: 200,
   893  			urlParams: map[string]string{
   894  				"name": "aviation_polygons",
   895  			},
   896  			queryParams: map[string]string{
   897  				"page":  "1",
   898  				"limit": "3",
   899  				"time":  "2018-04-12T16:29:00Z-0600",
   900  			},
   901  		},
   902  		// Happy-path GET request w/ zoneless timestamp filter (date/time)
   903  		// TODO: The athens test gpkg doesn't have any time data so this only checks if the collection
   904  		//	and interpretation of time values is working, no features will be filtered out.
   905  		{
   906  			requestMethod: HTTPMethodGET,
   907  			goContent: wfs3.FeatureCollection{
   908  				Links: []*wfs3.Link{
   909  					{Rel: "self", Href: fmt.Sprintf("http://%v/collections/aviation_polygons/items?limit=3&page=1", serveAddress), Type: "application/json"},
   910  					{Rel: "alternate", Href: fmt.Sprintf("http://%v/collections/aviation_polygons/items?f=text%%2Fhtml&limit=3&page=1", serveAddress), Type: "text/html"},
   911  					{Rel: "prev", Href: fmt.Sprintf("http://%v/collections/aviation_polygons/items?limit=3&page=0", serveAddress), Type: "application/json"},
   912  					{Rel: "next", Href: fmt.Sprintf("http://%v/collections/aviation_polygons/items?limit=3&page=2", serveAddress), Type: "application/json"},
   913  				},
   914  				NumberMatched:  8,
   915  				NumberReturned: 3,
   916  				// Populate the embedded geojson FeatureCollection
   917  				FeatureCollection: geojson.FeatureCollection{
   918  					Features: []geojson.Feature{
   919  						{
   920  							ID: uint64ptr(4),
   921  							Geometry: geojson.Geometry{
   922  								Geometry: geom.Polygon{
   923  									{
   924  										{23.7393297, 37.8862976},
   925  										{23.7392296, 37.8862617},
   926  										{23.7392581, 37.8862122},
   927  										{23.7385715, 37.8859662},
   928  										{23.7384902, 37.8861076},
   929  										{23.7391751, 37.8863529},
   930  										{23.7391999, 37.8863097},
   931  										{23.7393018, 37.8863462},
   932  										{23.7393297, 37.8862976},
   933  									},
   934  								},
   935  							},
   936  							Properties: map[string]interface{}{
   937  								"aeroway":    "terminal",
   938  								"building":   "yes",
   939  								"osm_way_id": "191315126",
   940  							},
   941  						},
   942  						{
   943  							ID: uint64ptr(5),
   944  							Geometry: geojson.Geometry{
   945  								Geometry: geom.Polygon{
   946  									{
   947  										{23.7400581, 37.8850307},
   948  										{23.7400919, 37.884972},
   949  										{23.7399529, 37.8849222},
   950  										{23.739979, 37.8848768},
   951  										{23.739275, 37.8846247},
   952  										{23.7391938, 37.884766},
   953  										{23.73991, 37.8850225},
   954  										{23.7399314, 37.8849853},
   955  										{23.7400581, 37.8850307},
   956  									},
   957  								},
   958  							},
   959  							Properties: map[string]interface{}{
   960  								"aeroway":    "terminal",
   961  								"building":   "yes",
   962  								"osm_way_id": "191315130",
   963  							},
   964  						},
   965  						{
   966  							ID: uint64ptr(6),
   967  							Geometry: geojson.Geometry{
   968  								Geometry: geom.Polygon{
   969  									{
   970  										{23.739719, 37.8856206},
   971  										{23.7396799, 37.8856886},
   972  										{23.739478, 37.8860396},
   973  										{23.7398555, 37.8861748},
   974  										{23.7398922, 37.886111},
   975  										{23.7402413, 37.8855038},
   976  										{23.7402659, 37.8854609},
   977  										{23.7402042, 37.8854388},
   978  										{23.7398885, 37.8853257},
   979  										{23.739719, 37.8856206},
   980  									},
   981  								},
   982  							},
   983  							Properties: map[string]interface{}{
   984  								"aeroway":    "terminal",
   985  								"building":   "yes",
   986  								"osm_way_id": "191315133",
   987  							},
   988  						},
   989  					},
   990  				},
   991  			},
   992  			contentOverride:    nil,
   993  			contentType:        config.JSONContentType,
   994  			expectedETag:       "953ff7048ec325ce",
   995  			expectedStatusCode: 200,
   996  			urlParams: map[string]string{
   997  				"name": "aviation_polygons",
   998  			},
   999  			queryParams: map[string]string{
  1000  				"page":  "1",
  1001  				"limit": "3",
  1002  				"time":  "2018-04-12T16:29:00",
  1003  			},
  1004  		},
  1005  		// Happy-path GET request w/ date only timestamp filter
  1006  		// TODO: The athens test gpkg doesn't have any time data so this only checks if the collection
  1007  		//	and interpretation of time values is working, no features will be filtered out.
  1008  		{
  1009  			requestMethod: HTTPMethodGET,
  1010  			goContent: wfs3.FeatureCollection{
  1011  				Links: []*wfs3.Link{
  1012  					{Rel: "self", Href: fmt.Sprintf("http://%v/collections/aviation_polygons/items?limit=3&page=1", serveAddress), Type: "application/json"},
  1013  					{Rel: "alternate", Href: fmt.Sprintf("http://%v/collections/aviation_polygons/items?f=text%%2Fhtml&limit=3&page=1", serveAddress), Type: "text/html"},
  1014  					{Rel: "prev", Href: fmt.Sprintf("http://%v/collections/aviation_polygons/items?limit=3&page=0", serveAddress), Type: "application/json"},
  1015  					{Rel: "next", Href: fmt.Sprintf("http://%v/collections/aviation_polygons/items?limit=3&page=2", serveAddress), Type: "application/json"},
  1016  				},
  1017  				NumberMatched:  8,
  1018  				NumberReturned: 3,
  1019  				// Populate the embedded geojson FeatureCollection
  1020  				FeatureCollection: geojson.FeatureCollection{
  1021  					Features: []geojson.Feature{
  1022  						{
  1023  							ID: uint64ptr(4),
  1024  							Geometry: geojson.Geometry{
  1025  								Geometry: geom.Polygon{
  1026  									{
  1027  										{23.7393297, 37.8862976},
  1028  										{23.7392296, 37.8862617},
  1029  										{23.7392581, 37.8862122},
  1030  										{23.7385715, 37.8859662},
  1031  										{23.7384902, 37.8861076},
  1032  										{23.7391751, 37.8863529},
  1033  										{23.7391999, 37.8863097},
  1034  										{23.7393018, 37.8863462},
  1035  										{23.7393297, 37.8862976},
  1036  									},
  1037  								},
  1038  							},
  1039  							Properties: map[string]interface{}{
  1040  								"aeroway":    "terminal",
  1041  								"building":   "yes",
  1042  								"osm_way_id": "191315126",
  1043  							},
  1044  						},
  1045  						{
  1046  							ID: uint64ptr(5),
  1047  							Geometry: geojson.Geometry{
  1048  								Geometry: geom.Polygon{
  1049  									{
  1050  										{23.7400581, 37.8850307},
  1051  										{23.7400919, 37.884972},
  1052  										{23.7399529, 37.8849222},
  1053  										{23.739979, 37.8848768},
  1054  										{23.739275, 37.8846247},
  1055  										{23.7391938, 37.884766},
  1056  										{23.73991, 37.8850225},
  1057  										{23.7399314, 37.8849853},
  1058  										{23.7400581, 37.8850307},
  1059  									},
  1060  								},
  1061  							},
  1062  							Properties: map[string]interface{}{
  1063  								"aeroway":    "terminal",
  1064  								"building":   "yes",
  1065  								"osm_way_id": "191315130",
  1066  							},
  1067  						},
  1068  						{
  1069  							ID: uint64ptr(6),
  1070  							Geometry: geojson.Geometry{
  1071  								Geometry: geom.Polygon{
  1072  									{
  1073  										{23.739719, 37.8856206},
  1074  										{23.7396799, 37.8856886},
  1075  										{23.739478, 37.8860396},
  1076  										{23.7398555, 37.8861748},
  1077  										{23.7398922, 37.886111},
  1078  										{23.7402413, 37.8855038},
  1079  										{23.7402659, 37.8854609},
  1080  										{23.7402042, 37.8854388},
  1081  										{23.7398885, 37.8853257},
  1082  										{23.739719, 37.8856206},
  1083  									},
  1084  								},
  1085  							},
  1086  							Properties: map[string]interface{}{
  1087  								"aeroway":    "terminal",
  1088  								"building":   "yes",
  1089  								"osm_way_id": "191315133",
  1090  							},
  1091  						},
  1092  					},
  1093  				},
  1094  			},
  1095  			contentOverride:    nil,
  1096  			contentType:        config.JSONContentType,
  1097  			expectedETag:       "953ff7048ec325ce",
  1098  			expectedStatusCode: 200,
  1099  			urlParams: map[string]string{
  1100  				"name": "aviation_polygons",
  1101  			},
  1102  			queryParams: map[string]string{
  1103  				"page":  "1",
  1104  				"limit": "3",
  1105  				"time":  "2018-04-12",
  1106  			},
  1107  		},
  1108  		// Bad GET request due to invalid timestamp filter
  1109  		// TODO: The athens test gpkg doesn't have any time data so this only checks if the collection
  1110  		//	and interpretation of time values is working, no features will be filtered out.
  1111  		{
  1112  			requestMethod: HTTPMethodGET,
  1113  			goContent: map[string]string{
  1114  				"code":        "InvalidParameterValue",
  1115  				"description": "unable to parse time string: '2018-04-12_broken'",
  1116  			},
  1117  			contentOverride:    nil,
  1118  			contentType:        config.JSONContentType,
  1119  			expectedETag:       "",
  1120  			expectedStatusCode: HTTPStatusClientError,
  1121  			urlParams: map[string]string{
  1122  				"name": "aviation_polygons",
  1123  			},
  1124  			queryParams: map[string]string{
  1125  				"page":  "1",
  1126  				"limit": "3",
  1127  				"time":  "2018-04-12_broken",
  1128  			},
  1129  		},
  1130  		// Happy-path HEAD request
  1131  		{
  1132  			requestMethod:      HTTPMethodHEAD,
  1133  			goContent:          nil,
  1134  			contentOverride:    nil,
  1135  			expectedETag:       "953ff7048ec325ce",
  1136  			expectedStatusCode: HTTPStatusOk,
  1137  			urlParams: map[string]string{
  1138  				"name": "aviation_polygons",
  1139  			},
  1140  		},
  1141  		// Happy-path GET request w/ Bounding Box
  1142  		{
  1143  			requestMethod: HTTPMethodGET,
  1144  			goContent: wfs3.FeatureCollection{
  1145  				Links: []*wfs3.Link{
  1146  					{Rel: "self", Href: fmt.Sprintf("http://%v/collections/aviation_polygons/items?limit=3&page=1", serveAddress), Type: "application/json"},
  1147  					{Rel: "alternate", Href: fmt.Sprintf("http://%v/collections/aviation_polygons/items?f=text%%2Fhtml&limit=3&page=1", serveAddress), Type: "text/html"},
  1148  					{Rel: "prev", Href: fmt.Sprintf("http://%v/collections/aviation_polygons/items?limit=3&page=0", serveAddress), Type: "application/json"},
  1149  				},
  1150  				NumberMatched:  5,
  1151  				NumberReturned: 2,
  1152  				// Populate the embedded geojson FeatureCollection
  1153  				FeatureCollection: geojson.FeatureCollection{
  1154  					Features: []geojson.Feature{
  1155  						{
  1156  							ID: uint64ptr(5),
  1157  							Geometry: geojson.Geometry{
  1158  								Geometry: geom.Polygon{
  1159  									{
  1160  										{23.7400581, 37.8850307},
  1161  										{23.7400919, 37.884972},
  1162  										{23.7399529, 37.8849222},
  1163  										{23.739979, 37.8848768},
  1164  										{23.739275, 37.8846247},
  1165  										{23.7391938, 37.884766},
  1166  										{23.73991, 37.8850225},
  1167  										{23.7399314, 37.8849853},
  1168  										{23.7400581, 37.8850307},
  1169  									},
  1170  								},
  1171  							},
  1172  							Properties: map[string]interface{}{
  1173  								"aeroway":    "terminal",
  1174  								"building":   "yes",
  1175  								"osm_way_id": "191315130",
  1176  							},
  1177  						},
  1178  						{
  1179  							ID: uint64ptr(6),
  1180  							Geometry: geojson.Geometry{
  1181  								Geometry: geom.Polygon{
  1182  									{
  1183  										{23.739719, 37.8856206},
  1184  										{23.7396799, 37.8856886},
  1185  										{23.739478, 37.8860396},
  1186  										{23.7398555, 37.8861748},
  1187  										{23.7398922, 37.886111},
  1188  										{23.7402413, 37.8855038},
  1189  										{23.7402659, 37.8854609},
  1190  										{23.7402042, 37.8854388},
  1191  										{23.7398885, 37.8853257},
  1192  										{23.739719, 37.8856206},
  1193  									},
  1194  								},
  1195  							},
  1196  							Properties: map[string]interface{}{
  1197  								"aeroway":    "terminal",
  1198  								"building":   "yes",
  1199  								"osm_way_id": "191315133",
  1200  							},
  1201  						},
  1202  					},
  1203  				},
  1204  			},
  1205  			contentOverride:    nil,
  1206  			contentType:        config.JSONContentType,
  1207  			expectedETag:       "953ff7048ec325ce",
  1208  			expectedStatusCode: 200,
  1209  			urlParams: map[string]string{
  1210  				"name": "aviation_polygons",
  1211  			},
  1212  			queryParams: map[string]string{
  1213  				"page":  "1",
  1214  				"limit": "3",
  1215  				"bbox":  "23.73901,37.88372,23.74178,37.88587",
  1216  			},
  1217  		},
  1218  		// Bad GET due to badly formatted Bounding Box (3 items instead of 4)
  1219  		{
  1220  			requestMethod:      HTTPMethodGET,
  1221  			goContent:          map[string]interface{}{"code": "InvalidParameterValue", "description": "'bbox' parameter has 3 items, expecting 4: '98.6,27.3,99.7'"},
  1222  			contentOverride:    nil,
  1223  			contentType:        config.JSONContentType,
  1224  			expectedStatusCode: HTTPStatusClientError,
  1225  			urlParams: map[string]string{
  1226  				"name": "aviation_polygons",
  1227  			},
  1228  			queryParams: map[string]string{
  1229  				"page":  "1",
  1230  				"limit": "3",
  1231  				"bbox":  "98.6,27.3,99.7",
  1232  			},
  1233  		},
  1234  		// Bad GET due to badly formatted Bounding Box (One item is invalid float representation)
  1235  		{
  1236  			requestMethod:      HTTPMethodGET,
  1237  			goContent:          map[string]interface{}{"code": "InvalidParameterValue", "description": "'bbox' parameter has invalid format for item 2/4: 'Joe' / '98.6,Joe,27.3,99.7'"},
  1238  			contentOverride:    nil,
  1239  			contentType:        config.JSONContentType,
  1240  			expectedStatusCode: HTTPStatusClientError,
  1241  			urlParams: map[string]string{
  1242  				"name": "aviation_polygons",
  1243  			},
  1244  			queryParams: map[string]string{
  1245  				"page":  "1",
  1246  				"limit": "3",
  1247  				"bbox":  "98.6,Joe,27.3,99.7",
  1248  			},
  1249  		},
  1250  		// Happy-path GET request w/ Property filter
  1251  		{
  1252  			requestMethod: HTTPMethodGET,
  1253  			goContent: wfs3.FeatureCollection{
  1254  				Links: []*wfs3.Link{
  1255  					{Rel: "self", Href: fmt.Sprintf("http://%v/collections/aviation_polygons/items?limit=3&page=0", serveAddress), Type: "application/json"},
  1256  					{Rel: "alternate", Href: fmt.Sprintf("http://%v/collections/aviation_polygons/items?f=text%%2Fhtml&limit=3&page=0", serveAddress), Type: "text/html"},
  1257  				},
  1258  				NumberMatched:  1,
  1259  				NumberReturned: 1,
  1260  				// Populate the embedded geojson FeatureCollection
  1261  				FeatureCollection: geojson.FeatureCollection{
  1262  					Features: []geojson.Feature{
  1263  						{
  1264  							ID: uint64ptr(8),
  1265  							Geometry: geojson.Geometry{
  1266  								Geometry: geom.Polygon{
  1267  									{
  1268  										{23.6698795, 37.9390531},
  1269  										{23.6698992, 37.9390386},
  1270  										{23.6699119, 37.9390199},
  1271  										{23.6699162, 37.9389989},
  1272  										{23.6699117, 37.938978},
  1273  										{23.6698987, 37.9389593},
  1274  										{23.6698788, 37.938945},
  1275  										{23.6698541, 37.9389366},
  1276  										{23.6698272, 37.9389349},
  1277  										{23.6698011, 37.9389403},
  1278  										{23.6697787, 37.938952},
  1279  										{23.6697622, 37.9389688},
  1280  										{23.6697536, 37.9389889},
  1281  										{23.6697537, 37.9390102},
  1282  										{23.6697626, 37.9390302},
  1283  										{23.6697793, 37.9390469},
  1284  										{23.6698019, 37.9390585},
  1285  										{23.669828, 37.9390636},
  1286  										{23.6698549, 37.9390617},
  1287  										{23.6698795, 37.9390531},
  1288  									},
  1289  								},
  1290  							},
  1291  							Properties: map[string]interface{}{
  1292  								"aeroway":    "helipad",
  1293  								"osm_way_id": "265713911",
  1294  								"source":     "bing",
  1295  							},
  1296  						},
  1297  					},
  1298  				},
  1299  			},
  1300  			contentOverride:    nil,
  1301  			contentType:        config.JSONContentType,
  1302  			expectedETag:       "953ff7048ec325ce",
  1303  			expectedStatusCode: 200,
  1304  			urlParams: map[string]string{
  1305  				"name": "aviation_polygons",
  1306  			},
  1307  			queryParams: map[string]string{
  1308  				"page":    "0",
  1309  				"limit":   "3",
  1310  				"aeroway": "helipad",
  1311  			},
  1312  		},
  1313  	}
  1314  
  1315  	for i, tc := range testCases {
  1316  		url := fmt.Sprintf("http://%v/collections/%v/items", serveAddress, tc.urlParams["name"])
  1317  
  1318  		var expectedContent []byte
  1319  		var err error
  1320  		switch tc.contentType {
  1321  		case config.JSONContentType:
  1322  			expectedContent, err = json.Marshal(tc.goContent)
  1323  			if err != nil {
  1324  				t.Errorf("[%v] problem marshalling expected content: %v", i, err)
  1325  				return
  1326  			}
  1327  		case "":
  1328  			expectedContent = []byte{}
  1329  		default:
  1330  			t.Errorf("[%v] unsupported content type for expected content: %v", i, tc.contentType)
  1331  			return
  1332  		}
  1333  
  1334  		responseWriter := httptest.NewRecorder()
  1335  		request := httptest.NewRequest(tc.requestMethod, url, bytes.NewBufferString(""))
  1336  		err = addQueryParams(request, tc.queryParams)
  1337  		if err != nil {
  1338  			t.Errorf("[%v] problem with request url query parameters: %v", i, err)
  1339  		}
  1340  		rctx := request.Context()
  1341  		rctx = context.WithValue(rctx, "contentOverride", tc.contentOverride)
  1342  		hrParams := make(httprouter.Params, 0, len(tc.urlParams))
  1343  		for k, v := range tc.urlParams {
  1344  			hrp := httprouter.Param{Key: k, Value: v}
  1345  			hrParams = append(hrParams, hrp)
  1346  		}
  1347  		rctx = context.WithValue(rctx, httprouter.ParamsKey, hrParams)
  1348  		request = request.WithContext(rctx)
  1349  
  1350  		collectionData(responseWriter, request)
  1351  		resp := responseWriter.Result()
  1352  		body, err := ioutil.ReadAll(resp.Body)
  1353  		if err != nil {
  1354  			t.Errorf("[%v] problem reading response body: %v", i, err)
  1355  		}
  1356  
  1357  		if resp.StatusCode != tc.expectedStatusCode {
  1358  			t.Errorf("[%v] Status Code %v != %v", i, resp.StatusCode, tc.expectedStatusCode)
  1359  		}
  1360  
  1361  		if tc.expectedETag != "" && (resp.Header.Get("ETag") != tc.expectedETag) {
  1362  			t.Errorf("[%v] ETag %v != %v", i, resp.Header.Get("ETag"), tc.expectedETag)
  1363  		}
  1364  
  1365  		if string(body) != string(expectedContent) {
  1366  			t.Errorf("[%v] result doesn't match expected", i)
  1367  			reducedOutputError(t, body, expectedContent)
  1368  		}
  1369  	}
  1370  }
  1371  
  1372  func TestSingleFeature(t *testing.T) {
  1373  	serveAddress := "tdd.net"
  1374  
  1375  	type TestCase struct {
  1376  		requestMethod      string
  1377  		goContent          interface{}
  1378  		contentOverride    interface{}
  1379  		contentType        string
  1380  		expectedETag       string
  1381  		expectedStatusCode int
  1382  		urlParams          map[string]string
  1383  	}
  1384  
  1385  	var i18 uint64 = 18
  1386  	testCases := []TestCase{
  1387  		// Happy-path GET request
  1388  		{
  1389  			requestMethod: HTTPMethodGET,
  1390  			goContent: wfs3.Feature{
  1391  				Links: []*wfs3.Link{
  1392  					{Rel: "self", Type: config.JSONContentType,
  1393  						Href: fmt.Sprintf("http://%v/collections/roads_lines/items/18", serveAddress),
  1394  					},
  1395  					{Rel: "alternate", Type: config.HTMLContentType,
  1396  						Href: fmt.Sprintf("http://%v/collections/roads_lines/items/18?f=text%%2Fhtml", serveAddress),
  1397  					},
  1398  					{Rel: "collection", Type: config.JSONContentType,
  1399  						Href: fmt.Sprintf("http://%v/collections/roads_lines", serveAddress),
  1400  					},
  1401  				},
  1402  				// Populate embedded geojson Feature
  1403  				Feature: geojson.Feature{
  1404  					ID: &i18,
  1405  					Geometry: geojson.Geometry{
  1406  						Geometry: geom.LineString{
  1407  							{23.708656, 37.9137612},
  1408  							{23.7086007, 37.9140051},
  1409  							{23.708592, 37.9140435},
  1410  							{23.7085454, 37.914249},
  1411  						},
  1412  					},
  1413  					Properties: map[string]interface{}{
  1414  						"highway": "secondary_link",
  1415  						"osm_id":  "4380983",
  1416  						"z_index": "6",
  1417  					},
  1418  				},
  1419  			},
  1420  			contentOverride:    nil,
  1421  			contentType:        config.JSONContentType,
  1422  			expectedETag:       "355e6572aaf34629",
  1423  			expectedStatusCode: 200,
  1424  			urlParams: map[string]string{
  1425  				"name":       "roads_lines",
  1426  				"feature_id": "18",
  1427  			},
  1428  		},
  1429  		// Happy-path HEAD request
  1430  		{
  1431  			requestMethod:      HTTPMethodHEAD,
  1432  			goContent:          nil,
  1433  			contentOverride:    nil,
  1434  			expectedETag:       "355e6572aaf34629",
  1435  			expectedStatusCode: 200,
  1436  			urlParams: map[string]string{
  1437  				"name":       "roads_lines",
  1438  				"feature_id": "18",
  1439  			},
  1440  		},
  1441  	}
  1442  
  1443  	for i, tc := range testCases {
  1444  		url := fmt.Sprintf("http://%v/collections/%v/items/%v",
  1445  			serveAddress, tc.urlParams["name"], tc.urlParams["feature_id"])
  1446  
  1447  		var expectedContent []byte
  1448  		var err error
  1449  		switch tc.contentType {
  1450  		case config.JSONContentType:
  1451  			expectedContent, err = json.Marshal(tc.goContent)
  1452  			if err != nil {
  1453  				t.Errorf("[%v] problem marshalling expected content: %v", i, err)
  1454  				return
  1455  			}
  1456  		case "":
  1457  			expectedContent = []byte{}
  1458  		default:
  1459  			t.Errorf("[%v] unsupported content type for expected content: %v", i, tc.contentType)
  1460  			return
  1461  		}
  1462  
  1463  		responseWriter := httptest.NewRecorder()
  1464  		request := httptest.NewRequest(tc.requestMethod, url, bytes.NewBufferString(""))
  1465  		rctx := request.Context()
  1466  		rctx = context.WithValue(rctx, "contentOverride", tc.contentOverride)
  1467  		hrParams := make(httprouter.Params, 0, len(tc.urlParams))
  1468  		for k, v := range tc.urlParams {
  1469  			hrp := httprouter.Param{Key: k, Value: v}
  1470  			hrParams = append(hrParams, hrp)
  1471  		}
  1472  		rctx = context.WithValue(rctx, httprouter.ParamsKey, hrParams)
  1473  		request = request.WithContext(rctx)
  1474  
  1475  		collectionData(responseWriter, request)
  1476  		resp := responseWriter.Result()
  1477  		body, err := ioutil.ReadAll(resp.Body)
  1478  		if err != nil {
  1479  			t.Errorf("[%v] problem reading response body: %v", i, err)
  1480  		}
  1481  
  1482  		if tc.expectedETag != "" && (resp.Header.Get("ETag") != tc.expectedETag) {
  1483  			t.Errorf("[%v] ETag %v != %v", i, resp.Header.Get("ETag"), tc.expectedETag)
  1484  		}
  1485  
  1486  		if resp.StatusCode != tc.expectedStatusCode {
  1487  			t.Errorf("[%v] Status Code %v != %v", i, resp.StatusCode, tc.expectedStatusCode)
  1488  		}
  1489  
  1490  		if string(body) != string(expectedContent) {
  1491  			t.Errorf("[%v] result doesn't match expected", i)
  1492  			reducedOutputError(t, body, expectedContent)
  1493  		}
  1494  	}
  1495  }
  1496  
  1497  // For large human-readable returns like JSON, limit the output displayed on error to the
  1498  //	mismatched line and a few surrounding lines
  1499  func reducedOutputError(t *testing.T, body, expectedContent []byte) {
  1500  	// Number of lines to output before and after mismatched line
  1501  	surroundSize := 5
  1502  	// Human readable versions of each
  1503  	bBuf := bytes.NewBufferString("")
  1504  	eBuf := bytes.NewBufferString("")
  1505  	json.Indent(bBuf, body, "", "  ")
  1506  	json.Indent(eBuf, expectedContent, "", "  ")
  1507  
  1508  	hrBody, err := ioutil.ReadAll(bBuf)
  1509  	if err != nil {
  1510  		t.Errorf("Problem reading human-friendly body: %v", err)
  1511  	}
  1512  	hrExpected, err := ioutil.ReadAll(eBuf)
  1513  	if err != nil {
  1514  		t.Errorf("Problem reading human-friendly expected: %v", err)
  1515  	}
  1516  
  1517  	hrBodyLines := strings.Split(string(hrBody), "\n")
  1518  	hrExpectedLines := strings.Split(string(hrExpected), "\n")
  1519  	maxInt := func(a, b int) int {
  1520  		if a > b {
  1521  			return a
  1522  		}
  1523  		return b
  1524  	}
  1525  	minInt := func(a, b int) int {
  1526  		if a < b {
  1527  			return a
  1528  		}
  1529  		return b
  1530  	}
  1531  	for i, bLine := range hrBodyLines {
  1532  		if bLine != hrExpectedLines[i] {
  1533  			firstLineIdx := maxInt(i-surroundSize, 0)
  1534  			lastLineIdxB := minInt(i+surroundSize, len(hrBodyLines))
  1535  			lastLineIdxE := minInt(i+surroundSize, len(hrExpectedLines))
  1536  
  1537  			mismatchB := strings.Join(hrBodyLines[firstLineIdx:lastLineIdxB], "\n")
  1538  			mismatchE := strings.Join(hrExpectedLines[firstLineIdx:lastLineIdxE], "\n")
  1539  			t.Errorf("Result doesn't match expected at line %v, showing %v-%v:\n%v\n--- != ---\n%v\n",
  1540  				i, firstLineIdx, lastLineIdxB, mismatchB, mismatchE)
  1541  			break
  1542  		}
  1543  	}
  1544  }
  1545  
  1546  func addQueryParams(req *http.Request, queryParams map[string]string) error {
  1547  	// Add query parameters to url
  1548  	if queryParams != nil && len(queryParams) > 0 {
  1549  		q, err := url.ParseQuery(req.URL.RawQuery)
  1550  		if err != nil {
  1551  			return err
  1552  		}
  1553  		for k, v := range queryParams {
  1554  			q[k] = []string{v}
  1555  		}
  1556  		req.URL.RawQuery = q.Encode()
  1557  	}
  1558  	return nil
  1559  }