go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/swarming/server/rbe/session_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 rbe
    16  
    17  import (
    18  	"context"
    19  	"testing"
    20  	"time"
    21  
    22  	statuspb "google.golang.org/genproto/googleapis/rpc/status"
    23  	"google.golang.org/grpc"
    24  	"google.golang.org/grpc/codes"
    25  	"google.golang.org/grpc/status"
    26  	"google.golang.org/protobuf/types/known/anypb"
    27  	"google.golang.org/protobuf/types/known/timestamppb"
    28  
    29  	"go.chromium.org/luci/common/clock/testclock"
    30  	"go.chromium.org/luci/server/secrets"
    31  
    32  	"go.chromium.org/luci/swarming/internal/remoteworkers"
    33  	internalspb "go.chromium.org/luci/swarming/proto/internals"
    34  	"go.chromium.org/luci/swarming/server/botsrv"
    35  	"go.chromium.org/luci/swarming/server/hmactoken"
    36  
    37  	. "github.com/smartystreets/goconvey/convey"
    38  	. "go.chromium.org/luci/common/testing/assertions"
    39  )
    40  
    41  func TestSessionServer(t *testing.T) {
    42  	t.Parallel()
    43  
    44  	Convey("With server", t, func() {
    45  		const (
    46  			fakeRBEInstance   = "fake-rbe-instance"
    47  			fakeBotID         = "fake-bot-id"
    48  			fakeSessionID     = "fake-rbe-session"
    49  			fakeFirstLeaseID  = "fake-first-lease-id"
    50  			fakeSecondLeaseID = "fake-second-lease-id"
    51  			fakeFirstTaskID   = "fake-first-task-id"
    52  			fakeSecondTaskID  = "fake-second-task-id"
    53  		)
    54  
    55  		fakePollState := &internalspb.PollState{
    56  			Id:          "fake-poll-token",
    57  			RbeInstance: fakeRBEInstance,
    58  			IpAllowlist: "fake-ip-allowlist",
    59  		}
    60  
    61  		fakeRequest := &botsrv.Request{
    62  			BotID:     fakeBotID,
    63  			SessionID: fakeSessionID,
    64  			PollState: fakePollState,
    65  			Dimensions: map[string][]string{
    66  				"id":     {fakeBotID},
    67  				"pool":   {"some-pool"},
    68  				"extra1": {"a", "b"},
    69  				"extra2": {"c", "d"},
    70  			},
    71  		}
    72  
    73  		payload := func(taskID string) *anypb.Any {
    74  			msg, err := anypb.New(&internalspb.TaskPayload{TaskId: taskID})
    75  			So(err, ShouldBeNil)
    76  			return msg
    77  		}
    78  
    79  		result := func() *anypb.Any {
    80  			msg, err := anypb.New(&internalspb.TaskResult{})
    81  			So(err, ShouldBeNil)
    82  			return msg
    83  		}
    84  
    85  		now := time.Date(2044, time.April, 4, 4, 4, 4, 4, time.UTC)
    86  		ctx := context.Background()
    87  		ctx, _ = testclock.UseTime(ctx, now)
    88  
    89  		rbe := &mockedBotsClient{}
    90  
    91  		srv := &SessionServer{
    92  			rbe: rbe,
    93  			hmacSecret: hmactoken.NewStaticSecret(secrets.Secret{
    94  				Active: []byte("secret"),
    95  			}),
    96  		}
    97  
    98  		Convey("CreateBotSession works", func() {
    99  			rbe.expectCreateBotSession(func(r *remoteworkers.CreateBotSessionRequest) (*remoteworkers.BotSession, error) {
   100  				So(r, ShouldResembleProto, &remoteworkers.CreateBotSessionRequest{
   101  					Parent: fakeRBEInstance,
   102  					BotSession: &remoteworkers.BotSession{
   103  						BotId:   fakeBotID,
   104  						Status:  remoteworkers.BotStatus_INITIALIZING,
   105  						Version: "bot-version",
   106  						Worker: &remoteworkers.Worker{
   107  							Properties: []*remoteworkers.Worker_Property{
   108  								{Key: "rbePoolID", Value: "rbe-pool-id"},
   109  								{Key: "rbePoolVersion", Value: "rbe-pool-version"},
   110  							},
   111  							Devices: []*remoteworkers.Device{
   112  								{
   113  									Handle: "primary",
   114  									Properties: []*remoteworkers.Device_Property{
   115  										{Key: "label:extra1", Value: "a"},
   116  										{Key: "label:extra1", Value: "b"},
   117  										{Key: "label:extra2", Value: "c"},
   118  										{Key: "label:extra2", Value: "d"},
   119  										{Key: "label:pool", Value: "some-pool"},
   120  									},
   121  								},
   122  							},
   123  						},
   124  					},
   125  				})
   126  				return &remoteworkers.BotSession{
   127  					Name:   fakeSessionID,
   128  					Status: remoteworkers.BotStatus_INITIALIZING,
   129  				}, nil
   130  			})
   131  
   132  			resp, err := srv.CreateBotSession(ctx, &CreateBotSessionRequest{
   133  				Dimensions: map[string][]string{
   134  					"ignored": {""}, // uses validated botsrv.Request.Dimensions instead
   135  				},
   136  				BotVersion: "bot-version",
   137  				WorkerProperties: &WorkerProperties{
   138  					PoolID:      "rbe-pool-id",
   139  					PoolVersion: "rbe-pool-version",
   140  				},
   141  			}, fakeRequest)
   142  
   143  			So(err, ShouldBeNil)
   144  
   145  			expectedExpiry := now.Add(sessionTokenExpiry).Round(time.Second)
   146  
   147  			msg := resp.(*CreateBotSessionResponse)
   148  			So(msg.SessionExpiry, ShouldEqual, expectedExpiry.Unix())
   149  			So(msg.SessionID, ShouldEqual, fakeSessionID)
   150  
   151  			session := &internalspb.BotSession{}
   152  			So(srv.hmacSecret.ValidateToken(msg.SessionToken, session), ShouldBeNil)
   153  			So(session, ShouldResembleProto, &internalspb.BotSession{
   154  				RbeBotSessionId: fakeSessionID,
   155  				PollState:       fakePollState,
   156  				Expiry:          timestamppb.New(expectedExpiry),
   157  			})
   158  		})
   159  
   160  		Convey("CreateBotSession propagates RBE error", func() {
   161  			rbe.expectCreateBotSession(func(r *remoteworkers.CreateBotSessionRequest) (*remoteworkers.BotSession, error) {
   162  				return nil, status.Errorf(codes.FailedPrecondition, "boom")
   163  			})
   164  			_, err := srv.CreateBotSession(ctx, &CreateBotSessionRequest{}, fakeRequest)
   165  			So(err, ShouldHaveGRPCStatus, codes.FailedPrecondition)
   166  			So(err, ShouldErrLike, "boom")
   167  		})
   168  
   169  		Convey("UpdateBotSession IDLE", func() {
   170  			rbe.expectUpdateBotSession(func(r *remoteworkers.UpdateBotSessionRequest) (*remoteworkers.BotSession, error) {
   171  				So(r, ShouldResembleProto, &remoteworkers.UpdateBotSessionRequest{
   172  					Name: fakeSessionID,
   173  					BotSession: &remoteworkers.BotSession{
   174  						Name:    fakeSessionID,
   175  						BotId:   fakeBotID,
   176  						Status:  remoteworkers.BotStatus_OK,
   177  						Version: "bot-version",
   178  						Worker: &remoteworkers.Worker{
   179  							Properties: []*remoteworkers.Worker_Property{
   180  								{Key: "rbePoolID", Value: "rbe-pool-id"},
   181  								{Key: "rbePoolVersion", Value: "rbe-pool-version"},
   182  							},
   183  							Devices: []*remoteworkers.Device{
   184  								{
   185  									Handle: "primary",
   186  									Properties: []*remoteworkers.Device_Property{
   187  										{Key: "label:extra1", Value: "a"},
   188  										{Key: "label:extra1", Value: "b"},
   189  										{Key: "label:extra2", Value: "c"},
   190  										{Key: "label:extra2", Value: "d"},
   191  										{Key: "label:pool", Value: "some-pool"},
   192  									},
   193  								},
   194  							},
   195  						},
   196  					},
   197  				})
   198  				return &remoteworkers.BotSession{
   199  					Name:   fakeSessionID,
   200  					Status: remoteworkers.BotStatus_OK,
   201  				}, nil
   202  			})
   203  
   204  			resp, err := srv.UpdateBotSession(ctx, &UpdateBotSessionRequest{
   205  				Status: "OK",
   206  				Dimensions: map[string][]string{
   207  					"ignored": {""}, // uses validated botsrv.Request.Dimensions instead
   208  				},
   209  				BotVersion: "bot-version",
   210  				WorkerProperties: &WorkerProperties{
   211  					PoolID:      "rbe-pool-id",
   212  					PoolVersion: "rbe-pool-version",
   213  				},
   214  			}, fakeRequest)
   215  
   216  			So(err, ShouldBeNil)
   217  
   218  			expectedExpiry := now.Add(sessionTokenExpiry).Round(time.Second)
   219  
   220  			msg := resp.(*UpdateBotSessionResponse)
   221  			So(msg.Status, ShouldEqual, "OK")
   222  			So(msg.SessionExpiry, ShouldEqual, expectedExpiry.Unix())
   223  			So(msg.Lease, ShouldBeNil)
   224  
   225  			session := &internalspb.BotSession{}
   226  			So(srv.hmacSecret.ValidateToken(msg.SessionToken, session), ShouldBeNil)
   227  			So(session, ShouldResembleProto, &internalspb.BotSession{
   228  				RbeBotSessionId: fakeSessionID,
   229  				PollState:       fakePollState,
   230  				Expiry:          timestamppb.New(expectedExpiry),
   231  			})
   232  		})
   233  
   234  		Convey("UpdateBotSession IDLE + DEADLINE_EXCEEDED", func() {
   235  			rbe.expectUpdateBotSession(func(r *remoteworkers.UpdateBotSessionRequest) (*remoteworkers.BotSession, error) {
   236  				return nil, status.Errorf(codes.DeadlineExceeded, "boom")
   237  			})
   238  
   239  			resp, err := srv.UpdateBotSession(ctx, &UpdateBotSessionRequest{
   240  				Status: "OK",
   241  			}, fakeRequest)
   242  
   243  			So(err, ShouldBeNil)
   244  
   245  			expectedExpiry := now.Add(sessionTokenExpiry).Round(time.Second)
   246  
   247  			msg := resp.(*UpdateBotSessionResponse)
   248  			So(msg.Status, ShouldEqual, "OK")
   249  			So(msg.SessionExpiry, ShouldEqual, expectedExpiry.Unix())
   250  			So(msg.Lease, ShouldBeNil)
   251  		})
   252  
   253  		Convey("UpdateBotSession TERMINATING", func() {
   254  			rbe.expectUpdateBotSession(func(r *remoteworkers.UpdateBotSessionRequest) (*remoteworkers.BotSession, error) {
   255  				So(r.BotSession.Status, ShouldEqual, remoteworkers.BotStatus_BOT_TERMINATING)
   256  				return &remoteworkers.BotSession{
   257  					Name:   fakeSessionID,
   258  					Status: remoteworkers.BotStatus_OK,
   259  					Leases: []*remoteworkers.Lease{
   260  						// Will be ignored.
   261  						{
   262  							Id:      fakeFirstLeaseID,
   263  							Payload: payload(fakeFirstTaskID),
   264  							State:   remoteworkers.LeaseState_PENDING,
   265  						},
   266  					},
   267  				}, nil
   268  			})
   269  
   270  			resp, err := srv.UpdateBotSession(ctx, &UpdateBotSessionRequest{
   271  				Status: "BOT_TERMINATING",
   272  			}, fakeRequest)
   273  
   274  			So(err, ShouldBeNil)
   275  
   276  			msg := resp.(*UpdateBotSessionResponse)
   277  			So(msg.Status, ShouldEqual, "OK")
   278  			So(msg.Lease, ShouldBeNil)
   279  		})
   280  
   281  		Convey("UpdateBotSession TERMINATING by RBE", func() {
   282  			rbe.expectUpdateBotSession(func(r *remoteworkers.UpdateBotSessionRequest) (*remoteworkers.BotSession, error) {
   283  				So(r.BotSession.Status, ShouldEqual, remoteworkers.BotStatus_OK)
   284  				return &remoteworkers.BotSession{
   285  					Name:   fakeSessionID,
   286  					Status: remoteworkers.BotStatus_BOT_TERMINATING,
   287  					Leases: []*remoteworkers.Lease{
   288  						// Will be ignored.
   289  						{
   290  							Id:      fakeFirstLeaseID,
   291  							Payload: payload(fakeFirstTaskID),
   292  							State:   remoteworkers.LeaseState_PENDING,
   293  						},
   294  					},
   295  				}, nil
   296  			})
   297  
   298  			resp, err := srv.UpdateBotSession(ctx, &UpdateBotSessionRequest{
   299  				Status: "OK",
   300  			}, fakeRequest)
   301  
   302  			So(err, ShouldBeNil)
   303  
   304  			msg := resp.(*UpdateBotSessionResponse)
   305  			So(msg.Status, ShouldEqual, "BOT_TERMINATING")
   306  			So(msg.Lease, ShouldBeNil)
   307  		})
   308  
   309  		Convey("UpdateBotSession IDLE => PENDING", func() {
   310  			rbe.expectUpdateBotSession(func(r *remoteworkers.UpdateBotSessionRequest) (*remoteworkers.BotSession, error) {
   311  				So(r.BotSession.Status, ShouldEqual, remoteworkers.BotStatus_OK)
   312  				return &remoteworkers.BotSession{
   313  					Name:   fakeSessionID,
   314  					Status: remoteworkers.BotStatus_OK,
   315  					Leases: []*remoteworkers.Lease{
   316  						{
   317  							Id:      fakeFirstLeaseID,
   318  							Payload: payload(fakeFirstTaskID),
   319  							State:   remoteworkers.LeaseState_PENDING,
   320  						},
   321  						// Will be ignored.
   322  						{
   323  							Id:      fakeSecondLeaseID,
   324  							Payload: payload(fakeSecondTaskID),
   325  							State:   remoteworkers.LeaseState_PENDING,
   326  						},
   327  					},
   328  				}, nil
   329  			})
   330  
   331  			resp, err := srv.UpdateBotSession(ctx, &UpdateBotSessionRequest{
   332  				Status: "OK",
   333  			}, fakeRequest)
   334  
   335  			So(err, ShouldBeNil)
   336  
   337  			lease := resp.(*UpdateBotSessionResponse).Lease
   338  			So(lease.ID, ShouldEqual, fakeFirstLeaseID)
   339  			So(lease.State, ShouldEqual, "PENDING")
   340  			So(lease.Payload, ShouldResembleProto, &internalspb.TaskPayload{
   341  				TaskId: fakeFirstTaskID,
   342  			})
   343  		})
   344  
   345  		Convey("UpdateBotSession ACTIVE", func() {
   346  			rbe.expectUpdateBotSession(func(r *remoteworkers.UpdateBotSessionRequest) (*remoteworkers.BotSession, error) {
   347  				So(r.BotSession.Status, ShouldEqual, remoteworkers.BotStatus_OK)
   348  				So(r.BotSession.Leases, ShouldResembleProto, []*remoteworkers.Lease{
   349  					{
   350  						Id:    fakeFirstLeaseID,
   351  						State: remoteworkers.LeaseState_ACTIVE,
   352  					},
   353  				})
   354  				return &remoteworkers.BotSession{
   355  					Name:   fakeSessionID,
   356  					Status: remoteworkers.BotStatus_OK,
   357  					Leases: []*remoteworkers.Lease{
   358  						{
   359  							Id:    fakeFirstLeaseID,
   360  							State: remoteworkers.LeaseState_ACTIVE,
   361  						},
   362  						// Will be ignored.
   363  						{
   364  							Id:      fakeSecondLeaseID,
   365  							Payload: payload(fakeSecondTaskID),
   366  							State:   remoteworkers.LeaseState_PENDING,
   367  						},
   368  					},
   369  				}, nil
   370  			})
   371  
   372  			resp, err := srv.UpdateBotSession(ctx, &UpdateBotSessionRequest{
   373  				Status: "OK",
   374  				Lease: &Lease{
   375  					ID:    fakeFirstLeaseID,
   376  					State: "ACTIVE",
   377  				},
   378  			}, fakeRequest)
   379  
   380  			So(err, ShouldBeNil)
   381  
   382  			lease := resp.(*UpdateBotSessionResponse).Lease
   383  			So(lease.ID, ShouldEqual, fakeFirstLeaseID)
   384  			So(lease.State, ShouldEqual, "ACTIVE")
   385  			So(lease.Payload, ShouldBeNil)
   386  		})
   387  
   388  		Convey("UpdateBotSession ACTIVE => CANCELLED", func() {
   389  			rbe.expectUpdateBotSession(func(r *remoteworkers.UpdateBotSessionRequest) (*remoteworkers.BotSession, error) {
   390  				So(r.BotSession.Status, ShouldEqual, remoteworkers.BotStatus_OK)
   391  				So(r.BotSession.Leases, ShouldResembleProto, []*remoteworkers.Lease{
   392  					{
   393  						Id:    fakeFirstLeaseID,
   394  						State: remoteworkers.LeaseState_ACTIVE,
   395  					},
   396  				})
   397  				return &remoteworkers.BotSession{
   398  					Name:   fakeSessionID,
   399  					Status: remoteworkers.BotStatus_OK,
   400  					Leases: []*remoteworkers.Lease{
   401  						{
   402  							Id:    fakeFirstLeaseID,
   403  							State: remoteworkers.LeaseState_CANCELLED,
   404  						},
   405  						// Will be ignored.
   406  						{
   407  							Id:      fakeSecondLeaseID,
   408  							Payload: payload(fakeSecondTaskID),
   409  							State:   remoteworkers.LeaseState_PENDING,
   410  						},
   411  					},
   412  				}, nil
   413  			})
   414  
   415  			resp, err := srv.UpdateBotSession(ctx, &UpdateBotSessionRequest{
   416  				Status: "OK",
   417  				Lease: &Lease{
   418  					ID:    fakeFirstLeaseID,
   419  					State: "ACTIVE",
   420  				},
   421  			}, fakeRequest)
   422  
   423  			So(err, ShouldBeNil)
   424  
   425  			lease := resp.(*UpdateBotSessionResponse).Lease
   426  			So(lease.ID, ShouldEqual, fakeFirstLeaseID)
   427  			So(lease.State, ShouldEqual, "CANCELLED")
   428  			So(lease.Payload, ShouldBeNil)
   429  		})
   430  
   431  		Convey("UpdateBotSession ACTIVE => IDLE", func() {
   432  			rbe.expectUpdateBotSession(func(r *remoteworkers.UpdateBotSessionRequest) (*remoteworkers.BotSession, error) {
   433  				So(r.BotSession.Status, ShouldEqual, remoteworkers.BotStatus_OK)
   434  				So(r.BotSession.Leases, ShouldResembleProto, []*remoteworkers.Lease{
   435  					{
   436  						Id:     fakeFirstLeaseID,
   437  						State:  remoteworkers.LeaseState_COMPLETED,
   438  						Status: &statuspb.Status{},
   439  						Result: result(),
   440  					},
   441  				})
   442  				return &remoteworkers.BotSession{
   443  					Name:   fakeSessionID,
   444  					Status: remoteworkers.BotStatus_OK,
   445  				}, nil
   446  			})
   447  
   448  			resp, err := srv.UpdateBotSession(ctx, &UpdateBotSessionRequest{
   449  				Status: "OK",
   450  				Lease: &Lease{
   451  					ID:     fakeFirstLeaseID,
   452  					State:  "COMPLETED",
   453  					Result: &internalspb.TaskResult{},
   454  				},
   455  			}, fakeRequest)
   456  
   457  			So(err, ShouldBeNil)
   458  			So(resp.(*UpdateBotSessionResponse).Lease, ShouldBeNil)
   459  		})
   460  
   461  		Convey("UpdateBotSession ACTIVE => PENDING", func() {
   462  			rbe.expectUpdateBotSession(func(r *remoteworkers.UpdateBotSessionRequest) (*remoteworkers.BotSession, error) {
   463  				So(r.BotSession.Status, ShouldEqual, remoteworkers.BotStatus_OK)
   464  				So(r.BotSession.Leases, ShouldResembleProto, []*remoteworkers.Lease{
   465  					{
   466  						Id:     fakeFirstLeaseID,
   467  						State:  remoteworkers.LeaseState_COMPLETED,
   468  						Status: &statuspb.Status{},
   469  						Result: result(),
   470  					},
   471  				})
   472  				return &remoteworkers.BotSession{
   473  					Name:   fakeSessionID,
   474  					Status: remoteworkers.BotStatus_OK,
   475  					Leases: []*remoteworkers.Lease{
   476  						{
   477  							Id:      fakeSecondLeaseID,
   478  							Payload: payload(fakeSecondTaskID),
   479  							State:   remoteworkers.LeaseState_PENDING,
   480  						},
   481  					},
   482  				}, nil
   483  			})
   484  
   485  			resp, err := srv.UpdateBotSession(ctx, &UpdateBotSessionRequest{
   486  				Status: "OK",
   487  				Lease: &Lease{
   488  					ID:     fakeFirstLeaseID,
   489  					State:  "COMPLETED",
   490  					Result: &internalspb.TaskResult{},
   491  				},
   492  			}, fakeRequest)
   493  
   494  			So(err, ShouldBeNil)
   495  
   496  			lease := resp.(*UpdateBotSessionResponse).Lease
   497  			So(lease.ID, ShouldEqual, fakeSecondLeaseID)
   498  			So(lease.State, ShouldEqual, "PENDING")
   499  			So(lease.Payload, ShouldResembleProto, &internalspb.TaskPayload{
   500  				TaskId: fakeSecondTaskID,
   501  			})
   502  		})
   503  
   504  		Convey("UpdateBotSession no session ID", func() {
   505  			_, err := srv.UpdateBotSession(ctx, &UpdateBotSessionRequest{
   506  				Status: "OK",
   507  			}, &botsrv.Request{})
   508  			So(err, ShouldHaveGRPCStatus, codes.InvalidArgument)
   509  			So(err, ShouldErrLike, "missing session ID")
   510  		})
   511  
   512  		Convey("UpdateBotSession expired session token", func() {
   513  			resp, err := srv.UpdateBotSession(ctx, &UpdateBotSessionRequest{
   514  				Status: "OK",
   515  			}, &botsrv.Request{
   516  				SessionTokenExpired: true,
   517  			})
   518  			So(err, ShouldBeNil)
   519  			So(resp, ShouldResemble, &UpdateBotSessionResponse{
   520  				Status: "BOT_TERMINATING",
   521  			})
   522  		})
   523  
   524  		Convey("UpdateBotSession propagates RBE error", func() {
   525  			rbe.expectUpdateBotSession(func(r *remoteworkers.UpdateBotSessionRequest) (*remoteworkers.BotSession, error) {
   526  				return nil, status.Errorf(codes.FailedPrecondition, "boom")
   527  			})
   528  			_, err := srv.UpdateBotSession(ctx, &UpdateBotSessionRequest{
   529  				Status: "OK",
   530  			}, fakeRequest)
   531  			So(err, ShouldHaveGRPCStatus, codes.FailedPrecondition)
   532  			So(err, ShouldErrLike, "boom")
   533  		})
   534  
   535  		Convey("UpdateBotSession bad session status", func() {
   536  			_, err := srv.UpdateBotSession(ctx, &UpdateBotSessionRequest{
   537  				Status: "huh",
   538  			}, fakeRequest)
   539  			So(err, ShouldHaveGRPCStatus, codes.InvalidArgument)
   540  			So(err, ShouldErrLike, "unrecognized session status")
   541  		})
   542  
   543  		Convey("UpdateBotSession missing session status", func() {
   544  			_, err := srv.UpdateBotSession(ctx, &UpdateBotSessionRequest{}, fakeRequest)
   545  			So(err, ShouldHaveGRPCStatus, codes.InvalidArgument)
   546  			So(err, ShouldErrLike, "missing session status")
   547  		})
   548  
   549  		Convey("UpdateBotSession bad lease state", func() {
   550  			_, err := srv.UpdateBotSession(ctx, &UpdateBotSessionRequest{
   551  				Status: "OK",
   552  				Lease:  &Lease{State: "huh"},
   553  			}, fakeRequest)
   554  			So(err, ShouldHaveGRPCStatus, codes.InvalidArgument)
   555  			So(err, ShouldErrLike, "unrecognized lease state")
   556  		})
   557  
   558  		Convey("UpdateBotSession missing lease state", func() {
   559  			_, err := srv.UpdateBotSession(ctx, &UpdateBotSessionRequest{
   560  				Status: "OK",
   561  				Lease:  &Lease{},
   562  			}, fakeRequest)
   563  			So(err, ShouldHaveGRPCStatus, codes.InvalidArgument)
   564  			So(err, ShouldErrLike, "missing lease state")
   565  		})
   566  
   567  		Convey("UpdateBotSession unexpected lease state", func() {
   568  			_, err := srv.UpdateBotSession(ctx, &UpdateBotSessionRequest{
   569  				Status: "OK",
   570  				Lease:  &Lease{State: "CANCELLED"},
   571  			}, fakeRequest)
   572  			So(err, ShouldHaveGRPCStatus, codes.InvalidArgument)
   573  			So(err, ShouldErrLike, "unexpected lease state")
   574  		})
   575  
   576  		Convey("UpdateBotSession ACTIVE lease disappears", func() {
   577  			rbe.expectUpdateBotSession(func(r *remoteworkers.UpdateBotSessionRequest) (*remoteworkers.BotSession, error) {
   578  				return &remoteworkers.BotSession{
   579  					Name:   fakeSessionID,
   580  					Status: remoteworkers.BotStatus_OK,
   581  					Leases: []*remoteworkers.Lease{
   582  						// Will be ignored.
   583  						{
   584  							Id:      fakeSecondLeaseID,
   585  							Payload: payload(fakeSecondTaskID),
   586  							State:   remoteworkers.LeaseState_PENDING,
   587  						},
   588  					},
   589  				}, nil
   590  			})
   591  
   592  			resp, err := srv.UpdateBotSession(ctx, &UpdateBotSessionRequest{
   593  				Status: "OK",
   594  				Lease: &Lease{
   595  					ID:    fakeFirstLeaseID,
   596  					State: "ACTIVE",
   597  				},
   598  			}, fakeRequest)
   599  
   600  			So(err, ShouldBeNil)
   601  			So(resp.(*UpdateBotSessionResponse).Lease, ShouldBeNil)
   602  		})
   603  
   604  		Convey("UpdateBotSession unexpected ACTIVE lease transition", func() {
   605  			rbe.expectUpdateBotSession(func(r *remoteworkers.UpdateBotSessionRequest) (*remoteworkers.BotSession, error) {
   606  				return &remoteworkers.BotSession{
   607  					Name:   fakeSessionID,
   608  					Status: remoteworkers.BotStatus_OK,
   609  					Leases: []*remoteworkers.Lease{
   610  						{
   611  							Id:    fakeFirstLeaseID,
   612  							State: remoteworkers.LeaseState_PENDING,
   613  						},
   614  					},
   615  				}, nil
   616  			})
   617  
   618  			_, err := srv.UpdateBotSession(ctx, &UpdateBotSessionRequest{
   619  				Status: "OK",
   620  				Lease: &Lease{
   621  					ID:    fakeFirstLeaseID,
   622  					State: "ACTIVE",
   623  				},
   624  			}, fakeRequest)
   625  
   626  			So(err, ShouldHaveGRPCStatus, codes.Internal)
   627  			So(err, ShouldErrLike, "unexpected ACTIVE lease state transition to PENDING")
   628  		})
   629  
   630  		Convey("UpdateBotSession unrecognized payload type", func() {
   631  			rbe.expectUpdateBotSession(func(r *remoteworkers.UpdateBotSessionRequest) (*remoteworkers.BotSession, error) {
   632  				wrong, _ := anypb.New(&timestamppb.Timestamp{})
   633  				return &remoteworkers.BotSession{
   634  					Name:   fakeSessionID,
   635  					Status: remoteworkers.BotStatus_OK,
   636  					Leases: []*remoteworkers.Lease{
   637  						{
   638  							Id:      fakeFirstLeaseID,
   639  							Payload: wrong,
   640  							State:   remoteworkers.LeaseState_PENDING,
   641  						},
   642  					},
   643  				}, nil
   644  			})
   645  
   646  			_, err := srv.UpdateBotSession(ctx, &UpdateBotSessionRequest{
   647  				Status: "OK",
   648  			}, fakeRequest)
   649  
   650  			So(err, ShouldHaveGRPCStatus, codes.Internal)
   651  			So(err, ShouldErrLike, "failed to unmarshal pending lease payload")
   652  		})
   653  	})
   654  }
   655  
   656  ////////////////////////////////////////////////////////////////////////////////
   657  
   658  type (
   659  	expectedCreate func(*remoteworkers.CreateBotSessionRequest) (*remoteworkers.BotSession, error)
   660  	expectedUpdate func(*remoteworkers.UpdateBotSessionRequest) (*remoteworkers.BotSession, error)
   661  )
   662  
   663  type mockedBotsClient struct {
   664  	expected []any // either expectedCreate or expectedUpdate
   665  }
   666  
   667  func (m *mockedBotsClient) expectCreateBotSession(cb expectedCreate) {
   668  	m.expected = append(m.expected, cb)
   669  }
   670  
   671  func (m *mockedBotsClient) expectUpdateBotSession(cb expectedUpdate) {
   672  	m.expected = append(m.expected, cb)
   673  }
   674  
   675  func (m *mockedBotsClient) popExpected() (cb any) {
   676  	So(m.expected, ShouldNotBeEmpty)
   677  	cb, m.expected = m.expected[0], m.expected[1:]
   678  	return cb
   679  }
   680  
   681  func (m *mockedBotsClient) CreateBotSession(ctx context.Context, in *remoteworkers.CreateBotSessionRequest, opts ...grpc.CallOption) (*remoteworkers.BotSession, error) {
   682  	cb := m.popExpected()
   683  	So(cb, ShouldHaveSameTypeAs, expectedCreate(nil))
   684  	return cb.(expectedCreate)(in)
   685  }
   686  
   687  func (m *mockedBotsClient) UpdateBotSession(ctx context.Context, in *remoteworkers.UpdateBotSessionRequest, opts ...grpc.CallOption) (*remoteworkers.BotSession, error) {
   688  	cb := m.popExpected()
   689  	So(cb, ShouldHaveSameTypeAs, expectedUpdate(nil))
   690  	return cb.(expectedUpdate)(in)
   691  }