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  }