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  }