go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/internal/redirect/redirect_test.go (about) 1 // Copyright 2024 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 redirect 16 17 import ( 18 "context" 19 "net/http" 20 "net/http/httptest" 21 "net/url" 22 "testing" 23 24 "github.com/julienschmidt/httprouter" 25 26 "go.chromium.org/luci/auth/identity" 27 "go.chromium.org/luci/gae/impl/memory" 28 "go.chromium.org/luci/gae/service/datastore" 29 "go.chromium.org/luci/server/auth" 30 "go.chromium.org/luci/server/auth/authtest" 31 "go.chromium.org/luci/server/router" 32 33 "go.chromium.org/luci/buildbucket/appengine/internal/config" 34 "go.chromium.org/luci/buildbucket/appengine/model" 35 "go.chromium.org/luci/buildbucket/bbperms" 36 pb "go.chromium.org/luci/buildbucket/proto" 37 38 . "github.com/smartystreets/goconvey/convey" 39 ) 40 41 func TestHandleViewBuild(t *testing.T) { 42 t.Parallel() 43 44 Convey("handleViewBuild", t, func() { 45 rsp := httptest.NewRecorder() 46 rctx := &router.Context{ 47 Writer: rsp, 48 } 49 50 userID := identity.Identity("user:user@example.com") 51 ctx := memory.Use(context.Background()) 52 datastore.GetTestable(ctx).AutoIndex(true) 53 datastore.GetTestable(ctx).Consistent(true) 54 55 ctx = auth.WithState(ctx, &authtest.FakeState{ 56 Identity: userID, 57 }) 58 59 build := &model.Build{ 60 ID: 123, 61 Proto: &pb.Build{ 62 Id: 123, 63 Builder: &pb.BuilderID{ 64 Project: "project", 65 Bucket: "bucket", 66 Builder: "builder", 67 }, 68 }, 69 BackendTarget: "foo", 70 } 71 bucket := &model.Bucket{ID: "bucket", Parent: model.ProjectKey(ctx, "project")} 72 So(datastore.Put(ctx, build, bucket), ShouldBeNil) 73 74 Convey("invalid build id", func() { 75 rctx.Request = (&http.Request{}).WithContext(ctx) 76 rctx.Params = httprouter.Params{ 77 {Key: "BuildID", Value: "foo"}, 78 } 79 80 handleViewBuild(rctx) 81 So(rsp.Code, ShouldEqual, http.StatusBadRequest) 82 So(rsp.Body.String(), ShouldContainSubstring, "invalid build id") 83 }) 84 85 Convey("permission denied", func() { 86 rctx.Request = (&http.Request{}).WithContext(ctx) 87 rctx.Params = httprouter.Params{ 88 {Key: "BuildID", Value: "123"}, 89 } 90 91 handleViewBuild(rctx) 92 So(rsp.Code, ShouldEqual, http.StatusNotFound) 93 So(rsp.Body.String(), ShouldContainSubstring, `resource not found or "user:user@example.com" does not have permission to view it`) 94 }) 95 96 Convey("build not exist", func() { 97 rctx.Request = (&http.Request{}).WithContext(ctx) 98 rctx.Params = httprouter.Params{ 99 {Key: "BuildID", Value: "9"}, 100 } 101 102 handleViewBuild(rctx) 103 So(rsp.Code, ShouldEqual, http.StatusNotFound) 104 So(rsp.Body.String(), ShouldContainSubstring, `resource not found or "user:user@example.com" does not have permission to view it`) 105 }) 106 107 Convey("anonymous", func() { 108 ctx = auth.WithState(ctx, &authtest.FakeState{ 109 Identity: identity.AnonymousIdentity, 110 }) 111 rctx.Request = (&http.Request{}).WithContext(ctx) 112 rctx.Request.URL = &url.URL{ 113 Path: "/build/123", 114 } 115 rctx.Params = httprouter.Params{ 116 {Key: "BuildID", Value: "123"}, 117 } 118 119 handleViewBuild(rctx) 120 So(rsp.Code, ShouldEqual, http.StatusFound) 121 So(rsp.Header().Get("Location"), ShouldEqual, "http://fake.example.com/login?dest=%2Fbuild%2F123") 122 }) 123 124 Convey("ok", func() { 125 ctx = auth.WithState(ctx, &authtest.FakeState{ 126 Identity: userID, 127 FakeDB: authtest.NewFakeDB( 128 authtest.MockPermission(userID, "project:bucket", bbperms.BuildsGet), 129 ), 130 }) 131 132 settingsCfg := &pb.SettingsCfg{ 133 Swarming: &pb.SwarmingSettings{ 134 MiloHostname: "milo.com", 135 }, 136 } 137 So(config.SetTestSettingsCfg(ctx, settingsCfg), ShouldBeNil) 138 139 rctx.Request = (&http.Request{}).WithContext(ctx) 140 rctx.Params = httprouter.Params{ 141 {Key: "BuildID", Value: "123"}, 142 } 143 144 handleViewBuild(rctx) 145 So(rsp.Code, ShouldEqual, http.StatusFound) 146 So(rsp.Header().Get("Location"), ShouldEqual, "https://milo.com/b/123") 147 }) 148 149 Convey("build with view_url", func() { 150 ctx = auth.WithState(ctx, &authtest.FakeState{ 151 Identity: userID, 152 FakeDB: authtest.NewFakeDB( 153 authtest.MockPermission(userID, "project:bucket", bbperms.BuildsGet), 154 ), 155 }) 156 157 settingsCfg := &pb.SettingsCfg{ 158 Swarming: &pb.SwarmingSettings{ 159 MiloHostname: "milo.com", 160 }, 161 } 162 So(config.SetTestSettingsCfg(ctx, settingsCfg), ShouldBeNil) 163 url := "https://another.com" 164 b := &model.Build{ 165 ID: 300, 166 Proto: &pb.Build{ 167 Id: 300, 168 Builder: &pb.BuilderID{ 169 Project: "project", 170 Bucket: "bucket", 171 Builder: "builder", 172 }, 173 ViewUrl: url, 174 }, 175 } 176 So(datastore.Put(ctx, b), ShouldBeNil) 177 178 rctx.Request = (&http.Request{}).WithContext(ctx) 179 rctx.Params = httprouter.Params{ 180 {Key: "BuildID", Value: "300"}, 181 } 182 handleViewBuild(rctx) 183 So(rsp.Code, ShouldEqual, http.StatusFound) 184 So(rsp.Header().Get("Location"), ShouldEqual, url) 185 }) 186 187 Convey("RedirectToTaskPage", func() { 188 ctx = auth.WithState(ctx, &authtest.FakeState{ 189 Identity: userID, 190 FakeDB: authtest.NewFakeDB( 191 authtest.MockPermission(userID, "project:bucket", bbperms.BuildsGet), 192 ), 193 }) 194 195 settingsCfg := &pb.SettingsCfg{ 196 Swarming: &pb.SwarmingSettings{ 197 MiloHostname: "milo.com", 198 }, 199 Backends: []*pb.BackendSetting{ 200 { 201 Target: "foo", 202 Mode: &pb.BackendSetting_FullMode_{ 203 FullMode: &pb.BackendSetting_FullMode{ 204 RedirectToTaskPage: true, 205 }, 206 }, 207 }, 208 }, 209 } 210 So(config.SetTestSettingsCfg(ctx, settingsCfg), ShouldBeNil) 211 bInfra := &model.BuildInfra{ 212 Build: datastore.KeyForObj(ctx, build), 213 Proto: &pb.BuildInfra{ 214 Backend: &pb.BuildInfra_Backend{ 215 Task: &pb.Task{ 216 Id: &pb.TaskID{ 217 Id: "task1", 218 Target: "foo", 219 }, 220 }, 221 }, 222 }, 223 } 224 225 Convey("task.Link is populated", func() { 226 bInfra.Proto.Backend.Task.Link = "https://fake-task-page-link" 227 So(datastore.Put(ctx, bInfra), ShouldBeNil) 228 rctx.Request = (&http.Request{}).WithContext(ctx) 229 rctx.Params = httprouter.Params{ 230 {Key: "BuildID", Value: "123"}, 231 } 232 233 handleViewBuild(rctx) 234 So(rsp.Code, ShouldEqual, http.StatusFound) 235 So(rsp.Header().Get("Location"), ShouldEqual, "https://fake-task-page-link") 236 }) 237 238 Convey("task.Link is not populated", func() { 239 bInfra.Proto.Backend.Task.Link = "" 240 So(datastore.Put(ctx, bInfra), ShouldBeNil) 241 rctx.Request = (&http.Request{}).WithContext(ctx) 242 rctx.Params = httprouter.Params{ 243 {Key: "BuildID", Value: "123"}, 244 } 245 246 handleViewBuild(rctx) 247 So(rsp.Code, ShouldEqual, http.StatusFound) 248 So(rsp.Header().Get("Location"), ShouldEqual, "https://milo.com/b/123") 249 }) 250 }) 251 }) 252 } 253 254 func TestHandleViewLog(t *testing.T) { 255 t.Parallel() 256 257 Convey("handleViewLog", t, func() { 258 rsp := httptest.NewRecorder() 259 rctx := &router.Context{ 260 Writer: rsp, 261 } 262 263 userID := identity.Identity("user:user@example.com") 264 ctx := memory.Use(context.Background()) 265 datastore.GetTestable(ctx).AutoIndex(true) 266 datastore.GetTestable(ctx).Consistent(true) 267 268 ctx = auth.WithState(ctx, &authtest.FakeState{ 269 Identity: userID, 270 FakeDB: authtest.NewFakeDB( 271 authtest.MockPermission(userID, "project:bucket", bbperms.BuildsGet), 272 ), 273 }) 274 275 build := &model.Build{ 276 ID: 123, 277 Proto: &pb.Build{ 278 Id: 123, 279 Builder: &pb.BuilderID{ 280 Project: "project", 281 Bucket: "bucket", 282 Builder: "builder", 283 }, 284 }, 285 } 286 bucket := &model.Bucket{ID: "bucket", Parent: model.ProjectKey(ctx, "project")} 287 bs := &model.BuildSteps{ 288 Build: datastore.KeyForObj(ctx, build), 289 } 290 So(bs.FromProto([]*pb.Step{ 291 { 292 Name: "first", 293 SummaryMarkdown: "summary", 294 Logs: []*pb.Log{{ 295 Name: "stdout", 296 Url: "url", 297 ViewUrl: "https://log.com/123/first", 298 }, 299 }, 300 }, 301 }), ShouldBeNil) 302 So(datastore.Put(ctx, build, bucket, bs), ShouldBeNil) 303 304 Convey("invalid build id", func() { 305 rctx.Request = (&http.Request{}).WithContext(ctx) 306 rctx.Params = httprouter.Params{ 307 {Key: "BuildID", Value: "foo"}, 308 {Key: "StepName", Value: "first"}, 309 } 310 311 handleViewLog(rctx) 312 So(rsp.Code, ShouldEqual, http.StatusBadRequest) 313 So(rsp.Body.String(), ShouldContainSubstring, "invalid build id") 314 }) 315 316 Convey("no access to build", func() { 317 ctx = auth.WithState(ctx, &authtest.FakeState{ 318 Identity: identity.Identity("user:random@example.com"), 319 }) 320 321 rctx.Request = (&http.Request{}).WithContext(ctx) 322 rctx.Params = httprouter.Params{ 323 {Key: "BuildID", Value: "123"}, 324 {Key: "StepName", Value: "first"}, 325 } 326 327 handleViewLog(rctx) 328 So(rsp.Code, ShouldEqual, http.StatusNotFound) 329 So(rsp.Body.String(), ShouldContainSubstring, `resource not found or "user:random@example.com" does not have permission to view it`) 330 }) 331 332 Convey("build not found", func() { 333 rctx.Request = (&http.Request{}).WithContext(ctx) 334 rctx.Params = httprouter.Params{ 335 {Key: "BuildID", Value: "9"}, 336 {Key: "StepName", Value: "first"}, 337 } 338 339 handleViewLog(rctx) 340 So(rsp.Code, ShouldEqual, http.StatusNotFound) 341 So(rsp.Body.String(), ShouldContainSubstring, `resource not found or "user:user@example.com" does not have permission to view it`) 342 }) 343 344 Convey("no steps", func() { 345 rctx.Request = (&http.Request{}).WithContext(ctx) 346 rctx.Params = httprouter.Params{ 347 {Key: "BuildID", Value: "123"}, 348 {Key: "StepName", Value: "first"}, 349 } 350 So(datastore.Delete(ctx, bs), ShouldBeNil) 351 352 handleViewLog(rctx) 353 So(rsp.Code, ShouldEqual, http.StatusNotFound) 354 So(rsp.Body.String(), ShouldContainSubstring, "no steps found") 355 }) 356 357 Convey("the requested step not found", func() { 358 rctx.Request = (&http.Request{}).WithContext(ctx) 359 rctx.Request.URL = &url.URL{} 360 rctx.Params = httprouter.Params{ 361 {Key: "BuildID", Value: "123"}, 362 {Key: "StepName", Value: "second"}, 363 } 364 365 handleViewLog(rctx) 366 So(rsp.Code, ShouldEqual, http.StatusNotFound) 367 So(rsp.Body.String(), ShouldContainSubstring, `view url for log "stdout" in step "second" in build 123 not found`) 368 }) 369 370 Convey("the requested log not found", func() { 371 rctx.Request = (&http.Request{}).WithContext(ctx) 372 rctx.Request.URL = &url.URL{ 373 RawQuery: "log=stderr", 374 } 375 rctx.Params = httprouter.Params{ 376 {Key: "BuildID", Value: "123"}, 377 {Key: "StepName", Value: "first"}, 378 } 379 380 handleViewLog(rctx) 381 So(rsp.Code, ShouldEqual, http.StatusNotFound) 382 So(rsp.Body.String(), ShouldContainSubstring, `view url for log "stderr" in step "first" in build 123 not found`) 383 }) 384 385 Convey("ok", func() { 386 rctx.Request = (&http.Request{}).WithContext(ctx) 387 rctx.Request.URL = &url.URL{} 388 rctx.Params = httprouter.Params{ 389 {Key: "BuildID", Value: "123"}, 390 {Key: "StepName", Value: "first"}, 391 } 392 393 handleViewLog(rctx) 394 So(rsp.Code, ShouldEqual, http.StatusFound) 395 So(rsp.Header().Get("Location"), ShouldEqual, "https://log.com/123/first") 396 }) 397 }) 398 }