blob: 99ce6d490fbaf94566b79fedb16d37e0ad18ebf3 [file] [log] [blame]
// Copyright 2024 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package bbtaskbackend
import (
"context"
"testing"
"google.golang.org/grpc/codes"
"google.golang.org/protobuf/encoding/prototext"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
"go.chromium.org/luci/auth/identity"
bbpb "go.chromium.org/luci/buildbucket/proto"
"go.chromium.org/luci/config"
"go.chromium.org/luci/config/cfgclient"
cfgmem "go.chromium.org/luci/config/impl/memory"
"go.chromium.org/luci/gae/impl/memory"
"go.chromium.org/luci/gae/service/datastore"
"go.chromium.org/luci/server/auth"
"go.chromium.org/luci/server/auth/authtest"
apipb "go.chromium.org/luci/swarming/proto/api_v2"
configpb "go.chromium.org/luci/swarming/proto/config"
"go.chromium.org/luci/swarming/server/acls"
"go.chromium.org/luci/swarming/server/cfg"
"go.chromium.org/luci/swarming/server/model"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
)
func TestFetchTasks(t *testing.T) {
t.Parallel()
Convey("FetchTasks", t, func() {
caller := identity.Identity("user:someone@example.com")
ctx := auth.WithState(memory.Use(context.Background()), &authtest.FakeState{
Identity: caller,
FakeDB: authtest.NewFakeDB(
authtest.MockPermission(caller, "project:visible-realm", acls.PermTasksGet),
),
})
srv := TaskBackend{
bbTarget: "target",
cfgProvider: mockCfgProvider(ctx),
}
reqKey, err := model.TaskIDToRequestKey(ctx, "65aba3a3e6b99310")
So(err, ShouldBeNil)
buildTask := &model.BuildTask{
Key: model.BuildTaskKey(ctx, reqKey),
BuildID: "1",
BuildbucketHost: "bb-host",
UpdateID: 100,
LatestTaskStatus: apipb.TaskState_RUNNING,
}
resultSummary := &model.TaskResultSummary{
Key: model.TaskResultSummaryKey(ctx, reqKey),
RequestRealm: "project:visible-realm",
RequestPool: "visible",
TaskResultCommon: model.TaskResultCommon{
State: apipb.TaskState_RUNNING,
Failure: false,
BotDimensions: model.BotDimensions{"dim": []string{"a", "b"}},
},
}
So(datastore.Put(ctx, buildTask, resultSummary), ShouldBeNil)
Convey("empty req", func() {
req := &bbpb.FetchTasksRequest{}
res, err := srv.FetchTasks(ctx, req)
So(err, ShouldBeNil)
So(res, ShouldResembleProto, &bbpb.FetchTasksResponse{})
})
Convey("bad task id format", func() {
req := &bbpb.FetchTasksRequest{
TaskIds: []*bbpb.TaskID{
{
Target: "target",
Id: "zzz",
},
},
}
res, err := srv.FetchTasks(ctx, req)
So(err, ShouldBeNil)
errPb, ok := res.Responses[0].Response.(*bbpb.FetchTasksResponse_Response_Error)
So(ok, ShouldBeTrue)
So(errPb.Error.Code, ShouldEqual, codes.InvalidArgument)
So(errPb.Error.Message, ShouldContainSubstring, "bad task ID")
})
Convey("BuildTask not found", func() {
So(datastore.Delete(ctx, buildTask), ShouldBeNil)
req := &bbpb.FetchTasksRequest{
TaskIds: []*bbpb.TaskID{
{
Target: "target",
Id: "65aba3a3e6b99310",
},
},
}
res, err := srv.FetchTasks(ctx, req)
So(err, ShouldBeNil)
errPb, ok := res.Responses[0].Response.(*bbpb.FetchTasksResponse_Response_Error)
So(ok, ShouldBeTrue)
So(errPb.Error.Code, ShouldEqual, codes.NotFound)
So(errPb.Error.Message, ShouldContainSubstring, "BuildTask not found 65aba3a3e6b99310")
})
Convey("TaskResultSummary not found", func() {
So(datastore.Delete(ctx, resultSummary), ShouldBeNil)
req := &bbpb.FetchTasksRequest{
TaskIds: []*bbpb.TaskID{
{
Target: "target",
Id: "65aba3a3e6b99310",
},
},
}
res, err := srv.FetchTasks(ctx, req)
So(err, ShouldBeNil)
errPb, ok := res.Responses[0].Response.(*bbpb.FetchTasksResponse_Response_Error)
So(ok, ShouldBeTrue)
So(errPb.Error.Code, ShouldEqual, codes.NotFound)
So(errPb.Error.Message, ShouldContainSubstring, "TaskResultSummary not found 65aba3a3e6b99310")
})
Convey("no perm", func() {
resultSummary.RequestRealm = "project:hidden-realm"
resultSummary.RequestPool = "hidden"
So(datastore.Put(ctx, resultSummary), ShouldBeNil)
req := &bbpb.FetchTasksRequest{
TaskIds: []*bbpb.TaskID{
{
Target: "target",
Id: "65aba3a3e6b99310",
},
},
}
res, err := srv.FetchTasks(ctx, req)
So(err, ShouldBeNil)
errPb, ok := res.Responses[0].Response.(*bbpb.FetchTasksResponse_Response_Error)
So(ok, ShouldBeTrue)
So(errPb.Error.Code, ShouldEqual, codes.PermissionDenied)
So(errPb.Error.Message, ShouldContainSubstring, "doesn't have permission")
})
Convey("ok - one", func() {
req := &bbpb.FetchTasksRequest{
TaskIds: []*bbpb.TaskID{
{
Target: "target",
Id: "65aba3a3e6b99310",
},
},
}
res, err := srv.FetchTasks(ctx, req)
So(err, ShouldBeNil)
So(res, ShouldResembleProto, &bbpb.FetchTasksResponse{
Responses: []*bbpb.FetchTasksResponse_Response{
{
Response: &bbpb.FetchTasksResponse_Response_Task{
Task: &bbpb.Task{
Status: bbpb.Status_STARTED,
Id: &bbpb.TaskID{
Id: "65aba3a3e6b99310",
Target: "target",
},
UpdateId: 100,
Details: &structpb.Struct{
Fields: map[string]*structpb.Value{
"bot_dimensions": {
Kind: &structpb.Value_StructValue{
StructValue: resultSummary.BotDimensions.ToStructPB(),
},
},
},
},
},
},
},
},
})
})
Convey("ok - multiple", func() {
key, err := model.TaskIDToRequestKey(ctx, "65aba3a3e6b99320")
So(err, ShouldBeNil)
bTask := &model.BuildTask{
Key: model.BuildTaskKey(ctx, key),
BuildID: "2",
BuildbucketHost: "bb-host",
UpdateID: 200,
LatestTaskStatus: apipb.TaskState_COMPLETED,
}
trs := &model.TaskResultSummary{
Key: model.TaskResultSummaryKey(ctx, key),
RequestRealm: "project:visible-realm",
RequestPool: "visible",
TaskResultCommon: model.TaskResultCommon{
State: apipb.TaskState_COMPLETED,
Failure: false,
BotDimensions: model.BotDimensions{"dim": []string{"a", "b"}},
},
}
So(datastore.Put(ctx, bTask, trs), ShouldBeNil)
req := &bbpb.FetchTasksRequest{
TaskIds: []*bbpb.TaskID{
{
Target: "target",
Id: "65aba3a3e6b99310",
},
{
Target: "target",
Id: "zzz",
},
{
Target: "target",
Id: "65aba3a3e6b99320",
},
{
Target: "target",
Id: "65aba3a3e6b99330",
},
},
}
res, err := srv.FetchTasks(ctx, req)
So(err, ShouldBeNil)
// the response should be [found, bad_id, found, not_found]
responses := res.Responses
So(responses[0].Response.(*bbpb.FetchTasksResponse_Response_Task).Task.Id.Id, ShouldEqual, "65aba3a3e6b99310")
So(responses[1].Response.(*bbpb.FetchTasksResponse_Response_Error).Error.Code, ShouldEqual, codes.InvalidArgument)
So(responses[2].Response.(*bbpb.FetchTasksResponse_Response_Task).Task.Id.Id, ShouldEqual, "65aba3a3e6b99320")
So(responses[3].Response.(*bbpb.FetchTasksResponse_Response_Error).Error.Code, ShouldEqual, codes.NotFound)
})
})
}
func mockCfgProvider(ctx context.Context) *cfg.Provider {
poolsPb := &configpb.PoolsCfg{
Pool: []*configpb.Pool{
{
Name: []string{"visible"},
Realm: "project:visible-realm",
},
}}
files := make(cfgmem.Files)
putPb := func(path string, msg proto.Message) {
if msg != nil {
blob, err := prototext.Marshal(msg)
if err != nil {
panic(err)
}
files[path] = string(blob)
}
}
putPb("pools.cfg", poolsPb)
// Load them back in a queriable form.
err := cfg.UpdateConfigs(cfgclient.Use(ctx, cfgmem.New(map[config.Set]cfgmem.Files{
"services/${appid}": files,
})))
if err != nil {
panic(err)
}
p, err := cfg.NewProvider(ctx)
if err != nil {
panic(err)
}
return p
}