go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/rpc/test_history_test.go (about) 1 // Copyright 2022 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 rpc 16 17 import ( 18 "context" 19 "testing" 20 "time" 21 22 "google.golang.org/grpc/codes" 23 "google.golang.org/protobuf/types/known/timestamppb" 24 25 "go.chromium.org/luci/resultdb/rdbperms" 26 "go.chromium.org/luci/server/auth" 27 "go.chromium.org/luci/server/auth/authtest" 28 "go.chromium.org/luci/server/span" 29 30 "go.chromium.org/luci/analysis/internal/testresults" 31 "go.chromium.org/luci/analysis/internal/testutil" 32 "go.chromium.org/luci/analysis/pbutil" 33 pb "go.chromium.org/luci/analysis/proto/v1" 34 35 . "github.com/smartystreets/goconvey/convey" 36 . "go.chromium.org/luci/common/testing/assertions" 37 ) 38 39 func TestTestHistoryServer(t *testing.T) { 40 Convey("TestHistoryServer", t, func() { 41 ctx := testutil.IntegrationTestContext(t) 42 43 ctx = auth.WithState(ctx, &authtest.FakeState{ 44 Identity: "user:someone@example.com", 45 IdentityPermissions: []authtest.RealmPermission{ 46 { 47 Realm: "project:realm", 48 Permission: rdbperms.PermListTestResults, 49 }, 50 { 51 Realm: "project:realm", 52 Permission: rdbperms.PermListTestExonerations, 53 }, 54 { 55 Realm: "project:other-realm", 56 Permission: rdbperms.PermListTestResults, 57 }, 58 { 59 Realm: "project:other-realm", 60 Permission: rdbperms.PermListTestExonerations, 61 }, 62 }, 63 }) 64 65 referenceTime := time.Date(2025, time.February, 12, 0, 0, 0, 0, time.UTC) 66 day := 24 * time.Hour 67 68 var1 := pbutil.Variant("key1", "val1", "key2", "val1") 69 var2 := pbutil.Variant("key1", "val2", "key2", "val1") 70 var3 := pbutil.Variant("key1", "val2", "key2", "val2") 71 var4 := pbutil.Variant("key1", "val1", "key2", "val2") 72 var5 := pbutil.Variant("key1", "val3", "key2", "val2") 73 74 _, err := span.ReadWriteTransaction(ctx, func(ctx context.Context) error { 75 insertTR := func(subRealm string, testID string) { 76 span.BufferWrite(ctx, (&testresults.TestRealm{ 77 Project: "project", 78 TestID: testID, 79 SubRealm: subRealm, 80 }).SaveUnverified()) 81 } 82 insertTR("realm", "test_id") 83 insertTR("realm", "test_id1") 84 insertTR("realm", "test_id2") 85 insertTR("other-realm", "test_id3") 86 insertTR("forbidden-realm", "test_id4") 87 88 insertTVR := func(subRealm string, variant *pb.Variant) { 89 span.BufferWrite(ctx, (&testresults.TestVariantRealm{ 90 Project: "project", 91 TestID: "test_id", 92 SubRealm: subRealm, 93 Variant: variant, 94 VariantHash: pbutil.VariantHash(variant), 95 }).SaveUnverified()) 96 } 97 98 insertTVR("realm", var1) 99 insertTVR("realm", var2) 100 insertTVR("realm", var3) 101 insertTVR("other-realm", var4) 102 insertTVR("forbidden-realm", var5) 103 104 insertTV := func(partitionTime time.Time, variant *pb.Variant, invId string, hasUnsubmittedChanges bool, isFromBisection bool, subRealm string) { 105 baseTestResult := testresults.NewTestResult(). 106 WithProject("project"). 107 WithTestID("test_id"). 108 WithVariantHash(pbutil.VariantHash(variant)). 109 WithPartitionTime(partitionTime). 110 WithIngestedInvocationID(invId). 111 WithSubRealm(subRealm). 112 WithStatus(pb.TestResultStatus_PASS). 113 WithIsFromBisection(isFromBisection). 114 WithoutRunDuration() 115 if hasUnsubmittedChanges { 116 baseTestResult = baseTestResult.WithSources(testresults.Sources{ 117 Changelists: []testresults.Changelist{ 118 { 119 Host: "anothergerrit.gerrit.instance", 120 Change: 5471, 121 Patchset: 6, 122 OwnerKind: pb.ChangelistOwnerKind_HUMAN, 123 }, 124 { 125 Host: "mygerrit-review.googlesource.com", 126 Change: 4321, 127 Patchset: 5, 128 OwnerKind: pb.ChangelistOwnerKind_AUTOMATION, 129 }, 130 }, 131 }) 132 } else { 133 baseTestResult = baseTestResult.WithSources(testresults.Sources{}) 134 } 135 136 trs := testresults.NewTestVerdict(). 137 WithBaseTestResult(baseTestResult.Build()). 138 WithStatus(pb.TestVerdictStatus_EXPECTED). 139 WithPassedAvgDuration(nil). 140 Build() 141 for _, tr := range trs { 142 span.BufferWrite(ctx, tr.SaveUnverified()) 143 } 144 } 145 146 insertTV(referenceTime.Add(-1*day), var1, "inv1", false, false, "realm") 147 insertTV(referenceTime.Add(-1*day), var1, "inv2", false, false, "realm") 148 insertTV(referenceTime.Add(-1*day), var2, "inv1", false, false, "realm") 149 insertTV(referenceTime.Add(-1*day), var2, "inv2", false, true, "realm") 150 151 insertTV(referenceTime.Add(-2*day), var1, "inv1", false, false, "realm") 152 insertTV(referenceTime.Add(-2*day), var1, "inv2", true, false, "realm") 153 insertTV(referenceTime.Add(-2*day), var2, "inv1", true, false, "realm") 154 155 insertTV(referenceTime.Add(-3*day), var3, "inv1", true, false, "realm") 156 157 insertTV(referenceTime.Add(-4*day), var4, "inv2", false, false, "other-realm") 158 insertTV(referenceTime.Add(-5*day), var5, "inv3", false, false, "forbidden-realm") 159 160 return nil 161 }) 162 So(err, ShouldBeNil) 163 164 server := NewTestHistoryServer() 165 166 Convey("Query", func() { 167 req := &pb.QueryTestHistoryRequest{ 168 Project: "project", 169 TestId: "test_id", 170 Predicate: &pb.TestVerdictPredicate{ 171 SubRealm: "realm", 172 }, 173 PageSize: 5, 174 } 175 176 expectedChangelists := []*pb.Changelist{ 177 { 178 Host: "anothergerrit.gerrit.instance", 179 Change: 5471, 180 Patchset: 6, 181 OwnerKind: pb.ChangelistOwnerKind_HUMAN, 182 }, 183 { 184 Host: "mygerrit-review.googlesource.com", 185 Change: 4321, 186 Patchset: 5, 187 OwnerKind: pb.ChangelistOwnerKind_AUTOMATION, 188 }, 189 } 190 191 Convey("unauthorised requests are rejected", func() { 192 testPerm := func(ctx context.Context) { 193 res, err := server.Query(ctx, req) 194 So(err, ShouldErrLike, `caller does not have permission`, `in realm "project:realm"`) 195 So(err, ShouldHaveGRPCStatus, codes.PermissionDenied) 196 So(res, ShouldBeNil) 197 } 198 199 // No permission. 200 ctx = auth.WithState(ctx, &authtest.FakeState{ 201 Identity: "user:someone@example.com", 202 }) 203 testPerm(ctx) 204 205 // testResults.list only. 206 ctx = auth.WithState(ctx, &authtest.FakeState{ 207 Identity: "user:someone@example.com", 208 IdentityPermissions: []authtest.RealmPermission{ 209 { 210 Realm: "project:realm", 211 Permission: rdbperms.PermListTestResults, 212 }, 213 { 214 Realm: "project:other_realm", 215 Permission: rdbperms.PermListTestExonerations, 216 }, 217 }, 218 }) 219 testPerm(ctx) 220 221 // testExonerations.list only. 222 ctx = auth.WithState(ctx, &authtest.FakeState{ 223 Identity: "user:someone@example.com", 224 IdentityPermissions: []authtest.RealmPermission{ 225 { 226 Realm: "project:other_realm", 227 Permission: rdbperms.PermListTestResults, 228 }, 229 { 230 Realm: "project:realm", 231 Permission: rdbperms.PermListTestExonerations, 232 }, 233 }, 234 }) 235 testPerm(ctx) 236 }) 237 238 Convey("invalid requests are rejected", func() { 239 req.PageSize = -1 240 res, err := server.Query(ctx, req) 241 So(err, ShouldNotBeNil) 242 So(err, ShouldHaveGRPCStatus, codes.InvalidArgument) 243 So(res, ShouldBeNil) 244 }) 245 246 Convey("multi-realms", func() { 247 req.Predicate.SubRealm = "" 248 req.Predicate.VariantPredicate = &pb.VariantPredicate{ 249 Predicate: &pb.VariantPredicate_Contains{ 250 Contains: pbutil.Variant("key2", "val2"), 251 }, 252 } 253 res, err := server.Query(ctx, req) 254 So(err, ShouldBeNil) 255 So(res, ShouldResembleProto, &pb.QueryTestHistoryResponse{ 256 Verdicts: []*pb.TestVerdict{ 257 { 258 TestId: "test_id", 259 VariantHash: pbutil.VariantHash(var3), 260 InvocationId: "inv1", 261 Status: pb.TestVerdictStatus_EXPECTED, 262 PartitionTime: timestamppb.New(referenceTime.Add(-3 * day)), 263 Changelists: expectedChangelists, 264 }, 265 { 266 TestId: "test_id", 267 VariantHash: pbutil.VariantHash(var4), 268 InvocationId: "inv2", 269 Status: pb.TestVerdictStatus_EXPECTED, 270 PartitionTime: timestamppb.New(referenceTime.Add(-4 * day)), 271 }, 272 }, 273 }) 274 }) 275 276 Convey("e2e", func() { 277 res, err := server.Query(ctx, req) 278 So(err, ShouldBeNil) 279 So(res, ShouldResembleProto, &pb.QueryTestHistoryResponse{ 280 Verdicts: []*pb.TestVerdict{ 281 { 282 TestId: "test_id", 283 VariantHash: pbutil.VariantHash(var1), 284 InvocationId: "inv1", 285 Status: pb.TestVerdictStatus_EXPECTED, 286 PartitionTime: timestamppb.New(referenceTime.Add(-1 * day)), 287 }, 288 { 289 TestId: "test_id", 290 VariantHash: pbutil.VariantHash(var1), 291 InvocationId: "inv2", 292 Status: pb.TestVerdictStatus_EXPECTED, 293 PartitionTime: timestamppb.New(referenceTime.Add(-1 * day)), 294 }, 295 { 296 TestId: "test_id", 297 VariantHash: pbutil.VariantHash(var2), 298 InvocationId: "inv1", 299 Status: pb.TestVerdictStatus_EXPECTED, 300 PartitionTime: timestamppb.New(referenceTime.Add(-1 * day)), 301 }, 302 { 303 TestId: "test_id", 304 VariantHash: pbutil.VariantHash(var1), 305 InvocationId: "inv1", 306 Status: pb.TestVerdictStatus_EXPECTED, 307 PartitionTime: timestamppb.New(referenceTime.Add(-2 * day)), 308 }, 309 { 310 TestId: "test_id", 311 VariantHash: pbutil.VariantHash(var1), 312 InvocationId: "inv2", 313 Status: pb.TestVerdictStatus_EXPECTED, 314 PartitionTime: timestamppb.New(referenceTime.Add(-2 * day)), 315 Changelists: expectedChangelists, 316 }, 317 }, 318 NextPageToken: res.NextPageToken, 319 }) 320 So(res.NextPageToken, ShouldNotBeEmpty) 321 322 req.PageToken = res.NextPageToken 323 res, err = server.Query(ctx, req) 324 So(err, ShouldBeNil) 325 So(res, ShouldResembleProto, &pb.QueryTestHistoryResponse{ 326 Verdicts: []*pb.TestVerdict{ 327 { 328 TestId: "test_id", 329 VariantHash: pbutil.VariantHash(var2), 330 InvocationId: "inv1", 331 Status: pb.TestVerdictStatus_EXPECTED, 332 PartitionTime: timestamppb.New(referenceTime.Add(-2 * day)), 333 Changelists: expectedChangelists, 334 }, 335 { 336 TestId: "test_id", 337 VariantHash: pbutil.VariantHash(var3), 338 InvocationId: "inv1", 339 Status: pb.TestVerdictStatus_EXPECTED, 340 PartitionTime: timestamppb.New(referenceTime.Add(-3 * day)), 341 Changelists: expectedChangelists, 342 }, 343 }, 344 }) 345 }) 346 347 Convey("include bisection", func() { 348 req.PageSize = 10 349 req.Predicate.IncludeBisectionResults = true 350 res, err := server.Query(ctx, req) 351 So(err, ShouldBeNil) 352 So(res, ShouldResembleProto, &pb.QueryTestHistoryResponse{ 353 Verdicts: []*pb.TestVerdict{ 354 { 355 TestId: "test_id", 356 VariantHash: pbutil.VariantHash(var1), 357 InvocationId: "inv1", 358 Status: pb.TestVerdictStatus_EXPECTED, 359 PartitionTime: timestamppb.New(referenceTime.Add(-1 * day)), 360 }, 361 { 362 TestId: "test_id", 363 VariantHash: pbutil.VariantHash(var1), 364 InvocationId: "inv2", 365 Status: pb.TestVerdictStatus_EXPECTED, 366 PartitionTime: timestamppb.New(referenceTime.Add(-1 * day)), 367 }, 368 { 369 TestId: "test_id", 370 VariantHash: pbutil.VariantHash(var2), 371 InvocationId: "inv1", 372 Status: pb.TestVerdictStatus_EXPECTED, 373 PartitionTime: timestamppb.New(referenceTime.Add(-1 * day)), 374 }, 375 { 376 TestId: "test_id", 377 VariantHash: pbutil.VariantHash(var2), 378 InvocationId: "inv2", 379 Status: pb.TestVerdictStatus_EXPECTED, 380 PartitionTime: timestamppb.New(referenceTime.Add(-1 * day)), 381 }, 382 { 383 TestId: "test_id", 384 VariantHash: pbutil.VariantHash(var1), 385 InvocationId: "inv1", 386 Status: pb.TestVerdictStatus_EXPECTED, 387 PartitionTime: timestamppb.New(referenceTime.Add(-2 * day)), 388 }, 389 { 390 TestId: "test_id", 391 VariantHash: pbutil.VariantHash(var1), 392 InvocationId: "inv2", 393 Status: pb.TestVerdictStatus_EXPECTED, 394 PartitionTime: timestamppb.New(referenceTime.Add(-2 * day)), 395 Changelists: expectedChangelists, 396 }, 397 { 398 TestId: "test_id", 399 VariantHash: pbutil.VariantHash(var2), 400 InvocationId: "inv1", 401 Status: pb.TestVerdictStatus_EXPECTED, 402 PartitionTime: timestamppb.New(referenceTime.Add(-2 * day)), 403 Changelists: expectedChangelists, 404 }, 405 { 406 TestId: "test_id", 407 VariantHash: pbutil.VariantHash(var3), 408 InvocationId: "inv1", 409 Status: pb.TestVerdictStatus_EXPECTED, 410 PartitionTime: timestamppb.New(referenceTime.Add(-3 * day)), 411 Changelists: expectedChangelists, 412 }, 413 }, 414 }) 415 }) 416 }) 417 418 Convey("QueryStats", func() { 419 req := &pb.QueryTestHistoryStatsRequest{ 420 Project: "project", 421 TestId: "test_id", 422 Predicate: &pb.TestVerdictPredicate{ 423 SubRealm: "realm", 424 }, 425 PageSize: 3, 426 } 427 428 Convey("unauthorised requests are rejected", func() { 429 testPerm := func(ctx context.Context) { 430 res, err := server.QueryStats(ctx, req) 431 So(err, ShouldErrLike, `caller does not have permission`, `in realm "project:realm"`) 432 So(err, ShouldHaveGRPCStatus, codes.PermissionDenied) 433 So(res, ShouldBeNil) 434 } 435 436 // No permission. 437 ctx = auth.WithState(ctx, &authtest.FakeState{ 438 Identity: "user:someone@example.com", 439 }) 440 testPerm(ctx) 441 442 // testResults.list only. 443 ctx = auth.WithState(ctx, &authtest.FakeState{ 444 Identity: "user:someone@example.com", 445 IdentityPermissions: []authtest.RealmPermission{ 446 { 447 Realm: "project:realm", 448 Permission: rdbperms.PermListTestResults, 449 }, 450 { 451 Realm: "project:other_realm", 452 Permission: rdbperms.PermListTestExonerations, 453 }, 454 }, 455 }) 456 testPerm(ctx) 457 458 // testExonerations.list only. 459 ctx = auth.WithState(ctx, &authtest.FakeState{ 460 Identity: "user:someone@example.com", 461 IdentityPermissions: []authtest.RealmPermission{ 462 { 463 Realm: "project:other_realm", 464 Permission: rdbperms.PermListTestResults, 465 }, 466 { 467 Realm: "project:realm", 468 Permission: rdbperms.PermListTestExonerations, 469 }, 470 }, 471 }) 472 testPerm(ctx) 473 }) 474 475 Convey("invalid requests are rejected", func() { 476 req.PageSize = -1 477 res, err := server.QueryStats(ctx, req) 478 So(err, ShouldNotBeNil) 479 So(err, ShouldHaveGRPCStatus, codes.InvalidArgument) 480 So(res, ShouldBeNil) 481 }) 482 483 Convey("multi-realms", func() { 484 req.Predicate.SubRealm = "" 485 req.Predicate.VariantPredicate = &pb.VariantPredicate{ 486 Predicate: &pb.VariantPredicate_Contains{ 487 Contains: pbutil.Variant("key2", "val2"), 488 }, 489 } 490 res, err := server.QueryStats(ctx, req) 491 So(err, ShouldBeNil) 492 So(res, ShouldResembleProto, &pb.QueryTestHistoryStatsResponse{ 493 Groups: []*pb.QueryTestHistoryStatsResponse_Group{ 494 { 495 PartitionTime: timestamppb.New(referenceTime.Add(-3 * day)), 496 VariantHash: pbutil.VariantHash(var3), 497 ExpectedCount: 1, 498 }, 499 { 500 PartitionTime: timestamppb.New(referenceTime.Add(-4 * day)), 501 VariantHash: pbutil.VariantHash(var4), 502 ExpectedCount: 1, 503 }, 504 }, 505 }) 506 }) 507 508 Convey("e2e", func() { 509 res, err := server.QueryStats(ctx, req) 510 So(err, ShouldBeNil) 511 So(res, ShouldResembleProto, &pb.QueryTestHistoryStatsResponse{ 512 Groups: []*pb.QueryTestHistoryStatsResponse_Group{ 513 { 514 PartitionTime: timestamppb.New(referenceTime.Add(-1 * day)), 515 VariantHash: pbutil.VariantHash(var1), 516 ExpectedCount: 2, 517 }, 518 { 519 PartitionTime: timestamppb.New(referenceTime.Add(-1 * day)), 520 VariantHash: pbutil.VariantHash(var2), 521 ExpectedCount: 1, 522 }, 523 { 524 PartitionTime: timestamppb.New(referenceTime.Add(-2 * day)), 525 VariantHash: pbutil.VariantHash(var1), 526 ExpectedCount: 2, 527 }, 528 }, 529 NextPageToken: res.NextPageToken, 530 }) 531 So(res.NextPageToken, ShouldNotBeEmpty) 532 533 req.PageToken = res.NextPageToken 534 res, err = server.QueryStats(ctx, req) 535 So(err, ShouldBeNil) 536 So(res, ShouldResembleProto, &pb.QueryTestHistoryStatsResponse{ 537 Groups: []*pb.QueryTestHistoryStatsResponse_Group{ 538 { 539 PartitionTime: timestamppb.New(referenceTime.Add(-2 * day)), 540 VariantHash: pbutil.VariantHash(var2), 541 ExpectedCount: 1, 542 }, 543 { 544 PartitionTime: timestamppb.New(referenceTime.Add(-3 * day)), 545 VariantHash: pbutil.VariantHash(var3), 546 ExpectedCount: 1, 547 }, 548 }, 549 }) 550 }) 551 552 Convey("include bisection", func() { 553 req.Predicate.IncludeBisectionResults = true 554 req.PageSize = 10 555 res, err := server.QueryStats(ctx, req) 556 So(err, ShouldBeNil) 557 So(res, ShouldResembleProto, &pb.QueryTestHistoryStatsResponse{ 558 Groups: []*pb.QueryTestHistoryStatsResponse_Group{ 559 { 560 PartitionTime: timestamppb.New(referenceTime.Add(-1 * day)), 561 VariantHash: pbutil.VariantHash(var1), 562 ExpectedCount: 2, 563 }, 564 { 565 PartitionTime: timestamppb.New(referenceTime.Add(-1 * day)), 566 VariantHash: pbutil.VariantHash(var2), 567 ExpectedCount: 2, 568 }, 569 { 570 PartitionTime: timestamppb.New(referenceTime.Add(-2 * day)), 571 VariantHash: pbutil.VariantHash(var1), 572 ExpectedCount: 2, 573 }, 574 { 575 PartitionTime: timestamppb.New(referenceTime.Add(-2 * day)), 576 VariantHash: pbutil.VariantHash(var2), 577 ExpectedCount: 1, 578 }, 579 { 580 PartitionTime: timestamppb.New(referenceTime.Add(-3 * day)), 581 VariantHash: pbutil.VariantHash(var3), 582 ExpectedCount: 1, 583 }, 584 }, 585 }) 586 }) 587 }) 588 589 Convey("QueryVariants", func() { 590 req := &pb.QueryVariantsRequest{ 591 Project: "project", 592 TestId: "test_id", 593 SubRealm: "realm", 594 PageSize: 2, 595 } 596 597 Convey("unauthorised requests are rejected", func() { 598 ctx = auth.WithState(ctx, &authtest.FakeState{ 599 Identity: "user:someone@example.com", 600 }) 601 res, err := server.QueryVariants(ctx, req) 602 So(err, ShouldErrLike, `caller does not have permission`, `in realm "project:realm"`) 603 So(err, ShouldHaveGRPCStatus, codes.PermissionDenied) 604 So(res, ShouldBeNil) 605 }) 606 607 Convey("invalid requests are rejected", func() { 608 req.PageSize = -1 609 res, err := server.QueryVariants(ctx, req) 610 So(err, ShouldNotBeNil) 611 So(err, ShouldHaveGRPCStatus, codes.InvalidArgument) 612 So(res, ShouldBeNil) 613 }) 614 615 Convey("multi-realms", func() { 616 req.PageSize = 0 617 req.SubRealm = "" 618 req.VariantPredicate = &pb.VariantPredicate{ 619 Predicate: &pb.VariantPredicate_Contains{ 620 Contains: pbutil.Variant("key2", "val2"), 621 }, 622 } 623 res, err := server.QueryVariants(ctx, req) 624 So(err, ShouldBeNil) 625 So(res, ShouldResembleProto, &pb.QueryVariantsResponse{ 626 Variants: []*pb.QueryVariantsResponse_VariantInfo{ 627 { 628 VariantHash: pbutil.VariantHash(var3), 629 Variant: var3, 630 }, 631 { 632 VariantHash: pbutil.VariantHash(var4), 633 Variant: var4, 634 }, 635 }, 636 }) 637 }) 638 639 Convey("e2e", func() { 640 res, err := server.QueryVariants(ctx, req) 641 So(err, ShouldBeNil) 642 So(res, ShouldResembleProto, &pb.QueryVariantsResponse{ 643 Variants: []*pb.QueryVariantsResponse_VariantInfo{ 644 { 645 VariantHash: pbutil.VariantHash(var1), 646 Variant: var1, 647 }, 648 { 649 VariantHash: pbutil.VariantHash(var3), 650 Variant: var3, 651 }, 652 }, 653 NextPageToken: res.NextPageToken, 654 }) 655 So(res.NextPageToken, ShouldNotBeEmpty) 656 657 req.PageToken = res.NextPageToken 658 res, err = server.QueryVariants(ctx, req) 659 So(err, ShouldBeNil) 660 So(res, ShouldResembleProto, &pb.QueryVariantsResponse{ 661 Variants: []*pb.QueryVariantsResponse_VariantInfo{ 662 { 663 VariantHash: pbutil.VariantHash(var2), 664 Variant: var2, 665 }, 666 }, 667 }) 668 }) 669 }) 670 671 Convey("QueryTests", func() { 672 req := &pb.QueryTestsRequest{ 673 Project: "project", 674 TestIdSubstring: "test_id", 675 SubRealm: "realm", 676 PageSize: 2, 677 } 678 679 Convey("unauthorised requests are rejected", func() { 680 ctx = auth.WithState(ctx, &authtest.FakeState{ 681 Identity: "user:someone@example.com", 682 }) 683 res, err := server.QueryTests(ctx, req) 684 So(err, ShouldErrLike, `caller does not have permission`, `in realm "project:realm"`) 685 So(err, ShouldHaveGRPCStatus, codes.PermissionDenied) 686 So(res, ShouldBeNil) 687 }) 688 689 Convey("invalid requests are rejected", func() { 690 req.PageSize = -1 691 res, err := server.QueryTests(ctx, req) 692 So(err, ShouldNotBeNil) 693 So(err, ShouldHaveGRPCStatus, codes.InvalidArgument) 694 So(res, ShouldBeNil) 695 }) 696 697 Convey("multi-realms", func() { 698 req.PageSize = 0 699 req.SubRealm = "" 700 res, err := server.QueryTests(ctx, req) 701 So(err, ShouldBeNil) 702 So(res, ShouldResembleProto, &pb.QueryTestsResponse{ 703 TestIds: []string{"test_id", "test_id1", "test_id2", "test_id3"}, 704 }) 705 }) 706 707 Convey("e2e", func() { 708 res, err := server.QueryTests(ctx, req) 709 So(err, ShouldBeNil) 710 So(res, ShouldResembleProto, &pb.QueryTestsResponse{ 711 TestIds: []string{"test_id", "test_id1"}, 712 NextPageToken: res.NextPageToken, 713 }) 714 So(res.NextPageToken, ShouldNotBeEmpty) 715 716 req.PageToken = res.NextPageToken 717 res, err = server.QueryTests(ctx, req) 718 So(err, ShouldBeNil) 719 So(res, ShouldResembleProto, &pb.QueryTestsResponse{ 720 TestIds: []string{"test_id2"}, 721 }) 722 }) 723 }) 724 }) 725 } 726 727 func TestValidateQueryTestHistoryRequest(t *testing.T) { 728 t.Parallel() 729 730 Convey("validateQueryTestHistoryRequest", t, func() { 731 req := &pb.QueryTestHistoryRequest{ 732 Project: "project", 733 TestId: "test_id", 734 Predicate: &pb.TestVerdictPredicate{ 735 SubRealm: "realm", 736 }, 737 PageSize: 5, 738 } 739 740 Convey("valid", func() { 741 err := validateQueryTestHistoryRequest(req) 742 So(err, ShouldBeNil) 743 }) 744 745 Convey("no project", func() { 746 req.Project = "" 747 err := validateQueryTestHistoryRequest(req) 748 So(err, ShouldErrLike, "project: unspecified") 749 }) 750 751 Convey("invalid project", func() { 752 req.Project = "project:realm" 753 err := validateQueryTestHistoryRequest(req) 754 So(err, ShouldErrLike, `project: must match ^[a-z0-9\-]{1,40}$`) 755 }) 756 757 Convey("no test_id", func() { 758 req.TestId = "" 759 err := validateQueryTestHistoryRequest(req) 760 So(err, ShouldErrLike, "test_id: unspecified") 761 }) 762 763 Convey("invalid test_id", func() { 764 req.TestId = "\xFF" 765 err := validateQueryTestHistoryRequest(req) 766 So(err, ShouldErrLike, "test_id: not a valid utf8 string") 767 }) 768 769 Convey("no predicate", func() { 770 req.Predicate = nil 771 err := validateQueryTestHistoryRequest(req) 772 So(err, ShouldErrLike, "predicate", "unspecified") 773 }) 774 775 Convey("no page size", func() { 776 req.PageSize = 0 777 err := validateQueryTestHistoryRequest(req) 778 So(err, ShouldBeNil) 779 }) 780 781 Convey("negative page size", func() { 782 req.PageSize = -1 783 err := validateQueryTestHistoryRequest(req) 784 So(err, ShouldErrLike, "page_size", "negative") 785 }) 786 }) 787 } 788 789 func TestValidateQueryTestHistoryStatsRequest(t *testing.T) { 790 t.Parallel() 791 792 Convey("validateQueryTestHistoryStatsRequest", t, func() { 793 req := &pb.QueryTestHistoryStatsRequest{ 794 Project: "project", 795 TestId: "test_id", 796 Predicate: &pb.TestVerdictPredicate{ 797 SubRealm: "realm", 798 }, 799 PageSize: 5, 800 } 801 802 Convey("valid", func() { 803 err := validateQueryTestHistoryStatsRequest(req) 804 So(err, ShouldBeNil) 805 }) 806 807 Convey("no project", func() { 808 req.Project = "" 809 err := validateQueryTestHistoryStatsRequest(req) 810 So(err, ShouldErrLike, "project: unspecified") 811 }) 812 813 Convey("invalid project", func() { 814 req.Project = "project:realm" 815 err := validateQueryTestHistoryStatsRequest(req) 816 So(err, ShouldErrLike, `project: must match ^[a-z0-9\-]{1,40}$`) 817 }) 818 819 Convey("no test_id", func() { 820 req.TestId = "" 821 err := validateQueryTestHistoryStatsRequest(req) 822 So(err, ShouldErrLike, "test_id: unspecified") 823 }) 824 825 Convey("invalid test_id", func() { 826 req.TestId = "\xFF" 827 err := validateQueryTestHistoryStatsRequest(req) 828 So(err, ShouldErrLike, "test_id: not a valid utf8 string") 829 }) 830 831 Convey("no predicate", func() { 832 req.Predicate = nil 833 err := validateQueryTestHistoryStatsRequest(req) 834 So(err, ShouldErrLike, "predicate", "unspecified") 835 }) 836 837 Convey("no page size", func() { 838 req.PageSize = 0 839 err := validateQueryTestHistoryStatsRequest(req) 840 So(err, ShouldBeNil) 841 }) 842 843 Convey("negative page size", func() { 844 req.PageSize = -1 845 err := validateQueryTestHistoryStatsRequest(req) 846 So(err, ShouldErrLike, "page_size", "negative") 847 }) 848 }) 849 } 850 851 func TestValidateQueryVariantsRequest(t *testing.T) { 852 t.Parallel() 853 854 Convey("validateQueryVariantsRequest", t, func() { 855 req := &pb.QueryVariantsRequest{ 856 Project: "project", 857 TestId: "test_id", 858 PageSize: 5, 859 } 860 861 Convey("valid", func() { 862 err := validateQueryVariantsRequest(req) 863 So(err, ShouldBeNil) 864 }) 865 866 Convey("no project", func() { 867 req.Project = "" 868 err := validateQueryVariantsRequest(req) 869 So(err, ShouldErrLike, "project: unspecified") 870 }) 871 872 Convey("invalid project", func() { 873 req.Project = "project:realm" 874 err := validateQueryVariantsRequest(req) 875 So(err, ShouldErrLike, `project: must match ^[a-z0-9\-]{1,40}$`) 876 }) 877 878 Convey("no test_id", func() { 879 req.TestId = "" 880 err := validateQueryVariantsRequest(req) 881 So(err, ShouldErrLike, "test_id: unspecified") 882 }) 883 884 Convey("invalid test_id", func() { 885 req.TestId = "\xFF" 886 err := validateQueryVariantsRequest(req) 887 So(err, ShouldErrLike, "test_id: not a valid utf8 string") 888 }) 889 890 Convey("bad sub_realm", func() { 891 req.SubRealm = "a:realm" 892 err := validateQueryVariantsRequest(req) 893 So(err, ShouldErrLike, "sub_realm: bad project-scoped realm name") 894 }) 895 896 Convey("no page size", func() { 897 req.PageSize = 0 898 err := validateQueryVariantsRequest(req) 899 So(err, ShouldBeNil) 900 }) 901 902 Convey("negative page size", func() { 903 req.PageSize = -1 904 err := validateQueryVariantsRequest(req) 905 So(err, ShouldErrLike, "page_size", "negative") 906 }) 907 }) 908 } 909 910 func TestValidateQueryTestsRequest(t *testing.T) { 911 t.Parallel() 912 913 Convey("validateQueryTestsRequest", t, func() { 914 req := &pb.QueryTestsRequest{ 915 Project: "project", 916 TestIdSubstring: "test_id", 917 PageSize: 5, 918 } 919 920 Convey("valid", func() { 921 err := validateQueryTestsRequest(req) 922 So(err, ShouldBeNil) 923 }) 924 925 Convey("no project", func() { 926 req.Project = "" 927 err := validateQueryTestsRequest(req) 928 So(err, ShouldErrLike, "project: unspecified") 929 }) 930 931 Convey("invalid project", func() { 932 req.Project = "project:realm" 933 err := validateQueryTestsRequest(req) 934 So(err, ShouldErrLike, `project: must match ^[a-z0-9\-]{1,40}$`) 935 }) 936 937 Convey("no test_id_substring", func() { 938 req.TestIdSubstring = "" 939 err := validateQueryTestsRequest(req) 940 So(err, ShouldErrLike, "test_id_substring: unspecified") 941 }) 942 943 Convey("bad test_id_substring", func() { 944 req.TestIdSubstring = "\xFF" 945 err := validateQueryTestsRequest(req) 946 So(err, ShouldErrLike, "test_id_substring: not a valid utf8 string") 947 }) 948 949 Convey("bad sub_realm", func() { 950 req.SubRealm = "a:realm" 951 err := validateQueryTestsRequest(req) 952 So(err, ShouldErrLike, "sub_realm: bad project-scoped realm name") 953 }) 954 955 Convey("no page size", func() { 956 req.PageSize = 0 957 err := validateQueryTestsRequest(req) 958 So(err, ShouldBeNil) 959 }) 960 961 Convey("negative page size", func() { 962 req.PageSize = -1 963 err := validateQueryTestsRequest(req) 964 So(err, ShouldErrLike, "page_size", "negative") 965 }) 966 }) 967 }