go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/frontend/routes_test.go (about) 1 // Copyright 2016 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package frontend 16 17 import ( 18 "context" 19 "flag" 20 "fmt" 21 "html/template" 22 "net/http" 23 "net/http/httptest" 24 "net/url" 25 "os" 26 "path/filepath" 27 "regexp" 28 "strings" 29 "testing" 30 "time" 31 32 "github.com/golang/protobuf/jsonpb" 33 "github.com/julienschmidt/httprouter" 34 "google.golang.org/protobuf/types/known/timestamppb" 35 36 "go.chromium.org/luci/auth/identity" 37 buildbucketpb "go.chromium.org/luci/buildbucket/proto" 38 "go.chromium.org/luci/common/clock/testclock" 39 "go.chromium.org/luci/gae/impl/memory" 40 "go.chromium.org/luci/server/auth" 41 "go.chromium.org/luci/server/auth/authtest" 42 "go.chromium.org/luci/server/router" 43 "go.chromium.org/luci/server/settings" 44 "go.chromium.org/luci/server/templates" 45 46 "go.chromium.org/luci/milo/frontend/ui" 47 "go.chromium.org/luci/milo/internal/model" 48 "go.chromium.org/luci/milo/internal/model/milostatus" 49 "go.chromium.org/luci/milo/internal/projectconfig" 50 51 . "github.com/smartystreets/goconvey/convey" 52 ) 53 54 var now = time.Date(2019, time.February, 3, 4, 5, 6, 7, time.UTC) 55 var nowTS = timestamppb.New(now) 56 57 // TODO(nodir): refactor this file. 58 59 // TestBundle is a template arg associated with a description used for testing. 60 type TestBundle struct { 61 // Description is a short one line description of what the data contains. 62 Description string 63 // Data is the data fed directly into the template. 64 Data templates.Args 65 } 66 67 type testPackage struct { 68 Data func() []TestBundle 69 DisplayName string 70 TemplateName string 71 } 72 73 var ( 74 allPackages = []testPackage{ 75 {buildbucketBuildTestData, "buildbucket.build", "pages/build.html"}, 76 {consoleTestData, "console", "pages/console.html"}, 77 {Frontpage, "frontpage", "pages/frontpage.html"}, 78 {relatedBuildsTableTestData, "widget", "widgets/related_builds_table.html"}, 79 } 80 ) 81 82 var generate = flag.Bool( 83 "test.generate", false, "Generate expectations instead of running tests.") 84 85 func expectFileName(name string) string { 86 name = strings.Replace(name, " ", "_", -1) 87 name = strings.Replace(name, "/", "_", -1) 88 name = strings.Replace(name, ":", "-", -1) 89 return filepath.Join("expectations", name) 90 } 91 92 func load(name string) ([]byte, error) { 93 filename := expectFileName(name) 94 return os.ReadFile(filename) 95 } 96 97 // mustWrite Writes a buffer into an expectation file. Should always work or 98 // panic. This is fine because this only runs when -generate is passed in, 99 // not during tests. 100 func mustWrite(name string, buf []byte) { 101 filename := expectFileName(name) 102 err := os.WriteFile(filename, buf, 0644) 103 if err != nil { 104 panic(err) 105 } 106 } 107 108 func TestPages(t *testing.T) { 109 fixZeroDurationRE := regexp.MustCompile(`(Running for:|waiting) 0s?`) 110 fixZeroDuration := func(text string) string { 111 return fixZeroDurationRE.ReplaceAllLiteralString(text, "[ZERO DURATION]") 112 } 113 114 SkipConvey("Testing basic rendering.", t, func() { 115 r := &http.Request{URL: &url.URL{Path: "/foobar"}} 116 c := context.Background() 117 c = memory.Use(c) 118 c, _ = testclock.UseTime(c, now) 119 c = auth.WithState(c, &authtest.FakeState{Identity: identity.AnonymousIdentity}) 120 c = settings.Use(c, settings.New(&settings.MemoryStorage{Expiration: time.Second})) 121 c = templates.Use(c, getTemplateBundle("appengine/templates", "testVersionID", false), &templates.Extra{Request: r}) 122 for _, p := range allPackages { 123 Convey(fmt.Sprintf("Testing handler %q", p.DisplayName), func() { 124 for _, b := range p.Data() { 125 Convey(fmt.Sprintf("Testing: %q", b.Description), func() { 126 args := b.Data 127 // This is not a path, but a file key, should always be "/". 128 tmplName := p.TemplateName 129 buf, err := templates.Render(c, tmplName, args) 130 So(err, ShouldBeNil) 131 fname := fmt.Sprintf( 132 "%s-%s.html", p.DisplayName, b.Description) 133 if *generate { 134 mustWrite(fname, buf) 135 } else { 136 localBuf, err := load(fname) 137 So(err, ShouldBeNil) 138 So(fixZeroDuration(string(buf)), ShouldEqual, fixZeroDuration(string(localBuf))) 139 } 140 }) 141 } 142 }) 143 } 144 }) 145 } 146 147 // buildbucketBuildTestData returns sample test data for build pages. 148 func buildbucketBuildTestData() []TestBundle { 149 bundles := []TestBundle{} 150 for _, tc := range []string{"linux-rel", "MacTests", "scheduled"} { 151 build, err := GetTestBuild("../buildsource/buildbucket", tc) 152 if err != nil { 153 panic(fmt.Errorf("Encountered error while fetching %s.\n%s", tc, err)) 154 } 155 bundles = append(bundles, TestBundle{ 156 Description: fmt.Sprintf("Test page: %s", tc), 157 Data: templates.Args{ 158 "BuildPage": &ui.BuildPage{ 159 Build: ui.Build{ 160 Build: build, 161 Now: nowTS, 162 }, 163 BuildbucketHost: "example.com", 164 }, 165 "XsrfTokenField": template.HTML(`<input name="[XSRF Token]" type="hidden" value="[XSRF Token]">`), 166 "RetryRequestID": "[Retry Request ID]", 167 }, 168 }) 169 } 170 return bundles 171 } 172 173 func consoleTestData() []TestBundle { 174 builder := &ui.BuilderRef{ 175 ID: "buildbucket/luci.project-foo.try/builder-bar", 176 ShortName: "tst", 177 Build: []*model.BuildSummary{ 178 { 179 Summary: model.Summary{ 180 Status: milostatus.Success, 181 }, 182 }, 183 nil, 184 }, 185 Builder: &model.BuilderSummary{ 186 BuilderID: "buildbucket/luci.project-foo.try/builder-bar", 187 ProjectID: "project-foo", 188 LastFinishedStatus: milostatus.InfraFailure, 189 }, 190 } 191 root := ui.NewCategory("Root") 192 root.AddBuilder([]string{"cat1", "cat2"}, builder) 193 return []TestBundle{ 194 { 195 Description: "Full console with Header", 196 Data: templates.Args{ 197 "Expand": false, 198 "Console": consoleRenderer{&ui.Console{ 199 Name: "Test", 200 Project: "Testing", 201 Header: &ui.ConsoleHeader{ 202 Oncalls: []*ui.OncallSummary{ 203 { 204 Name: "Sheriff", 205 Oncallers: template.HTML("test (primary), watcher (secondary)"), 206 }, 207 }, 208 Links: []ui.LinkGroup{ 209 { 210 Name: ui.NewLink("Some group", "", ""), 211 Links: []*ui.Link{ 212 ui.NewLink("LiNk", "something", ""), 213 ui.NewLink("LiNk2", "something2", ""), 214 }, 215 }, 216 }, 217 ConsoleGroups: []ui.ConsoleGroup{ 218 { 219 Title: ui.NewLink("bah", "something2", ""), 220 Consoles: []*ui.BuilderSummaryGroup{ 221 { 222 Name: ui.NewLink("hurrah", "something2", ""), 223 Builders: []*model.BuilderSummary{ 224 { 225 LastFinishedStatus: milostatus.Success, 226 }, 227 { 228 LastFinishedStatus: milostatus.Success, 229 }, 230 { 231 LastFinishedStatus: milostatus.Failure, 232 }, 233 }, 234 }, 235 }, 236 }, 237 { 238 Consoles: []*ui.BuilderSummaryGroup{ 239 { 240 Name: ui.NewLink("hurrah", "something2", ""), 241 Builders: []*model.BuilderSummary{ 242 { 243 LastFinishedStatus: milostatus.Success, 244 }, 245 }, 246 }, 247 }, 248 }, 249 }, 250 }, 251 Commit: []ui.Commit{ 252 { 253 AuthorEmail: "x@example.com", 254 CommitTime: time.Date(12, 12, 12, 12, 12, 12, 0, time.UTC), 255 Revision: ui.NewLink("12031802913871659324", "blah blah blah", ""), 256 Description: "Me too.", 257 }, 258 { 259 AuthorEmail: "y@example.com", 260 CommitTime: time.Date(12, 12, 12, 12, 12, 11, 0, time.UTC), 261 Revision: ui.NewLink("120931820931802913", "blah blah blah 1", ""), 262 Description: "I did something.", 263 }, 264 }, 265 Table: *root, 266 MaxDepth: 3, 267 }}, 268 }, 269 }, 270 } 271 } 272 273 func Frontpage() []TestBundle { 274 return []TestBundle{ 275 { 276 Description: "Basic frontpage", 277 Data: templates.Args{ 278 "frontpage": ui.Frontpage{ 279 Projects: []*projectconfig.Project{ 280 { 281 ID: "fakeproject", 282 HasConfig: true, 283 LogoURL: "https://example.com/logo.png", 284 }, 285 }, 286 }, 287 }, 288 }, 289 } 290 } 291 292 func relatedBuildsTableTestData() []TestBundle { 293 bundles := []TestBundle{} 294 for _, tc := range []string{"MacTests", "scheduled"} { 295 build, err := GetTestBuild("../buildsource/buildbucket", tc) 296 if err != nil { 297 panic(fmt.Errorf("Encountered error while fetching %s.\n%s", tc, err)) 298 } 299 bundles = append(bundles, TestBundle{ 300 Description: fmt.Sprintf("Test related builds table: %s", tc), 301 Data: templates.Args{ 302 "RelatedBuildsTable": &ui.RelatedBuildsTable{ 303 Build: ui.Build{ 304 Build: build, 305 Now: nowTS, 306 }, 307 RelatedBuilds: []*ui.Build{{ 308 Build: build, 309 Now: nowTS, 310 }}, 311 }, 312 }, 313 }) 314 } 315 return bundles 316 } 317 318 // GetTestBuild returns a debug build from testdata. 319 func GetTestBuild(relDir, name string) (*buildbucketpb.Build, error) { 320 fname := fmt.Sprintf("%s.build.jsonpb", name) 321 path := filepath.Join(relDir, "testdata", fname) 322 f, err := os.Open(path) 323 if err != nil { 324 return nil, err 325 } 326 defer f.Close() 327 result := &buildbucketpb.Build{} 328 return result, jsonpb.Unmarshal(f, result) 329 } 330 331 func TestCreateInterpolator(t *testing.T) { 332 Convey("Test createInterpolator", t, func() { 333 Convey("Should encode params", func() { 334 params := httprouter.Params{httprouter.Param{Key: "component2", Value: ":? +"}} 335 interpolator := createInterpolator("/component1/:component2") 336 337 path := interpolator(params) 338 So(path, ShouldEqual, "/component1/"+url.PathEscape(":? +")) 339 }) 340 341 Convey("Should support catching path segments with *", func() { 342 params := httprouter.Params{httprouter.Param{Key: "component2", Value: "/:?/ +"}} 343 interpolator := createInterpolator("/component1/*component2") 344 345 path := interpolator(params) 346 So(path, ShouldEqual, "/component1/"+url.PathEscape(":?")+"/"+url.PathEscape(" +")) 347 }) 348 349 Convey("Should support encoding / with *_", func() { 350 params := httprouter.Params{httprouter.Param{Key: "_component2", Value: "/:?/ +"}} 351 interpolator := createInterpolator("/component1/*_component2") 352 353 path := interpolator(params) 354 So(path, ShouldEqual, "/component1/"+url.PathEscape(":?/ +")) 355 }) 356 }) 357 } 358 359 func TestRedirect(t *testing.T) { 360 Convey("Test redirect", t, func() { 361 client := &http.Client{ 362 // Don't follow the redirect. We want to test the response directly. 363 CheckRedirect: func(req *http.Request, via []*http.Request) error { 364 return http.ErrUseLastResponse 365 }, 366 } 367 368 r := router.New() 369 ts := httptest.NewServer(r) 370 371 Convey("Should not double-encode params", func() { 372 r.GET("/foo/:param", router.NewMiddlewareChain(), redirect("/bar/:param", http.StatusFound)) 373 res, err := client.Get(ts.URL + "/foo/" + url.PathEscape(":? ")) 374 So(err, ShouldBeNil) 375 So(res.StatusCode, ShouldEqual, http.StatusFound) 376 So(res.Header.Get("Location"), ShouldEqual, "/bar/"+url.PathEscape(":? ")) 377 }) 378 }) 379 }