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  }