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  }