go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/services/recorder/update_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 "strings" 19 "testing" 20 "time" 21 22 "google.golang.org/genproto/protobuf/field_mask" 23 "google.golang.org/grpc/codes" 24 "google.golang.org/grpc/metadata" 25 "google.golang.org/protobuf/proto" 26 27 "go.chromium.org/luci/common/clock" 28 "go.chromium.org/luci/common/clock/testclock" 29 "go.chromium.org/luci/server/auth" 30 "go.chromium.org/luci/server/auth/authtest" 31 32 "go.chromium.org/luci/resultdb/internal/invocations" 33 "go.chromium.org/luci/resultdb/internal/spanutil" 34 "go.chromium.org/luci/resultdb/internal/testutil" 35 "go.chromium.org/luci/resultdb/internal/testutil/insert" 36 "go.chromium.org/luci/resultdb/pbutil" 37 pb "go.chromium.org/luci/resultdb/proto/v1" 38 39 . "github.com/smartystreets/goconvey/convey" 40 . "go.chromium.org/luci/common/testing/assertions" 41 "google.golang.org/protobuf/types/known/structpb" 42 ) 43 44 func TestValidateUpdateInvocationRequest(t *testing.T) { 45 t.Parallel() 46 now := testclock.TestRecentTimeUTC 47 Convey(`TestValidateUpdateInvocationRequest`, t, func() { 48 request := &pb.UpdateInvocationRequest{ 49 Invocation: &pb.Invocation{ 50 Name: "invocations/inv", 51 }, 52 UpdateMask: &field_mask.FieldMask{Paths: []string{}}, 53 } 54 55 Convey(`empty`, func() { 56 err := validateUpdateInvocationRequest(&pb.UpdateInvocationRequest{}, now) 57 So(err, ShouldErrLike, `invocation: name: unspecified`) 58 }) 59 60 Convey(`invalid id`, func() { 61 request.Invocation.Name = "1" 62 err := validateUpdateInvocationRequest(request, now) 63 So(err, ShouldErrLike, `invocation: name: does not match`) 64 }) 65 66 Convey(`empty update mask`, func() { 67 err := validateUpdateInvocationRequest(request, now) 68 So(err, ShouldErrLike, `update_mask: paths is empty`) 69 }) 70 71 Convey(`unsupported update mask`, func() { 72 request.UpdateMask.Paths = []string{"name"} 73 err := validateUpdateInvocationRequest(request, now) 74 So(err, ShouldErrLike, `update_mask: unsupported path "name"`) 75 }) 76 77 Convey(`deadline`, func() { 78 request.UpdateMask.Paths = []string{"deadline"} 79 80 Convey(`invalid`, func() { 81 deadline := pbutil.MustTimestampProto(now.Add(-time.Hour)) 82 request.Invocation.Deadline = deadline 83 err := validateUpdateInvocationRequest(request, now) 84 So(err, ShouldErrLike, `invocation: deadline: must be at least 10 seconds in the future`) 85 }) 86 87 Convey(`valid`, func() { 88 deadline := pbutil.MustTimestampProto(now.Add(time.Hour)) 89 request.Invocation.Deadline = deadline 90 err := validateUpdateInvocationRequest(request, now) 91 So(err, ShouldBeNil) 92 }) 93 }) 94 Convey(`bigquery exports`, func() { 95 request.UpdateMask = &field_mask.FieldMask{Paths: []string{"bigquery_exports"}} 96 97 Convey(`invalid`, func() { 98 request.Invocation.BigqueryExports = []*pb.BigQueryExport{{ 99 Project: "project", 100 Dataset: "dataset", 101 Table: "table", 102 // No ResultType. 103 }} 104 request.UpdateMask.Paths = []string{"bigquery_exports"} 105 err := validateUpdateInvocationRequest(request, now) 106 So(err, ShouldErrLike, `invocation: bigquery_exports[0]: result_type: unspecified`) 107 }) 108 109 Convey(`valid`, func() { 110 request.Invocation.BigqueryExports = []*pb.BigQueryExport{{ 111 Project: "project", 112 Dataset: "dataset", 113 Table: "table", 114 ResultType: &pb.BigQueryExport_TestResults_{ 115 TestResults: &pb.BigQueryExport_TestResults{}, 116 }, 117 }} 118 err := validateUpdateInvocationRequest(request, now) 119 So(err, ShouldBeNil) 120 }) 121 122 Convey(`empty`, func() { 123 request.Invocation.BigqueryExports = []*pb.BigQueryExport{} 124 err := validateUpdateInvocationRequest(request, now) 125 So(err, ShouldBeNil) 126 }) 127 }) 128 Convey(`properties`, func() { 129 request.UpdateMask.Paths = []string{"properties"} 130 131 Convey(`invalid`, func() { 132 request.Invocation.Properties = &structpb.Struct{ 133 Fields: map[string]*structpb.Value{ 134 "key1": structpb.NewStringValue(strings.Repeat("1", pbutil.MaxSizeProperties)), 135 }, 136 } 137 err := validateUpdateInvocationRequest(request, now) 138 So(err, ShouldErrLike, `invocation: properties: exceeds the maximum size of`, `bytes`) 139 }) 140 Convey(`valid`, func() { 141 request.Invocation.Properties = &structpb.Struct{ 142 Fields: map[string]*structpb.Value{ 143 "key_1": structpb.NewStringValue("value_1"), 144 "key_2": structpb.NewStructValue(&structpb.Struct{ 145 Fields: map[string]*structpb.Value{ 146 "child_key": structpb.NewNumberValue(1), 147 }, 148 }), 149 }, 150 } 151 err := validateUpdateInvocationRequest(request, now) 152 So(err, ShouldBeNil) 153 }) 154 }) 155 Convey(`source spec`, func() { 156 request.UpdateMask.Paths = []string{"source_spec"} 157 158 Convey(`valid`, func() { 159 request.Invocation.SourceSpec = &pb.SourceSpec{ 160 Sources: &pb.Sources{ 161 GitilesCommit: &pb.GitilesCommit{ 162 Host: "chromium.googlesource.com", 163 Project: "infra/infra", 164 Ref: "refs/heads/main", 165 CommitHash: "1234567890abcdefabcd1234567890abcdefabcd", 166 Position: 567, 167 }, 168 Changelists: []*pb.GerritChange{ 169 { 170 Host: "chromium-review.googlesource.com", 171 Project: "infra/luci-go", 172 Change: 12345, 173 Patchset: 321, 174 }, 175 }, 176 IsDirty: true, 177 }, 178 } 179 err := validateUpdateInvocationRequest(request, now) 180 So(err, ShouldBeNil) 181 }) 182 183 Convey(`invalid source spec`, func() { 184 request.Invocation.SourceSpec = &pb.SourceSpec{ 185 Sources: &pb.Sources{ 186 GitilesCommit: &pb.GitilesCommit{}, 187 }, 188 } 189 err := validateUpdateInvocationRequest(request, now) 190 So(err, ShouldErrLike, `invocation: source_spec: sources: gitiles_commit: host: unspecified`) 191 }) 192 }) 193 Convey(`baseline_id`, func() { 194 request.UpdateMask.Paths = []string{"baseline_id"} 195 196 Convey(`valid`, func() { 197 request.Invocation.BaselineId = "try:linux-rel" 198 err := validateUpdateInvocationRequest(request, now) 199 So(err, ShouldBeNil) 200 }) 201 Convey(`empty`, func() { 202 request.Invocation.BaselineId = "" 203 err := validateUpdateInvocationRequest(request, now) 204 So(err, ShouldErrLike, `invocation: baseline_id: unspecified`) 205 }) 206 Convey(`invalid`, func() { 207 request.Invocation.BaselineId = "try/linux-rel" 208 err := validateUpdateInvocationRequest(request, now) 209 So(err, ShouldErrLike, `invocation: baseline_id: does not match`) 210 }) 211 }) 212 }) 213 } 214 215 func TestUpdateInvocation(t *testing.T) { 216 Convey(`TestUpdateInvocation`, t, func() { 217 ctx := testutil.SpannerTestContext(t) 218 start := clock.Now(ctx).UTC() 219 220 recorder := newTestRecorderServer() 221 222 token, err := generateInvocationToken(ctx, "inv") 223 So(err, ShouldBeNil) 224 ctx = metadata.NewIncomingContext(ctx, metadata.Pairs(pb.UpdateTokenMetadataKey, token)) 225 226 validDeadline := pbutil.MustTimestampProto(start.Add(day)) 227 validBigqueryExports := []*pb.BigQueryExport{ 228 { 229 Project: "project", 230 Dataset: "dataset", 231 Table: "table1", 232 ResultType: &pb.BigQueryExport_TestResults_{ 233 TestResults: &pb.BigQueryExport_TestResults{}, 234 }, 235 }, 236 { 237 Project: "project", 238 Dataset: "dataset", 239 Table: "table2", 240 ResultType: &pb.BigQueryExport_TestResults_{ 241 TestResults: &pb.BigQueryExport_TestResults{}, 242 }, 243 }, 244 } 245 246 updateMask := &field_mask.FieldMask{ 247 Paths: []string{"deadline", "bigquery_exports", "properties", "source_spec"}, 248 } 249 250 Convey(`invalid request`, func() { 251 req := &pb.UpdateInvocationRequest{} 252 _, err := recorder.UpdateInvocation(ctx, req) 253 So(err, ShouldHaveAppStatus, codes.InvalidArgument, `bad request: invocation: name: unspecified`) 254 }) 255 256 Convey(`no invocation`, func() { 257 req := &pb.UpdateInvocationRequest{ 258 Invocation: &pb.Invocation{ 259 Name: "invocations/inv", 260 Deadline: validDeadline, 261 BigqueryExports: validBigqueryExports, 262 }, 263 UpdateMask: updateMask, 264 } 265 _, err := recorder.UpdateInvocation(ctx, req) 266 So(err, ShouldHaveAppStatus, codes.NotFound, `invocations/inv not found`) 267 }) 268 269 // Insert the invocation. 270 testutil.MustApply(ctx, insert.Invocation("inv", pb.Invocation_ACTIVE, nil)) 271 updateMask.Paths = append(updateMask.Paths, "baseline_id") 272 273 Convey("e2e no baseline permissions", func() { 274 req := &pb.UpdateInvocationRequest{ 275 Invocation: &pb.Invocation{ 276 Name: "invocations/inv", 277 BaselineId: "try:linux-rel", 278 }, 279 UpdateMask: &field_mask.FieldMask{Paths: []string{"baseline_id"}}, 280 } 281 282 inv, err := recorder.UpdateInvocation(ctx, req) 283 So(err, ShouldBeNil) 284 // the request does not permissions, so baseline should not be set and 285 // silently ignored. 286 So(inv.BaselineId, ShouldEqual, "") 287 }) 288 289 Convey("e2e", func() { 290 ctx = auth.WithState(ctx, &authtest.FakeState{ 291 Identity: "user:someone@example.com", 292 IdentityPermissions: []authtest.RealmPermission{ 293 // permission required to set baseline 294 {Realm: "testproject:testrealm", Permission: permPutBaseline}, 295 }, 296 }) 297 req := &pb.UpdateInvocationRequest{ 298 Invocation: &pb.Invocation{ 299 Name: "invocations/inv", 300 Deadline: validDeadline, 301 BigqueryExports: validBigqueryExports, 302 Properties: testutil.TestProperties(), 303 SourceSpec: &pb.SourceSpec{ 304 Sources: testutil.TestSourcesWithChangelistNumbers(431, 123), 305 }, 306 BaselineId: "try:linux-rel", 307 }, 308 UpdateMask: updateMask, 309 } 310 inv, err := recorder.UpdateInvocation(ctx, req) 311 So(err, ShouldBeNil) 312 313 expected := &pb.Invocation{ 314 Name: "invocations/inv", 315 Deadline: validDeadline, 316 BigqueryExports: validBigqueryExports, 317 Properties: testutil.TestProperties(), 318 SourceSpec: &pb.SourceSpec{ 319 // The invocation should be stored and returned 320 // normalized. 321 Sources: testutil.TestSourcesWithChangelistNumbers(123, 431), 322 }, 323 BaselineId: "try:linux-rel", 324 } 325 So(inv.Name, ShouldEqual, expected.Name) 326 So(inv.State, ShouldEqual, pb.Invocation_ACTIVE) 327 So(inv.Deadline, ShouldResembleProto, expected.Deadline) 328 So(inv.Properties, ShouldResembleProto, expected.Properties) 329 So(inv.SourceSpec, ShouldResembleProto, expected.SourceSpec) 330 So(inv.BaselineId, ShouldEqual, expected.BaselineId) 331 332 // Read from the database. 333 actual := &pb.Invocation{ 334 Name: expected.Name, 335 SourceSpec: &pb.SourceSpec{}, 336 } 337 invID := invocations.ID("inv") 338 var compressedProperties spanutil.Compressed 339 var compressedSources spanutil.Compressed 340 testutil.MustReadRow(ctx, "Invocations", invID.Key(), map[string]any{ 341 "Deadline": &actual.Deadline, 342 "BigQueryExports": &actual.BigqueryExports, 343 "Properties": &compressedProperties, 344 "Sources": &compressedSources, 345 "InheritSources": &actual.SourceSpec.Inherit, 346 "BaselineId": &actual.BaselineId, 347 }) 348 actual.Properties = &structpb.Struct{} 349 err = proto.Unmarshal(compressedProperties, actual.Properties) 350 So(err, ShouldBeNil) 351 actual.SourceSpec.Sources = &pb.Sources{} 352 err = proto.Unmarshal(compressedSources, actual.SourceSpec.Sources) 353 So(err, ShouldBeNil) 354 So(actual, ShouldResembleProto, expected) 355 }) 356 }) 357 }