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  }