go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/swarming/server/rpcs/base_test.go (about) 1 // Copyright 2023 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 "testing" 20 21 "google.golang.org/grpc" 22 "google.golang.org/protobuf/encoding/prototext" 23 "google.golang.org/protobuf/proto" 24 25 "go.chromium.org/luci/auth/identity" 26 "go.chromium.org/luci/config" 27 "go.chromium.org/luci/config/cfgclient" 28 cfgmem "go.chromium.org/luci/config/impl/memory" 29 "go.chromium.org/luci/gae/impl/memory" 30 "go.chromium.org/luci/server/auth" 31 "go.chromium.org/luci/server/auth/authtest" 32 "go.chromium.org/luci/server/auth/realms" 33 34 apipb "go.chromium.org/luci/swarming/proto/api_v2" 35 configpb "go.chromium.org/luci/swarming/proto/config" 36 "go.chromium.org/luci/swarming/server/acls" 37 "go.chromium.org/luci/swarming/server/cfg" 38 39 . "github.com/smartystreets/goconvey/convey" 40 ) 41 42 const ( 43 // DefaultFakeCaller is used in unit tests by default as the caller identity. 44 DefaultFakeCaller identity.Identity = "user:test@example.com" 45 // AdminFakeCaller is used in unit tests to make calls as an admin. 46 AdminFakeCaller identity.Identity = "user:admin@example.com" 47 ) 48 49 // MockedConfig is a bundle of configs to use in tests. 50 type MockedConfigs struct { 51 Settings *configpb.SettingsCfg 52 Pools *configpb.PoolsCfg 53 Bots *configpb.BotsCfg 54 Scripts map[string]string 55 } 56 57 // MockedRequestState is all per-RPC state that can be mocked. 58 type MockedRequestState struct { 59 Caller identity.Identity 60 AuthDB *authtest.FakeDB 61 Configs MockedConfigs 62 } 63 64 // NewMockedRequestState creates a new empty request state that can be mutated 65 // by calling its various methods before it is passed to MockRequestState. 66 func NewMockedRequestState() *MockedRequestState { 67 return &MockedRequestState{ 68 Caller: DefaultFakeCaller, 69 AuthDB: authtest.NewFakeDB( 70 authtest.MockMembership(AdminFakeCaller, "tests-admin-group"), 71 ), 72 Configs: MockedConfigs{ 73 Settings: &configpb.SettingsCfg{ 74 Auth: &configpb.AuthSettings{ 75 AdminsGroup: "tests-admin-group", 76 }, 77 }, 78 Pools: &configpb.PoolsCfg{}, 79 Bots: &configpb.BotsCfg{ 80 TrustedDimensions: []string{"pool"}, 81 }, 82 Scripts: map[string]string{}, 83 }, 84 } 85 } 86 87 // MockPool adds a new pool to the mocked request state. 88 func (s *MockedRequestState) MockPool(name, realm string) *configpb.Pool { 89 pb := &configpb.Pool{ 90 Name: []string{name}, 91 Realm: realm, 92 } 93 s.Configs.Pools.Pool = append(s.Configs.Pools.Pool, pb) 94 return pb 95 } 96 97 // MockBot adds a bot in some pool. 98 func (s *MockedRequestState) MockBot(botID, pool string) *configpb.BotGroup { 99 pb := &configpb.BotGroup{ 100 BotId: []string{botID}, 101 Dimensions: []string{"pool:" + pool}, 102 Auth: []*configpb.BotAuth{ 103 { 104 RequireLuciMachineToken: true, 105 }, 106 }, 107 } 108 s.Configs.Bots.BotGroup = append(s.Configs.Bots.BotGroup, pb) 109 return pb 110 } 111 112 // MockPerm mocks a permission the caller will have in some realm. 113 func (s *MockedRequestState) MockPerm(realm string, perm ...realms.Permission) { 114 for _, p := range perm { 115 s.AuthDB.AddMocks(authtest.MockPermission(s.Caller, realm, p)) 116 } 117 } 118 119 // SetCaller returns a copy of the state with a different active caller. 120 func (s *MockedRequestState) SetCaller(id identity.Identity) *MockedRequestState { 121 cpy := *s 122 cpy.Caller = id 123 return &cpy 124 } 125 126 // MockConfig puts configs in datastore and loads then into cfg.Provider. 127 // 128 // Configs must be valid, panics if they are not. 129 func MockConfigs(ctx context.Context, configs MockedConfigs) *cfg.Provider { 130 files := make(cfgmem.Files) 131 132 putPb := func(path string, msg proto.Message) { 133 if msg != nil { 134 blob, err := prototext.Marshal(msg) 135 if err != nil { 136 panic(err) 137 } 138 files[path] = string(blob) 139 } 140 } 141 142 putPb("settings.cfg", configs.Settings) 143 putPb("pools.cfg", configs.Pools) 144 putPb("bots.cfg", configs.Bots) 145 for path, body := range configs.Scripts { 146 files[path] = body 147 } 148 149 // Put new configs into the datastore. 150 err := cfg.UpdateConfigs(cfgclient.Use(ctx, cfgmem.New(map[config.Set]cfgmem.Files{ 151 "services/${appid}": files, 152 }))) 153 if err != nil { 154 panic(err) 155 } 156 157 // Load them back in a queriable form. 158 p, err := cfg.NewProvider(ctx) 159 if err != nil { 160 panic(err) 161 } 162 return p 163 } 164 165 // MockRequestState prepares a full mock of a per-RPC request state. 166 // 167 // Panics if it is invalid. 168 func MockRequestState(ctx context.Context, state *MockedRequestState) context.Context { 169 ctx = auth.WithState(ctx, &authtest.FakeState{ 170 Identity: state.Caller, 171 FakeDB: state.AuthDB, 172 }) 173 cfg := MockConfigs(ctx, state.Configs).Config(ctx) 174 return context.WithValue(ctx, &requestStateCtxKey, &RequestState{ 175 Config: cfg, 176 ACL: acls.NewChecker(ctx, cfg), 177 }) 178 } 179 180 func TestServerInterceptor(t *testing.T) { 181 t.Parallel() 182 183 Convey("With config in datastore", t, func() { 184 ctx := memory.Use(context.Background()) 185 ctx = auth.WithState(ctx, &authtest.FakeState{ 186 Identity: identity.AnonymousIdentity, 187 }) 188 cfg := MockConfigs(ctx, MockedConfigs{}) 189 190 interceptor := ServerInterceptor(cfg, []*grpc.ServiceDesc{ 191 &apipb.Swarming_ServiceDesc, 192 }) 193 194 Convey("Sets up state", func() { 195 var state *RequestState 196 err := interceptor(ctx, "/swarming.v2.Swarming/GetPermissions", func(ctx context.Context) error { 197 state = State(ctx) 198 return nil 199 }) 200 So(err, ShouldBeNil) 201 So(state, ShouldNotBeNil) 202 So(state.Config, ShouldNotBeNil) 203 So(state.ACL, ShouldNotBeNil) 204 }) 205 206 Convey("Skips unrelated APIs", func() { 207 var called bool 208 err := interceptor(ctx, "/another.Service/GetPermissions", func(ctx context.Context) error { 209 called = true 210 defer func() { So(recover(), ShouldNotBeNil) }() 211 State(ctx) // panics 212 return nil 213 }) 214 So(err, ShouldBeNil) 215 So(called, ShouldBeTrue) 216 }) 217 }) 218 }