go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/services/recorder/batch_create_invocations_test.go (about)

     1  // Copyright 2020 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  	"testing"
    20  	"time"
    21  
    22  	"github.com/golang/protobuf/proto"
    23  	"google.golang.org/grpc"
    24  	"google.golang.org/grpc/codes"
    25  	"google.golang.org/grpc/status"
    26  
    27  	"go.chromium.org/luci/common/clock"
    28  	"go.chromium.org/luci/common/clock/testclock"
    29  	"go.chromium.org/luci/common/testing/prpctest"
    30  	"go.chromium.org/luci/grpc/appstatus"
    31  	"go.chromium.org/luci/server/auth"
    32  	"go.chromium.org/luci/server/auth/authtest"
    33  	"go.chromium.org/luci/server/span"
    34  
    35  	"go.chromium.org/luci/resultdb/internal/invocations"
    36  	"go.chromium.org/luci/resultdb/internal/testutil"
    37  	"go.chromium.org/luci/resultdb/pbutil"
    38  	pb "go.chromium.org/luci/resultdb/proto/v1"
    39  
    40  	. "github.com/smartystreets/goconvey/convey"
    41  	. "go.chromium.org/luci/common/testing/assertions"
    42  )
    43  
    44  func TestValidateBatchCreateInvocationsRequest(t *testing.T) {
    45  	t.Parallel()
    46  	now := testclock.TestRecentTimeUTC
    47  
    48  	Convey(`TestValidateBatchCreateInvocationsRequest`, t, func() {
    49  		Convey(`invalid request id - Batch`, func() {
    50  			_, _, err := validateBatchCreateInvocationsRequest(
    51  				now,
    52  				[]*pb.CreateInvocationRequest{{
    53  					InvocationId: "u-a",
    54  					Invocation: &pb.Invocation{
    55  						Realm: "testproject:testrealm",
    56  					},
    57  				}},
    58  				"😃",
    59  			)
    60  			So(err, ShouldErrLike, "request_id: does not match")
    61  		})
    62  		Convey(`non-matching request id - Batch`, func() {
    63  			_, _, err := validateBatchCreateInvocationsRequest(
    64  				now,
    65  				[]*pb.CreateInvocationRequest{{
    66  					InvocationId: "u-a",
    67  					Invocation: &pb.Invocation{
    68  						Realm: "testproject:testrealm",
    69  					},
    70  					RequestId: "valid, but different"}},
    71  				"valid",
    72  			)
    73  			So(err, ShouldErrLike, `request_id: "valid" does not match`)
    74  		})
    75  		Convey(`Too many requests`, func() {
    76  			_, _, err := validateBatchCreateInvocationsRequest(
    77  				now,
    78  				make([]*pb.CreateInvocationRequest, 1000),
    79  				"valid",
    80  			)
    81  			So(err, ShouldErrLike, `the number of requests in the batch exceeds 500`)
    82  		})
    83  		Convey(`valid`, func() {
    84  			ids, _, err := validateBatchCreateInvocationsRequest(
    85  				now,
    86  				[]*pb.CreateInvocationRequest{{
    87  					InvocationId: "u-a",
    88  					RequestId:    "valid",
    89  					Invocation: &pb.Invocation{
    90  						Realm: "testproject:testrealm",
    91  					},
    92  				}},
    93  				"valid",
    94  			)
    95  			So(err, ShouldBeNil)
    96  			So(ids.Has("u-a"), ShouldBeTrue)
    97  			So(len(ids), ShouldEqual, 1)
    98  		})
    99  	})
   100  }
   101  
   102  func TestBatchCreateInvocations(t *testing.T) {
   103  	Convey(`TestBatchCreateInvocations`, t, func() {
   104  		ctx := testutil.SpannerTestContext(t)
   105  		// Configure mock authentication to allow creation of custom invocation ids.
   106  		authState := &authtest.FakeState{
   107  			Identity: "user:someone@example.com",
   108  			IdentityPermissions: []authtest.RealmPermission{
   109  				{Realm: "testproject:testrealm", Permission: permCreateInvocation},
   110  				{Realm: "testproject:testrealm", Permission: permExportToBigQuery},
   111  				{Realm: "testproject:testrealm", Permission: permSetProducerResource},
   112  				{Realm: "testproject:testrealm", Permission: permIncludeInvocation},
   113  				{Realm: "testproject:createonly", Permission: permCreateInvocation},
   114  				{Realm: "testproject:testrealm", Permission: permPutBaseline},
   115  			},
   116  		}
   117  		ctx = auth.WithState(ctx, authState)
   118  
   119  		start := clock.Now(ctx).UTC()
   120  
   121  		// Setup a full HTTP server in order to retrieve response headers.
   122  		server := &prpctest.Server{}
   123  		server.UnaryServerInterceptor = func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
   124  			res, err := handler(ctx, req)
   125  			err = appstatus.GRPCifyAndLog(ctx, err)
   126  			return res, err
   127  		}
   128  		pb.RegisterRecorderServer(server, newTestRecorderServer())
   129  		server.Start(ctx)
   130  		defer server.Close()
   131  		client, err := server.NewClient()
   132  		So(err, ShouldBeNil)
   133  		recorder := pb.NewRecorderPRPCClient(client)
   134  
   135  		Convey(`idempotent`, func() {
   136  			req := &pb.BatchCreateInvocationsRequest{
   137  				Requests: []*pb.CreateInvocationRequest{{
   138  					InvocationId: "u-batchinv",
   139  					Invocation:   &pb.Invocation{Realm: "testproject:testrealm"},
   140  				}, {
   141  					InvocationId: "u-batchinv2",
   142  					Invocation:   &pb.Invocation{Realm: "testproject:testrealm"},
   143  				}},
   144  				RequestId: "request id",
   145  			}
   146  			res, err := recorder.BatchCreateInvocations(ctx, req)
   147  			So(err, ShouldBeNil)
   148  
   149  			res2, err := recorder.BatchCreateInvocations(ctx, req)
   150  			So(err, ShouldBeNil)
   151  			// Update tokens are regenerated the second time, but they are both valid.
   152  			res2.UpdateTokens = res.UpdateTokens
   153  			// Otherwise, the responses must be identical.
   154  			So(res2, ShouldResembleProto, res)
   155  		})
   156  		Convey(`inclusion of non-existent invocation`, func() {
   157  			req := &pb.BatchCreateInvocationsRequest{
   158  				Requests: []*pb.CreateInvocationRequest{{
   159  					InvocationId: "u-batchinv",
   160  					Invocation: &pb.Invocation{
   161  						Realm:               "testproject:testrealm",
   162  						IncludedInvocations: []string{"invocations/u-missing-inv"},
   163  					},
   164  				}, {
   165  					InvocationId: "u-batchinv2",
   166  					Invocation:   &pb.Invocation{Realm: "testproject:testrealm"},
   167  				}},
   168  			}
   169  			_, err := recorder.BatchCreateInvocations(ctx, req)
   170  			So(err, ShouldErrLike, "invocations/u-missing-inv not found")
   171  		})
   172  
   173  		Convey(`inclusion of existing disallowed invocation`, func() {
   174  			req := &pb.BatchCreateInvocationsRequest{
   175  				Requests: []*pb.CreateInvocationRequest{{
   176  					InvocationId: "u-batchinv",
   177  					Invocation:   &pb.Invocation{Realm: "testproject:createonly"},
   178  				}},
   179  			}
   180  			_, err := recorder.BatchCreateInvocations(ctx, req)
   181  			So(err, ShouldBeNil)
   182  
   183  			req = &pb.BatchCreateInvocationsRequest{
   184  				Requests: []*pb.CreateInvocationRequest{{
   185  					InvocationId: "u-batchinv2",
   186  					Invocation: &pb.Invocation{
   187  						Realm:               "testproject:testrealm",
   188  						IncludedInvocations: []string{"invocations/u-batchinv"},
   189  					},
   190  				}},
   191  				RequestId: "request id",
   192  			}
   193  			_, err = recorder.BatchCreateInvocations(ctx, req)
   194  			So(err, ShouldErrLike, "caller does not have permission resultdb.invocations.include")
   195  		})
   196  
   197  		Convey(`Same request ID, different identity`, func() {
   198  			req := &pb.BatchCreateInvocationsRequest{
   199  				Requests: []*pb.CreateInvocationRequest{{
   200  					InvocationId: "u-inv",
   201  					Invocation:   &pb.Invocation{Realm: "testproject:testrealm"},
   202  				}},
   203  				RequestId: "request id",
   204  			}
   205  			_, err := recorder.BatchCreateInvocations(ctx, req)
   206  			So(err, ShouldBeNil)
   207  
   208  			authState.Identity = "user:someone-else@example.com"
   209  			_, err = recorder.BatchCreateInvocations(ctx, req)
   210  			So(status.Code(err), ShouldEqual, codes.AlreadyExists)
   211  		})
   212  
   213  		Convey(`end to end`, func() {
   214  			deadline := pbutil.MustTimestampProto(start.Add(time.Hour))
   215  			bqExport := &pb.BigQueryExport{
   216  				Project: "project",
   217  				Dataset: "dataset",
   218  				Table:   "table",
   219  				ResultType: &pb.BigQueryExport_TestResults_{
   220  					TestResults: &pb.BigQueryExport_TestResults{},
   221  				},
   222  			}
   223  			req := &pb.BatchCreateInvocationsRequest{
   224  				Requests: []*pb.CreateInvocationRequest{
   225  					{
   226  						InvocationId: "u-batch-inv",
   227  						Invocation: &pb.Invocation{
   228  							Deadline: deadline,
   229  							Tags:     pbutil.StringPairs("a", "1", "b", "2"),
   230  							BigqueryExports: []*pb.BigQueryExport{
   231  								bqExport,
   232  							},
   233  							ProducerResource:    "//builds.example.com/builds/1",
   234  							Realm:               "testproject:testrealm",
   235  							IncludedInvocations: []string{"invocations/u-batch-inv2"},
   236  							Properties:          testutil.TestProperties(),
   237  							SourceSpec: &pb.SourceSpec{
   238  								Inherit: true,
   239  							},
   240  							BaselineId: "testrealm:testbuilder",
   241  						},
   242  					},
   243  					{
   244  						InvocationId: "u-batch-inv2",
   245  						Invocation: &pb.Invocation{
   246  							Deadline: deadline,
   247  							Tags:     pbutil.StringPairs("a", "1", "b", "2"),
   248  							BigqueryExports: []*pb.BigQueryExport{
   249  								bqExport,
   250  							},
   251  							ProducerResource: "//builds.example.com/builds/2",
   252  							Realm:            "testproject:testrealm",
   253  							Properties:       testutil.TestProperties(),
   254  							SourceSpec: &pb.SourceSpec{
   255  								Sources: testutil.TestSources(),
   256  							},
   257  						},
   258  					},
   259  				},
   260  			}
   261  
   262  			resp, err := recorder.BatchCreateInvocations(ctx, req)
   263  			So(err, ShouldBeNil)
   264  
   265  			expected := proto.Clone(req.Requests[0].Invocation).(*pb.Invocation)
   266  			proto.Merge(expected, &pb.Invocation{
   267  				Name:      "invocations/u-batch-inv",
   268  				State:     pb.Invocation_ACTIVE,
   269  				CreatedBy: "user:someone@example.com",
   270  
   271  				// we use Spanner commit time, so skip the check
   272  				CreateTime: resp.Invocations[0].CreateTime,
   273  			})
   274  			expected2 := proto.Clone(req.Requests[1].Invocation).(*pb.Invocation)
   275  			proto.Merge(expected2, &pb.Invocation{
   276  				Name:      "invocations/u-batch-inv2",
   277  				State:     pb.Invocation_ACTIVE,
   278  				CreatedBy: "user:someone@example.com",
   279  
   280  				// we use Spanner commit time, so skip the check
   281  				CreateTime: resp.Invocations[1].CreateTime,
   282  			})
   283  			So(resp.Invocations[0], ShouldResembleProto, expected)
   284  			So(resp.Invocations[1], ShouldResembleProto, expected2)
   285  			So(resp.UpdateTokens, ShouldHaveLength, 2)
   286  
   287  			ctx, cancel := span.ReadOnlyTransaction(ctx)
   288  			defer cancel()
   289  
   290  			inv, err := invocations.Read(ctx, "u-batch-inv")
   291  			So(err, ShouldBeNil)
   292  			So(inv, ShouldResembleProto, expected)
   293  
   294  			inv2, err := invocations.Read(ctx, "u-batch-inv2")
   295  			So(err, ShouldBeNil)
   296  			So(inv2, ShouldResembleProto, expected2)
   297  
   298  			// Check fields not present in the proto.
   299  			var invExpirationTime, expectedResultsExpirationTime time.Time
   300  			err = invocations.ReadColumns(ctx, "u-batch-inv", map[string]any{
   301  				"InvocationExpirationTime":          &invExpirationTime,
   302  				"ExpectedTestResultsExpirationTime": &expectedResultsExpirationTime,
   303  			})
   304  			So(err, ShouldBeNil)
   305  			So(expectedResultsExpirationTime, ShouldHappenWithin, time.Second, start.Add(expectedResultExpiration))
   306  			So(invExpirationTime, ShouldHappenWithin, time.Second, start.Add(invocationExpirationDuration))
   307  			incIDs, err := invocations.ReadIncluded(ctx, invocations.ID("u-batch-inv"))
   308  			So(err, ShouldBeNil)
   309  			So(incIDs.Has(invocations.ID("u-batch-inv2")), ShouldBeTrue)
   310  		})
   311  	})
   312  }