github.com/GoogleCloudPlatform/testgrid@v0.0.174/pkg/api/v1/config_http_test.go (about) 1 /* 2 Copyright 2021 The TestGrid Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package v1 18 19 import ( 20 "net/http" 21 "net/http/httptest" 22 "testing" 23 24 apipb "github.com/GoogleCloudPlatform/testgrid/pb/api/v1" 25 configpb "github.com/GoogleCloudPlatform/testgrid/pb/config" 26 statepb "github.com/GoogleCloudPlatform/testgrid/pb/state" 27 summarypb "github.com/GoogleCloudPlatform/testgrid/pb/summary" 28 "github.com/google/go-cmp/cmp" 29 "google.golang.org/protobuf/encoding/protojson" 30 "google.golang.org/protobuf/proto" 31 "google.golang.org/protobuf/testing/protocmp" 32 ) 33 34 func TestQueryParams(t *testing.T) { 35 tests := []struct { 36 name string 37 url string 38 expected string 39 }{ 40 { 41 name: "No Query Parameters", 42 }, 43 { 44 name: "Passes scope parameter only", 45 url: "/foo?scope=gs://example/bucket&bucket=fake&format=json", 46 expected: "?scope=gs://example/bucket", 47 }, 48 { 49 name: "Use only the first scope parameter", 50 url: "/foo?scope=gs://example/bucket&scope=gs://fake/bucket", 51 expected: "?scope=gs://example/bucket", 52 }, 53 } 54 for _, test := range tests { 55 t.Run(test.name, func(t *testing.T) { 56 req, err := http.NewRequest("GET", test.url, nil) 57 if err != nil { 58 t.Fatalf("can't create request for %s", test.url) 59 } 60 result := queryParams(req.URL.Query().Get(scopeParam)) 61 if result != test.expected { 62 t.Errorf("Want %q, but got %q", test.expected, result) 63 } 64 }) 65 } 66 } 67 68 type TestSpec struct { 69 name string 70 config map[string]*configpb.Configuration 71 summaries map[string]*summarypb.DashboardSummary 72 grid map[string]*statepb.Grid 73 endpoint string 74 params string 75 expectedResponse proto.Message 76 expectedCode int 77 } 78 79 func TestListDashboardGroupHTTP(t *testing.T) { 80 tests := []TestSpec{ 81 { 82 name: "Returns an empty JSON when there's no groups", 83 config: map[string]*configpb.Configuration{ 84 "gs://default/config": {}, 85 }, 86 expectedResponse: &apipb.ListDashboardGroupsResponse{}, 87 expectedCode: http.StatusOK, 88 }, 89 { 90 name: "Returns a Dashboard Group", 91 config: map[string]*configpb.Configuration{ 92 "gs://default/config": { 93 DashboardGroups: []*configpb.DashboardGroup{ 94 { 95 Name: "Group1", 96 }, 97 }, 98 }, 99 }, 100 expectedResponse: &apipb.ListDashboardGroupsResponse{ 101 DashboardGroups: []*apipb.Resource{ 102 { 103 Name: "Group1", 104 Link: "/dashboard-groups/group1", 105 }, 106 }, 107 }, 108 expectedCode: http.StatusOK, 109 }, 110 { 111 name: "Returns multiple Dashboard Groups", 112 config: map[string]*configpb.Configuration{ 113 "gs://default/config": { 114 DashboardGroups: []*configpb.DashboardGroup{ 115 { 116 Name: "Group1", 117 }, 118 { 119 Name: "Second Group", 120 }, 121 }, 122 }, 123 }, 124 expectedResponse: &apipb.ListDashboardGroupsResponse{ 125 DashboardGroups: []*apipb.Resource{ 126 { 127 Name: "Group1", 128 Link: "/dashboard-groups/group1", 129 }, 130 { 131 Name: "Second Group", 132 Link: "/dashboard-groups/secondgroup", 133 }, 134 }, 135 }, 136 expectedCode: http.StatusOK, 137 }, 138 { 139 name: "Reads specified configs", 140 config: map[string]*configpb.Configuration{ 141 "gs://example/config": { 142 DashboardGroups: []*configpb.DashboardGroup{ 143 { 144 Name: "Group1", 145 }, 146 }, 147 }, 148 }, 149 params: "?scope=gs://example", 150 expectedResponse: &apipb.ListDashboardGroupsResponse{ 151 DashboardGroups: []*apipb.Resource{ 152 { 153 Name: "Group1", 154 Link: "/dashboard-groups/group1?scope=gs://example", 155 }, 156 }, 157 }, 158 expectedCode: http.StatusOK, 159 }, 160 { 161 name: "Server error with unreadable config", 162 params: "?scope=gs://bad-path", 163 expectedCode: http.StatusNotFound, 164 }, 165 } 166 var resp apipb.ListDashboardGroupsResponse 167 RunTestsAgainstEndpoint(t, "/dashboard-groups", tests, &resp) 168 } 169 170 func TestGetDashboardGroupHTTP(t *testing.T) { 171 tests := []TestSpec{ 172 { 173 name: "Returns an error when there's no resource", 174 config: map[string]*configpb.Configuration{ 175 "gs://default/config": {}, 176 }, 177 endpoint: "missing", 178 expectedCode: http.StatusNotFound, 179 }, 180 { 181 name: "Returns empty JSON from an empty Dashboard Group", 182 config: map[string]*configpb.Configuration{ 183 "gs://default/config": { 184 DashboardGroups: []*configpb.DashboardGroup{ 185 { 186 Name: "Group1", 187 }, 188 }, 189 }, 190 }, 191 endpoint: "/group1", 192 expectedResponse: &apipb.GetDashboardGroupResponse{}, 193 expectedCode: http.StatusOK, 194 }, 195 { 196 name: "Returns dashboards from group", 197 config: map[string]*configpb.Configuration{ 198 "gs://default/config": { 199 DashboardGroups: []*configpb.DashboardGroup{ 200 { 201 Name: "stooges", 202 DashboardNames: []string{"larry", "curly", "moe"}, 203 }, 204 }, 205 }, 206 }, 207 endpoint: "/stooges", 208 expectedResponse: &apipb.GetDashboardGroupResponse{ 209 Dashboards: []*apipb.Resource{ 210 { 211 Name: "curly", 212 Link: "/dashboards/curly", 213 }, 214 { 215 Name: "larry", 216 Link: "/dashboards/larry", 217 }, 218 { 219 Name: "moe", 220 Link: "/dashboards/moe", 221 }, 222 }, 223 }, 224 expectedCode: http.StatusOK, 225 }, 226 { 227 name: "Reads 'scope' parameter", 228 config: map[string]*configpb.Configuration{ 229 "gs://default/config": { 230 DashboardGroups: []*configpb.DashboardGroup{ 231 { 232 Name: "wrong-group", 233 DashboardNames: []string{"no"}, 234 }, 235 }, 236 }, 237 "gs://example/config": { 238 DashboardGroups: []*configpb.DashboardGroup{ 239 { 240 Name: "right-group", 241 DashboardNames: []string{"yes"}, 242 }, 243 }, 244 }, 245 }, 246 endpoint: "/rightgroup?scope=gs://example", 247 expectedResponse: &apipb.GetDashboardGroupResponse{ 248 Dashboards: []*apipb.Resource{ 249 { 250 Name: "yes", 251 Link: "/dashboards/yes?scope=gs://example", 252 }, 253 }, 254 }, 255 expectedCode: http.StatusOK, 256 }, 257 } 258 var resp apipb.GetDashboardGroupResponse 259 RunTestsAgainstEndpoint(t, "/dashboard-groups", tests, &resp) 260 } 261 262 func TestListDashboardsHTTP(t *testing.T) { 263 tests := []TestSpec{ 264 { 265 name: "Returns an empty JSON when there is no dashboards", 266 config: map[string]*configpb.Configuration{ 267 "gs://default/config": {}, 268 }, 269 expectedResponse: &apipb.ListDashboardsResponse{}, 270 expectedCode: http.StatusOK, 271 }, 272 { 273 name: "Returns a Dashboard which doesn't belong to a grop", 274 config: map[string]*configpb.Configuration{ 275 "gs://default/config": { 276 Dashboards: []*configpb.Dashboard{ 277 { 278 Name: "Dashboard1", 279 }, 280 }, 281 }, 282 }, 283 expectedResponse: &apipb.ListDashboardsResponse{ 284 Dashboards: []*apipb.DashboardResource{ 285 { 286 Name: "Dashboard1", 287 Link: "/dashboards/dashboard1", 288 }, 289 }, 290 }, 291 expectedCode: http.StatusOK, 292 }, 293 { 294 name: "Returns multiple Dashboards which belong to different groups", 295 config: map[string]*configpb.Configuration{ 296 "gs://default/config": { 297 Dashboards: []*configpb.Dashboard{ 298 { 299 Name: "Dashboard1", 300 }, 301 { 302 Name: "Dashboard2", 303 }, 304 }, 305 DashboardGroups: []*configpb.DashboardGroup{ 306 { 307 Name: "DashboardGroup1", 308 DashboardNames: []string{"Dashboard1"}, 309 }, 310 { 311 Name: "DashboardGroup2", 312 DashboardNames: []string{"Dashboard2"}, 313 }, 314 }, 315 }, 316 }, 317 expectedResponse: &apipb.ListDashboardsResponse{ 318 Dashboards: []*apipb.DashboardResource{ 319 { 320 Name: "Dashboard1", 321 Link: "/dashboards/dashboard1", 322 DashboardGroupName: "DashboardGroup1", 323 }, 324 { 325 Name: "Dashboard2", 326 Link: "/dashboards/dashboard2", 327 DashboardGroupName: "DashboardGroup2", 328 }, 329 }, 330 }, 331 expectedCode: http.StatusOK, 332 }, 333 { 334 name: "Reads from other config/scope", 335 config: map[string]*configpb.Configuration{ 336 "gs://example/config": { 337 Dashboards: []*configpb.Dashboard{ 338 { 339 Name: "Dashboard1", 340 }, 341 }, 342 }, 343 }, 344 params: "?scope=gs://example", 345 expectedResponse: &apipb.ListDashboardsResponse{ 346 Dashboards: []*apipb.DashboardResource{ 347 { 348 Name: "Dashboard1", 349 Link: "/dashboards/dashboard1?scope=gs://example", 350 }, 351 }, 352 }, 353 expectedCode: http.StatusOK, 354 }, 355 { 356 name: "Server error with unreadable config", 357 params: "?scope=gs://bad-path", 358 expectedCode: http.StatusNotFound, 359 }, 360 } 361 var resp apipb.ListDashboardsResponse 362 RunTestsAgainstEndpoint(t, "/dashboards", tests, &resp) 363 } 364 365 func TestGetDashboardHTTP(t *testing.T) { 366 tests := []TestSpec{ 367 { 368 name: "Returns an error when there's no resource", 369 config: map[string]*configpb.Configuration{ 370 "gs://default/config": {}, 371 }, 372 endpoint: "/missing", 373 expectedCode: http.StatusNotFound, 374 }, 375 { 376 name: "Returns empty JSON from an empty Dashboard", 377 config: map[string]*configpb.Configuration{ 378 "gs://default/config": { 379 Dashboards: []*configpb.Dashboard{ 380 { 381 Name: "Dashboard1", 382 }, 383 }, 384 }, 385 }, 386 endpoint: "/dashboard1", 387 expectedResponse: &apipb.GetDashboardResponse{}, 388 expectedCode: http.StatusOK, 389 }, 390 { 391 name: "Returns dashboard info from dashboard", 392 config: map[string]*configpb.Configuration{ 393 "gs://default/config": { 394 Dashboards: []*configpb.Dashboard{ 395 { 396 Name: "Dashboard1", 397 DefaultTab: "defaultTab", 398 HighlightToday: true, 399 DownplayFailingTabs: true, 400 Notifications: []*configpb.Notification{ 401 { 402 Summary: "Notification summary", 403 ContextLink: "Notification context link", 404 }, 405 }, 406 }, 407 }, 408 }, 409 }, 410 endpoint: "/dashboard1", 411 expectedResponse: &apipb.GetDashboardResponse{ 412 Notifications: []*configpb.Notification{ 413 { 414 Summary: "Notification summary", 415 ContextLink: "Notification context link", 416 }, 417 }, 418 DefaultTab: "defaultTab", 419 SuppressFailingTabs: true, 420 HighlightToday: true, 421 }, 422 expectedCode: http.StatusOK, 423 }, 424 { 425 name: "Reads 'scope' parameter", 426 config: map[string]*configpb.Configuration{ 427 "gs://default/config": { 428 Dashboards: []*configpb.Dashboard{ 429 { 430 Name: "wrong-dashboard", 431 DefaultTab: "wrong-dashboard defaultTab", 432 HighlightToday: true, 433 DownplayFailingTabs: true, 434 Notifications: []*configpb.Notification{ 435 { 436 Summary: "Notification summary", 437 ContextLink: "Notification context link", 438 }, 439 }, 440 }, 441 }, 442 }, 443 "gs://example/config": { 444 Dashboards: []*configpb.Dashboard{ 445 { 446 Name: "correct-dashboard", 447 DefaultTab: "correct-dashboard defaultTab", 448 HighlightToday: true, 449 DownplayFailingTabs: true, 450 Notifications: []*configpb.Notification{}, 451 }, 452 }, 453 }, 454 }, 455 endpoint: "/correctdashboard", 456 params: "?scope=gs://example", 457 expectedResponse: &apipb.GetDashboardResponse{ 458 DefaultTab: "correct-dashboard defaultTab", 459 SuppressFailingTabs: true, 460 HighlightToday: true, 461 }, 462 expectedCode: http.StatusOK, 463 }, 464 } 465 var resp apipb.GetDashboardResponse 466 RunTestsAgainstEndpoint(t, "/dashboards", tests, &resp) 467 } 468 469 func TestListDashboardTabsHTTP(t *testing.T) { 470 tests := []TestSpec{ 471 { 472 name: "Returns an error", 473 config: map[string]*configpb.Configuration{ 474 "gs://default/config": {}, 475 }, 476 endpoint: "/missingdashboard/tabs", 477 expectedCode: http.StatusNotFound, 478 }, 479 { 480 name: "Returns empty JSON from an empty Dashboard", 481 config: map[string]*configpb.Configuration{ 482 "gs://default/config": { 483 Dashboards: []*configpb.Dashboard{ 484 { 485 Name: "Dashboard1", 486 DashboardTab: []*configpb.DashboardTab{}, 487 }, 488 }, 489 }, 490 }, 491 endpoint: "/dashboard1/tabs", 492 expectedResponse: &apipb.ListDashboardTabsResponse{}, 493 expectedCode: http.StatusOK, 494 }, 495 { 496 name: "Returns tabs list from a Dashboard", 497 config: map[string]*configpb.Configuration{ 498 "gs://default/config": { 499 Dashboards: []*configpb.Dashboard{ 500 { 501 Name: "Dashboard1", 502 DashboardTab: []*configpb.DashboardTab{ 503 { 504 Name: "tab 1", 505 }, 506 { 507 Name: "tab 2", 508 }, 509 }, 510 }, 511 }, 512 }, 513 }, 514 endpoint: "/dashboard1/tabs", 515 expectedResponse: &apipb.ListDashboardTabsResponse{ 516 DashboardTabs: []*apipb.Resource{ 517 { 518 Name: "tab 1", 519 Link: "/dashboards/dashboard1/tabs/tab1", 520 }, 521 { 522 Name: "tab 2", 523 Link: "/dashboards/dashboard1/tabs/tab2", 524 }, 525 }, 526 }, 527 expectedCode: http.StatusOK, 528 }, 529 { 530 name: "Reads 'scope' parameter", 531 config: map[string]*configpb.Configuration{ 532 "gs://default/config": { 533 Dashboards: []*configpb.Dashboard{ 534 { 535 Name: "wrong-dashboard", 536 DashboardTab: []*configpb.DashboardTab{ 537 { 538 Name: "wrong-dashboard tab 1", 539 }, 540 }, 541 }, 542 }, 543 }, 544 "gs://example/config": { 545 Dashboards: []*configpb.Dashboard{ 546 { 547 Name: "correct-dashboard", 548 DashboardTab: []*configpb.DashboardTab{ 549 { 550 Name: "correct-dashboard tab 1", 551 }, 552 }, 553 }, 554 }, 555 }, 556 }, 557 endpoint: "/correctdashboard/tabs", 558 params: "?scope=gs://example", 559 expectedResponse: &apipb.ListDashboardTabsResponse{ 560 DashboardTabs: []*apipb.Resource{ 561 { 562 Name: "correct-dashboard tab 1", 563 Link: "/dashboards/correctdashboard/tabs/correctdashboardtab1?scope=gs://example", 564 }, 565 }, 566 }, 567 expectedCode: http.StatusOK, 568 }, 569 } 570 var resp apipb.ListDashboardTabsResponse 571 RunTestsAgainstEndpoint(t, "/dashboards", tests, &resp) 572 } 573 574 func RunTestsAgainstEndpoint(t *testing.T, baseEndpoint string, tests []TestSpec, resp proto.Message) { 575 for _, test := range tests { 576 t.Run(test.name, func(t *testing.T) { 577 router := Route(nil, setupTestServer(t, test.config, nil, nil)) 578 absEndpoint := baseEndpoint + test.endpoint + test.params 579 request, err := http.NewRequest("GET", absEndpoint, nil) 580 if err != nil { 581 t.Fatalf("Can't form request: %v", err) 582 } 583 response := httptest.NewRecorder() 584 router.ServeHTTP(response, request) 585 586 if response.Code != test.expectedCode { 587 t.Errorf("Expected %d, but got %d", test.expectedCode, response.Code) 588 } 589 590 if response.Code == http.StatusOK { 591 if err := protojson.Unmarshal(response.Body.Bytes(), resp); err != nil { 592 t.Fatalf("Failed to unmarshal json message into a proto message: %v", err) 593 } 594 if diff := cmp.Diff(test.expectedResponse, resp, protocmp.Transform()); diff != "" { 595 t.Errorf("Obtained unexpected diff (-want +got):\n%s", diff) 596 } 597 } 598 }) 599 } 600 }