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 }