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 }