go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/bugs/buganizer/fake_client.go (about) 1 // Copyright 2022 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 buganizer 16 17 import ( 18 "context" 19 "fmt" 20 "strconv" 21 "strings" 22 23 "google.golang.org/api/iterator" 24 "google.golang.org/grpc/codes" 25 "google.golang.org/grpc/status" 26 "google.golang.org/protobuf/proto" 27 "google.golang.org/protobuf/types/known/timestamppb" 28 29 "go.chromium.org/luci/common/clock" 30 "go.chromium.org/luci/common/errors" 31 "go.chromium.org/luci/third_party/google.golang.org/genproto/googleapis/devtools/issuetracker/v1" 32 ) 33 34 // ComponentWithNoAccess is a componentID for which all access checks to the 35 // fake client will return false. 36 const ComponentWithNoAccess = 999999 37 38 // FakeClient is an implementation of ClientWrapperInterface that fakes the 39 // actions performed using an in-memory store. 40 type FakeClient struct { 41 FakeStore *FakeIssueStore 42 // Defines a custom error to return when attempting to create 43 // an issue comment. Use this to test failed updates. 44 CreateCommentError error 45 } 46 47 func NewFakeClient() *FakeClient { 48 issueStore := NewFakeIssueStore() 49 return &FakeClient{ 50 FakeStore: issueStore, 51 } 52 } 53 54 // Required by interface but doesn't perform any closer in the fake state. 55 func (fic *FakeClient) Close() { 56 } 57 58 func (fic *FakeClient) BatchGetIssues(ctx context.Context, in *issuetracker.BatchGetIssuesRequest) (*issuetracker.BatchGetIssuesResponse, error) { 59 issues, err := fic.FakeStore.BatchGetIssues(in.IssueIds) 60 if err != nil { 61 return nil, errors.Annotate(err, "fake batch get issues").Err() 62 } 63 return &issuetracker.BatchGetIssuesResponse{ 64 Issues: issues, 65 }, nil 66 } 67 68 func (fic *FakeClient) GetIssue(ctx context.Context, in *issuetracker.GetIssueRequest) (*issuetracker.Issue, error) { 69 issueData, err := fic.FakeStore.GetIssue(in.IssueId) 70 if err != nil { 71 return nil, errors.Annotate(err, "fake get issue").Err() 72 } 73 if issueData.ShouldReturnAccessPermissionError { 74 return nil, status.Error(codes.PermissionDenied, "cannot access bug") 75 } 76 return issueData.Issue, nil 77 } 78 79 // CreateIssue creates an issue in the in-memory store. 80 func (fic *FakeClient) CreateIssue(ctx context.Context, in *issuetracker.CreateIssueRequest) (*issuetracker.Issue, error) { 81 if in.Issue.IssueId != 0 { 82 return nil, errors.New("cannot set IssueId in CreateIssue requests") 83 } 84 // Copy the request to make sure the proto we store 85 // does not alias the request. 86 issue := proto.Clone(in.Issue).(*issuetracker.Issue) 87 88 // Move the issue description from IssueComment (the input-only field) 89 // to Description (the output-only field). 90 issue.Description = &issuetracker.IssueComment{ 91 CommentNumber: 1, 92 Comment: issue.IssueComment.Comment, 93 } 94 issue.IssueComment = nil 95 96 if in.TemplateOptions != nil && in.TemplateOptions.ApplyTemplate { 97 issue.IssueState.Ccs = []*issuetracker.User{ 98 { 99 EmailAddress: "testcc1@google.com", 100 }, 101 { 102 EmailAddress: "testcc2@google.com", 103 }, 104 } 105 } 106 107 return fic.FakeStore.StoreIssue(ctx, issue), nil 108 } 109 110 // GetAutomationAccess checks access to a ComponentID. Access is always true 111 // except for component ID ComponentWithNoAccess which is false. 112 func (fic *FakeClient) GetAutomationAccess(ctx context.Context, in *issuetracker.GetAutomationAccessRequest) (*issuetracker.GetAutomationAccessResponse, error) { 113 return &issuetracker.GetAutomationAccessResponse{ 114 HasAccess: !strings.Contains(in.ResourceName, strconv.Itoa(ComponentWithNoAccess)), 115 }, nil 116 } 117 118 // ModifyIssue modifies and issue in the in-memory store. 119 // This method handles a specific set of updates, 120 // please check the implementation and add any 121 // required field to the set of known fields. 122 func (fic *FakeClient) ModifyIssue(ctx context.Context, in *issuetracker.ModifyIssueRequest) (*issuetracker.Issue, error) { 123 issueData, err := fic.FakeStore.GetIssue(in.IssueId) 124 if err != nil { 125 return nil, errors.Annotate(err, "fake modify issue").Err() 126 } 127 if issueData.UpdateError != nil { 128 return nil, issueData.UpdateError 129 } 130 if issueData.ShouldReturnAccessPermissionError { 131 return nil, status.Error(codes.PermissionDenied, "cannot access bug") 132 } 133 issue := issueData.Issue 134 // The fields in the switch statement are the only 135 // fields supported by the method. 136 for _, addPath := range in.AddMask.Paths { 137 switch addPath { 138 case "status": 139 if issue.IssueState.Status != in.Add.Status { 140 issue.IssueState.Status = in.Add.Status 141 142 now := timestamppb.New(clock.Now(ctx)) 143 issue.ModifiedTime = now 144 if _, ok := ClosedStatuses[in.Add.Status]; ok { 145 if !issue.ResolvedTime.IsValid() { 146 // Resolved time is zero or unset. Set it. 147 issue.ResolvedTime = now 148 } 149 } else { 150 issue.ResolvedTime = nil 151 } 152 if in.Add.Status == issuetracker.Issue_VERIFIED { 153 if !issue.VerifiedTime.IsValid() { 154 // Verified time is zero or unset. Set it. 155 issue.VerifiedTime = now 156 } 157 } else { 158 issue.VerifiedTime = nil 159 } 160 } 161 case "priority": 162 if issue.IssueState.Priority != in.Add.Priority { 163 issue.IssueState.Priority = in.Add.Priority 164 issue.ModifiedTime = timestamppb.New(clock.Now(ctx)) 165 } 166 case "verifier": 167 if issue.IssueState.Verifier != in.Add.Verifier { 168 issue.IssueState.Verifier = in.Add.Verifier 169 issue.ModifiedTime = timestamppb.New(clock.Now(ctx)) 170 } 171 case "assignee": 172 if issue.IssueState.Assignee != in.Add.Assignee { 173 issue.IssueState.Assignee = in.Add.Assignee 174 issue.ModifiedTime = timestamppb.New(clock.Now(ctx)) 175 } 176 default: 177 return nil, errors.New(fmt.Sprintf("add_mask uses unsupported issue field: %s", addPath)) 178 } 179 } 180 // The fields in the switch statement are the only 181 // fields supported by the method. 182 for _, removePath := range in.RemoveMask.Paths { 183 switch removePath { 184 case "assignee": 185 if in.Remove.Assignee != nil && in.Remove.Assignee.EmailAddress == "" { 186 issue.IssueState.Assignee = nil 187 issue.ModifiedTime = timestamppb.New(clock.Now(ctx)) 188 } 189 default: 190 return nil, errors.New(fmt.Sprintf("remove_mask uses unsupported issue field: %s", removePath)) 191 } 192 } 193 194 if in.IssueComment != nil { 195 issueData.Comments = append(issueData.Comments, in.IssueComment) 196 } 197 return issue, nil 198 } 199 200 func (fic *FakeClient) ListIssueUpdates(ctx context.Context, in *issuetracker.ListIssueUpdatesRequest) IssueUpdateIterator { 201 issueUpdates, err := fic.FakeStore.ListIssueUpdates(in.IssueId) 202 if err != nil { 203 return &fakeIssueUpdateIterator{ 204 err: errors.Annotate(err, "fake list issue updates").Err(), 205 } 206 } 207 return &fakeIssueUpdateIterator{ 208 items: issueUpdates, 209 } 210 } 211 212 func (fic *FakeClient) CreateIssueComment(ctx context.Context, in *issuetracker.CreateIssueCommentRequest) (*issuetracker.IssueComment, error) { 213 if fic.CreateCommentError != nil { 214 return nil, fic.CreateCommentError 215 } 216 issueData, err := fic.FakeStore.GetIssue(in.IssueId) 217 if err != nil { 218 return nil, errors.Annotate(err, "fake create issue comment").Err() 219 } 220 in.Comment.IssueId = in.IssueId 221 issueData.Comments = append(issueData.Comments, in.Comment) 222 return in.Comment, nil 223 } 224 225 func (fic *FakeClient) UpdateIssueComment(ctx context.Context, in *issuetracker.UpdateIssueCommentRequest) (*issuetracker.IssueComment, error) { 226 issueData, err := fic.FakeStore.GetIssue(in.IssueId) 227 if err != nil { 228 return nil, errors.Annotate(err, "fake update issue comment").Err() 229 } 230 if in.CommentNumber < 1 || int(in.CommentNumber) > len(issueData.Comments) { 231 return nil, errors.New("comment number is out of bounds") 232 } 233 comment := issueData.Comments[in.CommentNumber-1] 234 comment.Comment = in.Comment.Comment 235 issueData.Issue.Description = comment 236 return comment, nil 237 } 238 239 func (fic *FakeClient) ListIssueComments(ctx context.Context, in *issuetracker.ListIssueCommentsRequest) IssueCommentIterator { 240 issueData, err := fic.FakeStore.GetIssue(in.IssueId) 241 if err != nil { 242 return &fakeIssueCommentIterator{ 243 err: errors.Annotate(err, "fake list issue comments").Err(), 244 } 245 } 246 247 return &fakeIssueCommentIterator{ 248 items: issueData.Comments, 249 } 250 } 251 252 func (fic *FakeClient) CreateHotlistEntry(ctx context.Context, in *issuetracker.CreateHotlistEntryRequest) (*issuetracker.HotlistEntry, error) { 253 err := fic.FakeStore.CreateHotlistEntry(in.HotlistEntry.IssueId, in.HotlistId) 254 if err != nil { 255 return nil, errors.Annotate(err, "fake create hotlist entry").Err() 256 } 257 return &issuetracker.HotlistEntry{ 258 IssueId: in.HotlistEntry.IssueId, 259 Position: 0, // Not currently populated by fake implementation. 260 }, nil 261 } 262 263 type fakeIssueUpdateIterator struct { 264 pointer int 265 err error 266 items []*issuetracker.IssueUpdate 267 } 268 269 func (fui *fakeIssueUpdateIterator) Next() (*issuetracker.IssueUpdate, error) { 270 if fui.err != nil { 271 return nil, fui.err 272 } 273 274 if fui.pointer == len(fui.items) { 275 fui.err = iterator.Done 276 return nil, fui.err 277 } 278 279 currentIndex := fui.pointer 280 fui.pointer++ 281 return fui.items[currentIndex], nil 282 } 283 284 type fakeIssueCommentIterator struct { 285 pointer int 286 err error 287 items []*issuetracker.IssueComment 288 } 289 290 func (fci *fakeIssueCommentIterator) Next() (*issuetracker.IssueComment, error) { 291 if fci.err != nil { 292 return nil, fci.err 293 } 294 295 if fci.pointer == len(fci.items) { 296 fci.err = iterator.Done 297 return nil, fci.err 298 } 299 currentIndex := fci.pointer 300 fci.pointer++ 301 return fci.items[currentIndex], nil 302 }