go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/swarming/server/rpcs/bots_count_bots_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 rpcs 16 17 import ( 18 "context" 19 "strings" 20 "testing" 21 22 "google.golang.org/grpc/codes" 23 24 "go.chromium.org/luci/gae/impl/memory" 25 "go.chromium.org/luci/gae/service/datastore" 26 27 apipb "go.chromium.org/luci/swarming/proto/api_v2" 28 "go.chromium.org/luci/swarming/server/model" 29 30 . "github.com/smartystreets/goconvey/convey" 31 . "go.chromium.org/luci/common/testing/assertions" 32 ) 33 34 func TestCountBots(t *testing.T) { 35 t.Parallel() 36 37 ctx := memory.Use(context.Background()) 38 datastore.GetTestable(ctx).AutoIndex(true) 39 datastore.GetTestable(ctx).Consistent(true) 40 41 // See bots_list_bots_test.go 42 state := setupTestBots(ctx) 43 44 callImpl := func(ctx context.Context, req *apipb.BotsCountRequest) (*apipb.BotsCount, error) { 45 return (&BotsServer{ 46 // memory.Use(...) datastore fake doesn't support IN queries currently. 47 BotQuerySplitMode: model.SplitCompletely, 48 }).CountBots(ctx, req) 49 } 50 call := func(req *apipb.BotsCountRequest) (*apipb.BotsCount, error) { 51 return callImpl(MockRequestState(ctx, state), req) 52 } 53 callAsAdmin := func(req *apipb.BotsCountRequest) (*apipb.BotsCount, error) { 54 return callImpl(MockRequestState(ctx, state.SetCaller(AdminFakeCaller)), req) 55 } 56 57 Convey("Dimensions filter is checked", t, func() { 58 _, err := call(&apipb.BotsCountRequest{ 59 Dimensions: []*apipb.StringPair{ 60 {Key: "", Value: ""}, 61 }, 62 }) 63 So(err, ShouldHaveGRPCStatus, codes.InvalidArgument) 64 }) 65 66 Convey("ACLs", t, func() { 67 Convey("Listing only visible pools: OK", func() { 68 _, err := call(&apipb.BotsCountRequest{ 69 Dimensions: []*apipb.StringPair{ 70 {Key: "pool", Value: "visible-pool1|visible-pool2"}, 71 }, 72 }) 73 So(err, ShouldBeNil) 74 }) 75 76 Convey("Listing visible and invisible pool: permission denied", func() { 77 _, err := call(&apipb.BotsCountRequest{ 78 Dimensions: []*apipb.StringPair{ 79 {Key: "pool", Value: "visible-pool1|hidden-pool1"}, 80 }, 81 }) 82 So(err, ShouldHaveGRPCStatus, codes.PermissionDenied) 83 }) 84 85 Convey("Listing visible and invisible pool as admin: OK", func() { 86 _, err := callAsAdmin(&apipb.BotsCountRequest{ 87 Dimensions: []*apipb.StringPair{ 88 {Key: "pool", Value: "visible-pool1|hidden-pool1"}, 89 }, 90 }) 91 So(err, ShouldBeNil) 92 }) 93 94 Convey("Listing all pools as non-admin: permission denied", func() { 95 _, err := call(&apipb.BotsCountRequest{}) 96 So(err, ShouldHaveGRPCStatus, codes.PermissionDenied) 97 }) 98 99 Convey("Listing all pools as admin: OK", func() { 100 _, err := callAsAdmin(&apipb.BotsCountRequest{}) 101 So(err, ShouldBeNil) 102 }) 103 }) 104 105 Convey("Filtering", t, func() { 106 count := func(dims ...string) *apipb.BotsCount { 107 req := &apipb.BotsCountRequest{} 108 for _, kv := range dims { 109 k, v, _ := strings.Cut(kv, ":") 110 req.Dimensions = append(req.Dimensions, &apipb.StringPair{ 111 Key: k, 112 Value: v, 113 }) 114 } 115 resp, err := callAsAdmin(req) 116 So(err, ShouldBeNil) 117 So(resp.Now, ShouldNotBeNil) 118 resp.Now = nil // for easier comparison 119 return resp 120 } 121 122 // No filters. 123 So(count(), ShouldResembleProto, &apipb.BotsCount{ 124 Count: 24, 125 Quarantined: 3, 126 Maintenance: 3, 127 Dead: 3, 128 Busy: 3, 129 }) 130 131 // Simple filter. 132 So(count("idx:1"), ShouldResembleProto, &apipb.BotsCount{ 133 Count: 8, 134 Quarantined: 1, 135 Maintenance: 1, 136 Dead: 1, 137 Busy: 1, 138 }) 139 140 // AND filter. 141 So(count("idx:1", "pool:visible-pool2"), ShouldResembleProto, &apipb.BotsCount{ 142 Count: 1, 143 }) 144 145 // OR filter. 146 So(count("idx:0|1"), ShouldResembleProto, &apipb.BotsCount{ 147 Count: 16, 148 Quarantined: 2, 149 Maintenance: 2, 150 Dead: 2, 151 Busy: 2, 152 }) 153 154 // OR filter with intersecting results. This covers all bots, twice. 155 So(count("idx:0|1|2", "dup:0|1|2"), ShouldResembleProto, &apipb.BotsCount{ 156 Count: 24, 157 Quarantined: 3, 158 Maintenance: 3, 159 Dead: 3, 160 Busy: 3, 161 }) 162 163 // OR filter with no results. 164 So(count("idx:4|5|6", "pool:visible-pool1"), ShouldResembleProto, &apipb.BotsCount{ 165 Count: 0, 166 Quarantined: 0, 167 Maintenance: 0, 168 Dead: 0, 169 Busy: 0, 170 }) 171 }) 172 }