go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/services/recorder/update_included_invocations_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 "testing" 19 20 "google.golang.org/grpc/codes" 21 "google.golang.org/grpc/metadata" 22 23 "go.chromium.org/luci/server/auth" 24 "go.chromium.org/luci/server/auth/authtest" 25 26 "go.chromium.org/luci/resultdb/internal/invocations" 27 "go.chromium.org/luci/resultdb/internal/testutil" 28 "go.chromium.org/luci/resultdb/internal/testutil/insert" 29 pb "go.chromium.org/luci/resultdb/proto/v1" 30 31 . "github.com/smartystreets/goconvey/convey" 32 . "go.chromium.org/luci/common/testing/assertions" 33 ) 34 35 func TestValidateUpdateIncludedInvocationsRequest(t *testing.T) { 36 t.Parallel() 37 Convey(`TestValidateUpdateIncludedInvocationsRequest`, t, func() { 38 Convey(`Valid`, func() { 39 err := validateUpdateIncludedInvocationsRequest(&pb.UpdateIncludedInvocationsRequest{ 40 IncludingInvocation: "invocations/a", 41 AddInvocations: []string{"invocations/b"}, 42 RemoveInvocations: []string{"invocations/c"}, 43 }) 44 So(err, ShouldBeNil) 45 }) 46 47 Convey(`Invalid including_invocation`, func() { 48 err := validateUpdateIncludedInvocationsRequest(&pb.UpdateIncludedInvocationsRequest{ 49 IncludingInvocation: "x", 50 AddInvocations: []string{"invocations/b"}, 51 RemoveInvocations: []string{"invocations/c"}, 52 }) 53 So(err, ShouldErrLike, `including_invocation: does not match`) 54 }) 55 Convey(`Invalid add_invocations`, func() { 56 err := validateUpdateIncludedInvocationsRequest(&pb.UpdateIncludedInvocationsRequest{ 57 IncludingInvocation: "invocations/a", 58 AddInvocations: []string{"x"}, 59 RemoveInvocations: []string{"invocations/c"}, 60 }) 61 So(err, ShouldErrLike, `add_invocations: "x": does not match`) 62 }) 63 Convey(`Invalid remove_invocations`, func() { 64 err := validateUpdateIncludedInvocationsRequest(&pb.UpdateIncludedInvocationsRequest{ 65 IncludingInvocation: "invocations/a", 66 AddInvocations: []string{"invocations/b"}, 67 RemoveInvocations: []string{"x"}, 68 }) 69 So(err, ShouldErrLike, `remove_invocations: "x": does not match`) 70 }) 71 Convey(`Include itself`, func() { 72 err := validateUpdateIncludedInvocationsRequest(&pb.UpdateIncludedInvocationsRequest{ 73 IncludingInvocation: "invocations/a", 74 AddInvocations: []string{"invocations/a"}, 75 RemoveInvocations: []string{"invocations/c"}, 76 }) 77 So(err, ShouldErrLike, `cannot include itself`) 78 }) 79 Convey(`Add and remove same invocation`, func() { 80 err := validateUpdateIncludedInvocationsRequest(&pb.UpdateIncludedInvocationsRequest{ 81 IncludingInvocation: "invocations/a", 82 AddInvocations: []string{"invocations/b"}, 83 RemoveInvocations: []string{"invocations/b"}, 84 }) 85 So(err, ShouldErrLike, `cannot add and remove the same invocation(s) at the same time: ["invocations/b"]`) 86 }) 87 }) 88 } 89 90 func TestUpdateIncludedInvocations(t *testing.T) { 91 Convey(`TestIncludedInvocations`, t, func() { 92 ctx := auth.WithState(testutil.SpannerTestContext(t), &authtest.FakeState{ 93 Identity: "user:someone@example.com", 94 IdentityPermissions: []authtest.RealmPermission{ 95 {Realm: "testproject:testrealm", Permission: permIncludeInvocation}, 96 }, 97 }) 98 recorder := newTestRecorderServer() 99 100 token, err := generateInvocationToken(ctx, "including") 101 So(err, ShouldBeNil) 102 ctx = metadata.NewIncomingContext(ctx, metadata.Pairs(pb.UpdateTokenMetadataKey, token)) 103 104 assertIncluded := func(includedInvID invocations.ID) { 105 var throwAway invocations.ID 106 testutil.MustReadRow(ctx, "IncludedInvocations", invocations.InclusionKey("including", includedInvID), map[string]any{ 107 "IncludedInvocationID": &throwAway, 108 }) 109 } 110 assertNotIncluded := func(includedInvID invocations.ID) { 111 var throwAway invocations.ID 112 testutil.MustNotFindRow(ctx, "IncludedInvocations", invocations.InclusionKey("including", includedInvID), map[string]any{ 113 "IncludedInvocationID": &throwAway, 114 }) 115 } 116 117 Convey(`Invalid request`, func() { 118 _, err := recorder.UpdateIncludedInvocations(ctx, &pb.UpdateIncludedInvocationsRequest{}) 119 So(err, ShouldHaveAppStatus, codes.InvalidArgument, `bad request: including_invocation: unspecified`) 120 }) 121 122 Convey(`With valid request`, func() { 123 req := &pb.UpdateIncludedInvocationsRequest{ 124 IncludingInvocation: "invocations/including", 125 AddInvocations: []string{ 126 "invocations/included", 127 "invocations/included2", 128 }, 129 RemoveInvocations: []string{ 130 "invocations/toberemoved", 131 "invocations/neverexisted", 132 }, 133 } 134 135 Convey(`No including invocation`, func() { 136 testutil.MustApply(ctx, 137 insert.Invocation("included", pb.Invocation_FINALIZED, map[string]any{"Realm": "testproject:testrealm"}), 138 insert.Invocation("included2", pb.Invocation_FINALIZED, map[string]any{"Realm": "testproject:testrealm"}), 139 ) 140 _, err := recorder.UpdateIncludedInvocations(ctx, req) 141 So(err, ShouldHaveAppStatus, codes.NotFound, `invocations/including not found`) 142 }) 143 144 Convey(`With existing inclusion`, func() { 145 testutil.MustApply(ctx, 146 insert.Invocation("including", pb.Invocation_ACTIVE, nil), 147 insert.Invocation("toberemoved", pb.Invocation_FINALIZED, map[string]any{"Realm": "testproject:testrealm"}), 148 ) 149 _, err := recorder.UpdateIncludedInvocations(ctx, &pb.UpdateIncludedInvocationsRequest{ 150 IncludingInvocation: "invocations/including", 151 AddInvocations: []string{"invocations/toberemoved"}, 152 }) 153 So(err, ShouldBeNil) 154 assertIncluded("toberemoved") 155 156 Convey(`No included invocation`, func() { 157 _, err := recorder.UpdateIncludedInvocations(ctx, req) 158 So(err, ShouldHaveAppStatus, codes.NotFound, `invocations/included`) 159 }) 160 161 Convey(`Leaking disallowed`, func() { 162 testutil.MustApply(ctx, 163 insert.Invocation("included", pb.Invocation_FINALIZED, map[string]any{"Realm": "testproject:testrealm"}), 164 insert.Invocation("included2", pb.Invocation_FINALIZED, map[string]any{"Realm": "testproject:secretrealm"}), 165 ) 166 167 _, err := recorder.UpdateIncludedInvocations(ctx, req) 168 So(err, ShouldHaveAppStatus, codes.PermissionDenied, `caller does not have permission resultdb.invocations.include in realm of invocation included2`) 169 assertNotIncluded("included2") 170 }) 171 172 Convey(`Success - idempotent`, func() { 173 testutil.MustApply(ctx, 174 insert.Invocation("included", pb.Invocation_FINALIZED, map[string]any{"Realm": "testproject:testrealm"}), 175 insert.Invocation("included2", pb.Invocation_FINALIZED, map[string]any{"Realm": "testproject:testrealm"}), 176 ) 177 178 _, err := recorder.UpdateIncludedInvocations(ctx, req) 179 So(err, ShouldBeNil) 180 assertIncluded("included") 181 assertIncluded("included2") 182 assertNotIncluded("toberemoved") 183 184 _, err = recorder.UpdateIncludedInvocations(ctx, req) 185 So(err, ShouldBeNil) 186 assertIncluded("included") 187 assertIncluded("included2") 188 assertNotIncluded("toberemoved") 189 }) 190 }) 191 }) 192 }) 193 }