go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/services/recorder/create_invocation_test.go (about) 1 // Copyright 2019 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 recorder 16 17 import ( 18 "context" 19 "strings" 20 "testing" 21 "time" 22 23 "cloud.google.com/go/spanner" 24 "github.com/golang/protobuf/proto" 25 "google.golang.org/grpc" 26 "google.golang.org/grpc/codes" 27 "google.golang.org/grpc/metadata" 28 "google.golang.org/protobuf/reflect/protoreflect" 29 "google.golang.org/protobuf/types/known/structpb" 30 31 "go.chromium.org/luci/common/clock" 32 "go.chromium.org/luci/common/clock/testclock" 33 "go.chromium.org/luci/common/testing/prpctest" 34 "go.chromium.org/luci/grpc/appstatus" 35 "go.chromium.org/luci/server/auth" 36 "go.chromium.org/luci/server/auth/authtest" 37 "go.chromium.org/luci/server/span" 38 "go.chromium.org/luci/server/tq" 39 40 "go.chromium.org/luci/resultdb/internal/invocations" 41 "go.chromium.org/luci/resultdb/internal/tasks/taskspb" 42 "go.chromium.org/luci/resultdb/internal/testutil" 43 "go.chromium.org/luci/resultdb/internal/testutil/insert" 44 "go.chromium.org/luci/resultdb/pbutil" 45 pb "go.chromium.org/luci/resultdb/proto/v1" 46 47 . "github.com/smartystreets/goconvey/convey" 48 . "go.chromium.org/luci/common/testing/assertions" 49 ) 50 51 func TestValidateInvocationDeadline(t *testing.T) { 52 Convey(`ValidateInvocationDeadline`, t, func() { 53 now := testclock.TestRecentTimeUTC 54 55 Convey(`deadline in the past`, func() { 56 deadline := pbutil.MustTimestampProto(now.Add(-time.Hour)) 57 err := validateInvocationDeadline(deadline, now) 58 So(err, ShouldErrLike, `must be at least 10 seconds in the future`) 59 }) 60 61 Convey(`deadline 5s in the future`, func() { 62 deadline := pbutil.MustTimestampProto(now.Add(5 * time.Second)) 63 err := validateInvocationDeadline(deadline, now) 64 So(err, ShouldErrLike, `must be at least 10 seconds in the future`) 65 }) 66 67 Convey(`deadline in the future`, func() { 68 deadline := pbutil.MustTimestampProto(now.Add(1e3 * time.Hour)) 69 err := validateInvocationDeadline(deadline, now) 70 So(err, ShouldErrLike, `must be before 120h in the future`) 71 }) 72 }) 73 } 74 75 func TestVerifyCreateInvocationPermissions(t *testing.T) { 76 t.Parallel() 77 Convey(`TestVerifyCreateInvocationPermissions`, t, func() { 78 ctx := auth.WithState(context.Background(), &authtest.FakeState{ 79 Identity: "user:someone@example.com", 80 IdentityPermissions: []authtest.RealmPermission{ 81 {Realm: "chromium:ci", Permission: permCreateInvocation}, 82 }, 83 }) 84 Convey(`reserved prefix`, func() { 85 err := verifyCreateInvocationPermissions(ctx, &pb.CreateInvocationRequest{ 86 InvocationId: "build:8765432100", 87 Invocation: &pb.Invocation{ 88 Realm: "chromium:ci", 89 }, 90 }) 91 So(err, ShouldErrLike, `only invocations created by trusted systems may have id not starting with "u-"`) 92 }) 93 94 Convey(`reserved prefix, allowed`, func() { 95 ctx = auth.WithState(context.Background(), &authtest.FakeState{ 96 Identity: "user:someone@example.com", 97 IdentityPermissions: []authtest.RealmPermission{ 98 {Realm: "chromium:ci", Permission: permCreateInvocation}, 99 {Realm: "chromium:ci", Permission: permCreateWithReservedID}, 100 }, 101 }) 102 err := verifyCreateInvocationPermissions(ctx, &pb.CreateInvocationRequest{ 103 InvocationId: "build:8765432100", 104 Invocation: &pb.Invocation{ 105 Realm: "chromium:ci", 106 }, 107 }) 108 So(err, ShouldBeNil) 109 }) 110 Convey(`producer_resource disallowed`, func() { 111 ctx = auth.WithState(context.Background(), &authtest.FakeState{ 112 Identity: "user:someone@example.com", 113 IdentityPermissions: []authtest.RealmPermission{ 114 {Realm: "chromium:ci", Permission: permCreateInvocation}, 115 }, 116 }) 117 err := verifyCreateInvocationPermissions(ctx, &pb.CreateInvocationRequest{ 118 InvocationId: "u-0", 119 Invocation: &pb.Invocation{ 120 Realm: "chromium:ci", 121 ProducerResource: "//builds.example.com/builds/1", 122 }, 123 }) 124 So(err, ShouldErrLike, `only invocations created by trusted system may have a populated producer_resource field`) 125 }) 126 127 Convey(`producer_resource allowed`, func() { 128 ctx = auth.WithState(context.Background(), &authtest.FakeState{ 129 Identity: "user:someone@example.com", 130 IdentityPermissions: []authtest.RealmPermission{ 131 {Realm: "chromium:ci", Permission: permCreateInvocation}, 132 {Realm: "chromium:ci", Permission: permSetProducerResource}, 133 }, 134 }) 135 err := verifyCreateInvocationPermissions(ctx, &pb.CreateInvocationRequest{ 136 InvocationId: "u-0", 137 Invocation: &pb.Invocation{ 138 Realm: "chromium:ci", 139 ProducerResource: "//builds.example.com/builds/1", 140 }, 141 }) 142 So(err, ShouldBeNil) 143 }) 144 Convey(`bigquery_exports allowed`, func() { 145 ctx = auth.WithState(context.Background(), &authtest.FakeState{ 146 Identity: "user:someone@example.com", 147 IdentityPermissions: []authtest.RealmPermission{ 148 {Realm: "chromium:ci", Permission: permCreateInvocation}, 149 {Realm: "chromium:ci", Permission: permExportToBigQuery}, 150 }, 151 }) 152 err := verifyCreateInvocationPermissions(ctx, &pb.CreateInvocationRequest{ 153 InvocationId: "u-abc", 154 Invocation: &pb.Invocation{ 155 Realm: "chromium:ci", 156 BigqueryExports: []*pb.BigQueryExport{ 157 { 158 Project: "project", 159 Dataset: "dataset", 160 Table: "table", 161 ResultType: &pb.BigQueryExport_TestResults_{ 162 TestResults: &pb.BigQueryExport_TestResults{}, 163 }, 164 }, 165 }, 166 }, 167 }) 168 So(err, ShouldBeNil) 169 }) 170 Convey(`bigquery_exports disallowed`, func() { 171 ctx = auth.WithState(context.Background(), &authtest.FakeState{ 172 Identity: "user:someone@example.com", 173 IdentityPermissions: []authtest.RealmPermission{ 174 {Realm: "chromium:ci", Permission: permCreateInvocation}, 175 }, 176 }) 177 err := verifyCreateInvocationPermissions(ctx, &pb.CreateInvocationRequest{ 178 InvocationId: "u-abc", 179 Invocation: &pb.Invocation{ 180 Realm: "chromium:ci", 181 BigqueryExports: []*pb.BigQueryExport{ 182 { 183 Project: "project", 184 Dataset: "dataset", 185 Table: "table", 186 ResultType: &pb.BigQueryExport_TestResults_{ 187 TestResults: &pb.BigQueryExport_TestResults{}, 188 }, 189 }, 190 }, 191 }, 192 }) 193 So(err, ShouldErrLike, `does not have permission to set bigquery exports`) 194 }) 195 Convey(`baseline allowed`, func() { 196 ctx = auth.WithState(context.Background(), &authtest.FakeState{ 197 Identity: "user:someone@example.com", 198 IdentityPermissions: []authtest.RealmPermission{ 199 {Realm: "chromium:try", Permission: permCreateInvocation}, 200 {Realm: "chromium:try", Permission: permPutBaseline}, 201 }, 202 }) 203 err := verifyCreateInvocationPermissions(ctx, &pb.CreateInvocationRequest{ 204 InvocationId: "u-abc", 205 Invocation: &pb.Invocation{ 206 Realm: "chromium:try", 207 BaselineId: "try:linux-rel", 208 }, 209 }) 210 So(err, ShouldBeNil) 211 }) 212 Convey(`baseline disallowed`, func() { 213 ctx = auth.WithState(context.Background(), &authtest.FakeState{ 214 Identity: "user:someone@example.com", 215 IdentityPermissions: []authtest.RealmPermission{ 216 {Realm: "chromium:try", Permission: permCreateInvocation}, 217 }, 218 }) 219 err := verifyCreateInvocationPermissions(ctx, &pb.CreateInvocationRequest{ 220 InvocationId: "u-abc", 221 Invocation: &pb.Invocation{ 222 Realm: "chromium:try", 223 BaselineId: "try:linux-rel", 224 }, 225 }) 226 So(err, ShouldErrLike, `does not have permission to set baseline ids`) 227 }) 228 Convey(`creation disallowed`, func() { 229 ctx = auth.WithState(context.Background(), &authtest.FakeState{ 230 Identity: "user:someone@example.com", 231 IdentityPermissions: []authtest.RealmPermission{}, 232 }) 233 err := verifyCreateInvocationPermissions(ctx, &pb.CreateInvocationRequest{ 234 InvocationId: "build:8765432100", 235 Invocation: &pb.Invocation{ 236 Realm: "chromium:ci", 237 }, 238 }) 239 So(err, ShouldErrLike, `does not have permission to create invocations`) 240 }) 241 Convey(`invalid realm`, func() { 242 ctx = auth.WithState(context.Background(), &authtest.FakeState{ 243 Identity: "user:someone@example.com", 244 IdentityPermissions: []authtest.RealmPermission{}, 245 }) 246 err := verifyCreateInvocationPermissions(ctx, &pb.CreateInvocationRequest{ 247 InvocationId: "build:8765432100", 248 Invocation: &pb.Invocation{ 249 Realm: "invalid:", 250 }, 251 }) 252 So(err, ShouldHaveAppStatus, codes.InvalidArgument, `invocation: realm: bad global realm name`) 253 }) 254 }) 255 256 } 257 func TestValidateCreateInvocationRequest(t *testing.T) { 258 t.Parallel() 259 now := testclock.TestRecentTimeUTC 260 Convey(`TestValidateCreateInvocationRequest`, t, func() { 261 addedInvs := make(invocations.IDSet) 262 deadline := pbutil.MustTimestampProto(now.Add(time.Hour)) 263 request := &pb.CreateInvocationRequest{ 264 InvocationId: "u-abc", 265 Invocation: &pb.Invocation{ 266 Deadline: deadline, 267 Tags: pbutil.StringPairs("a", "b", "a", "c", "d", "e"), 268 Realm: "chromium:ci", 269 IncludedInvocations: []string{"invocations/u-abc-2"}, 270 State: pb.Invocation_FINALIZING, 271 }, 272 } 273 274 Convey(`valid`, func() { 275 err := validateCreateInvocationRequest(request, now, addedInvs) 276 So(err, ShouldBeNil) 277 }) 278 279 Convey(`empty`, func() { 280 err := validateCreateInvocationRequest(&pb.CreateInvocationRequest{}, now, addedInvs) 281 So(err, ShouldErrLike, `invocation_id: unspecified`) 282 }) 283 284 Convey(`invalid id`, func() { 285 request.InvocationId = "1" 286 err := validateCreateInvocationRequest(request, now, addedInvs) 287 So(err, ShouldErrLike, `invocation_id: does not match`) 288 }) 289 290 Convey(`invalid request id`, func() { 291 request.RequestId = "😃" 292 err := validateCreateInvocationRequest(request, now, addedInvs) 293 So(err, ShouldErrLike, "request_id: does not match") 294 }) 295 296 Convey(`invalid tags`, func() { 297 request.Invocation.Tags = pbutil.StringPairs("1", "a") 298 err := validateCreateInvocationRequest(request, now, addedInvs) 299 So(err, ShouldErrLike, `invocation: tags: "1":"a": key: does not match`) 300 }) 301 302 Convey(`invalid deadline`, func() { 303 request.Invocation.Deadline = pbutil.MustTimestampProto(now.Add(-time.Hour)) 304 err := validateCreateInvocationRequest(request, now, addedInvs) 305 So(err, ShouldErrLike, `invocation: deadline: must be at least 10 seconds in the future`) 306 }) 307 308 Convey(`invalid realm`, func() { 309 request.Invocation.Realm = "B@d/f::rm@t" 310 err := validateCreateInvocationRequest(request, now, addedInvs) 311 So(err, ShouldErrLike, `invocation: realm: bad global realm name`) 312 }) 313 314 Convey(`invalid state`, func() { 315 request.Invocation.State = pb.Invocation_FINALIZED 316 err := validateCreateInvocationRequest(request, now, addedInvs) 317 So(err, ShouldErrLike, `invocation: state: cannot be created in the state FINALIZED`) 318 }) 319 320 Convey(`invalid included invocation`, func() { 321 request.Invocation.IncludedInvocations = []string{"not an invocation name"} 322 err := validateCreateInvocationRequest(request, now, addedInvs) 323 So(err, ShouldErrLike, `included_invocations[0]: invalid included invocation name`) 324 }) 325 326 Convey(`invalid bigqueryExports`, func() { 327 request.Invocation.BigqueryExports = []*pb.BigQueryExport{ 328 { 329 Project: "project", 330 }, 331 } 332 err := validateCreateInvocationRequest(request, now, addedInvs) 333 So(err, ShouldErrLike, `bigquery_export[0]: dataset: unspecified`) 334 }) 335 336 Convey(`invalid source spec`, func() { 337 request.Invocation.SourceSpec = &pb.SourceSpec{ 338 Sources: &pb.Sources{ 339 GitilesCommit: &pb.GitilesCommit{}, 340 }, 341 } 342 err := validateCreateInvocationRequest(request, now, addedInvs) 343 So(err, ShouldErrLike, `source_spec: sources: gitiles_commit: host: unspecified`) 344 }) 345 346 Convey(`invalid baseline`, func() { 347 request.Invocation.BaselineId = "try/linux-rel" 348 err := validateCreateInvocationRequest(request, now, addedInvs) 349 So(err, ShouldErrLike, `invocation: baseline_id: does not match`) 350 }) 351 352 Convey(`invalid properties`, func() { 353 request.Invocation.Properties = &structpb.Struct{ 354 Fields: map[string]*structpb.Value{ 355 "a": structpb.NewStringValue(strings.Repeat("a", pbutil.MaxSizeProperties)), 356 }, 357 } 358 err := validateCreateInvocationRequest(request, now, addedInvs) 359 So(err, ShouldErrLike, `properties: exceeds the maximum size of`, `bytes`) 360 }) 361 }) 362 } 363 364 func TestCreateInvocation(t *testing.T) { 365 Convey(`TestCreateInvocation`, t, func() { 366 ctx := testutil.SpannerTestContext(t) 367 ctx, sched := tq.TestingContext(ctx, nil) 368 ctx = auth.WithState(ctx, &authtest.FakeState{ 369 Identity: "user:someone@example.com", 370 IdentityPermissions: []authtest.RealmPermission{ 371 {Realm: "testproject:testrealm", Permission: permCreateInvocation}, 372 {Realm: "testproject:testrealm", Permission: permCreateWithReservedID}, 373 {Realm: "testproject:testrealm", Permission: permExportToBigQuery}, 374 {Realm: "testproject:testrealm", Permission: permSetProducerResource}, 375 {Realm: "testproject:testrealm", Permission: permIncludeInvocation}, 376 {Realm: "testproject:createonly", Permission: permCreateInvocation}, 377 {Realm: "testproject:testrealm", Permission: permPutBaseline}, 378 }, 379 }) 380 381 start := clock.Now(ctx).UTC() 382 383 // Setup a full HTTP server in order to retrieve response headers. 384 server := &prpctest.Server{} 385 server.UnaryServerInterceptor = func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) { 386 res, err := handler(ctx, req) 387 err = appstatus.GRPCifyAndLog(ctx, err) 388 return res, err 389 } 390 pb.RegisterRecorderServer(server, newTestRecorderServer()) 391 server.Start(ctx) 392 defer server.Close() 393 client, err := server.NewClient() 394 So(err, ShouldBeNil) 395 recorder := pb.NewRecorderPRPCClient(client) 396 397 Convey(`empty request`, func() { 398 _, err := recorder.CreateInvocation(ctx, &pb.CreateInvocationRequest{}) 399 So(err, ShouldHaveGRPCStatus, codes.InvalidArgument, `invocation: unspecified`) 400 }) 401 Convey(`invalid realm`, func() { 402 req := &pb.CreateInvocationRequest{ 403 InvocationId: "u-inv", 404 Invocation: &pb.Invocation{ 405 Realm: "testproject:", 406 }, 407 RequestId: "request id", 408 } 409 _, err := recorder.CreateInvocation(ctx, req) 410 So(err, ShouldHaveGRPCStatus, codes.InvalidArgument, `invocation: realm`) 411 }) 412 Convey(`missing invocation id`, func() { 413 _, err := recorder.CreateInvocation(ctx, &pb.CreateInvocationRequest{ 414 Invocation: &pb.Invocation{ 415 Realm: "testproject:testrealm", 416 }, 417 }) 418 So(err, ShouldHaveGRPCStatus, codes.InvalidArgument, `invocation_id: unspecified`) 419 }) 420 421 req := &pb.CreateInvocationRequest{ 422 InvocationId: "u-inv", 423 Invocation: &pb.Invocation{ 424 Realm: "testproject:testrealm", 425 }, 426 } 427 428 Convey(`already exists`, func() { 429 _, err := span.Apply(ctx, []*spanner.Mutation{ 430 insert.Invocation("u-inv", 1, nil), 431 }) 432 So(err, ShouldBeNil) 433 434 _, err = recorder.CreateInvocation(ctx, req) 435 So(err, ShouldHaveGRPCStatus, codes.AlreadyExists) 436 }) 437 438 Convey(`unsorted tags`, func() { 439 req.Invocation.Tags = pbutil.StringPairs("b", "2", "a", "1") 440 inv, err := recorder.CreateInvocation(ctx, req) 441 So(err, ShouldBeNil) 442 So(inv.Tags, ShouldResemble, pbutil.StringPairs("a", "1", "b", "2")) 443 }) 444 445 Convey(`no invocation in request`, func() { 446 _, err := recorder.CreateInvocation(ctx, &pb.CreateInvocationRequest{InvocationId: "u-inv"}) 447 So(err, ShouldErrLike, "invocation: unspecified") 448 }) 449 450 Convey(`idempotent`, func() { 451 req := &pb.CreateInvocationRequest{ 452 InvocationId: "u-inv", 453 Invocation: &pb.Invocation{ 454 Realm: "testproject:testrealm", 455 }, 456 RequestId: "request id", 457 } 458 res, err := recorder.CreateInvocation(ctx, req) 459 So(err, ShouldBeNil) 460 461 res2, err := recorder.CreateInvocation(ctx, req) 462 So(err, ShouldBeNil) 463 So(res2, ShouldResembleProto, res) 464 }) 465 Convey(`included invocation`, func() { 466 req = &pb.CreateInvocationRequest{ 467 InvocationId: "u-inv", 468 Invocation: &pb.Invocation{ 469 Realm: "testproject:testrealm", 470 IncludedInvocations: []string{"invocations/u-inv-child"}, 471 }, 472 } 473 Convey(`non-existing invocation`, func() { 474 _, err := recorder.CreateInvocation(ctx, req) 475 So(err, ShouldErrLike, "invocations/u-inv-child not found") 476 }) 477 Convey(`non-permitted invocation`, func() { 478 incReq := &pb.CreateInvocationRequest{ 479 InvocationId: "u-inv-child", 480 Invocation: &pb.Invocation{ 481 Realm: "testproject:createonly", 482 }, 483 } 484 _, err := recorder.CreateInvocation(ctx, incReq) 485 So(err, ShouldBeNil) 486 487 _, err = recorder.CreateInvocation(ctx, req) 488 So(err, ShouldErrLike, "caller does not have permission resultdb.invocations.include") 489 }) 490 Convey(`valid`, func() { 491 _, err := recorder.CreateInvocation(ctx, &pb.CreateInvocationRequest{ 492 InvocationId: "u-inv-child", 493 Invocation: &pb.Invocation{ 494 Realm: "testproject:testrealm", 495 }, 496 }) 497 So(err, ShouldBeNil) 498 499 _, err = recorder.CreateInvocation(ctx, req) 500 So(err, ShouldBeNil) 501 502 incIDs, err := invocations.ReadIncluded(span.Single(ctx), invocations.ID("u-inv")) 503 So(err, ShouldBeNil) 504 So(incIDs.Has(invocations.ID("u-inv-child")), ShouldBeTrue) 505 }) 506 }) 507 508 Convey(`end to end`, func() { 509 deadline := pbutil.MustTimestampProto(start.Add(time.Hour)) 510 headers := &metadata.MD{} 511 512 // Included invocation 513 req := &pb.CreateInvocationRequest{ 514 InvocationId: "u-inv-child", 515 Invocation: &pb.Invocation{ 516 Realm: "testproject:testrealm", 517 }, 518 } 519 _, err := recorder.CreateInvocation(ctx, req, grpc.Header(headers)) 520 So(err, ShouldBeNil) 521 522 // Including invocation. 523 bqExport := &pb.BigQueryExport{ 524 Project: "project", 525 Dataset: "dataset", 526 Table: "table", 527 ResultType: &pb.BigQueryExport_TestResults_{ 528 TestResults: &pb.BigQueryExport_TestResults{}, 529 }, 530 } 531 532 req = &pb.CreateInvocationRequest{ 533 InvocationId: "u-inv", 534 Invocation: &pb.Invocation{ 535 Deadline: deadline, 536 Tags: pbutil.StringPairs("a", "1", "b", "2"), 537 BigqueryExports: []*pb.BigQueryExport{ 538 bqExport, 539 }, 540 ProducerResource: "//builds.example.com/builds/1", 541 Realm: "testproject:testrealm", 542 IncludedInvocations: []string{"invocations/u-inv-child"}, 543 State: pb.Invocation_FINALIZING, 544 Properties: testutil.TestProperties(), 545 SourceSpec: &pb.SourceSpec{ 546 Sources: testutil.TestSources(), 547 }, 548 BaselineId: "testrealm:test-builder", 549 }, 550 } 551 inv, err := recorder.CreateInvocation(ctx, req, grpc.Header(headers)) 552 So(err, ShouldBeNil) 553 So(sched.Tasks().Payloads(), ShouldResembleProto, []protoreflect.ProtoMessage{ 554 &taskspb.TryFinalizeInvocation{InvocationId: "u-inv"}, 555 }) 556 557 expected := proto.Clone(req.Invocation).(*pb.Invocation) 558 proto.Merge(expected, &pb.Invocation{ 559 Name: "invocations/u-inv", 560 CreatedBy: "user:someone@example.com", 561 562 // we use Spanner commit time, so skip the check 563 CreateTime: inv.CreateTime, 564 }) 565 So(inv, ShouldResembleProto, expected) 566 567 So(headers.Get(pb.UpdateTokenMetadataKey), ShouldHaveLength, 1) 568 569 ctx, cancel := span.ReadOnlyTransaction(ctx) 570 defer cancel() 571 572 inv, err = invocations.Read(ctx, "u-inv") 573 So(err, ShouldBeNil) 574 So(inv, ShouldResembleProto, expected) 575 576 // Check fields not present in the proto. 577 var invExpirationTime, expectedResultsExpirationTime time.Time 578 err = invocations.ReadColumns(ctx, "u-inv", map[string]any{ 579 "InvocationExpirationTime": &invExpirationTime, 580 "ExpectedTestResultsExpirationTime": &expectedResultsExpirationTime, 581 }) 582 So(err, ShouldBeNil) 583 So(expectedResultsExpirationTime, ShouldHappenWithin, time.Second, start.Add(expectedResultExpiration)) 584 So(invExpirationTime, ShouldHappenWithin, time.Second, start.Add(invocationExpirationDuration)) 585 }) 586 }) 587 }