go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/rpc/rules_test.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 rpc 16 17 import ( 18 "fmt" 19 "sort" 20 "strings" 21 "testing" 22 "time" 23 24 "google.golang.org/protobuf/types/known/fieldmaskpb" 25 "google.golang.org/protobuf/types/known/timestamppb" 26 27 "go.chromium.org/luci/gae/impl/memory" 28 "go.chromium.org/luci/server/auth" 29 "go.chromium.org/luci/server/auth/authtest" 30 "go.chromium.org/luci/server/secrets" 31 "go.chromium.org/luci/server/secrets/testsecrets" 32 "go.chromium.org/luci/server/span" 33 34 "go.chromium.org/luci/analysis/internal/bugs" 35 bugspb "go.chromium.org/luci/analysis/internal/bugs/proto" 36 "go.chromium.org/luci/analysis/internal/clustering" 37 "go.chromium.org/luci/analysis/internal/clustering/algorithms/testname" 38 "go.chromium.org/luci/analysis/internal/clustering/rules" 39 "go.chromium.org/luci/analysis/internal/config" 40 "go.chromium.org/luci/analysis/internal/perms" 41 "go.chromium.org/luci/analysis/internal/testutil" 42 configpb "go.chromium.org/luci/analysis/proto/config" 43 pb "go.chromium.org/luci/analysis/proto/v1" 44 45 . "github.com/smartystreets/goconvey/convey" 46 . "go.chromium.org/luci/common/testing/assertions" 47 ) 48 49 func TestRules(t *testing.T) { 50 Convey("With Server", t, func() { 51 ctx := testutil.IntegrationTestContext(t) 52 53 // For user identification. 54 ctx = authtest.MockAuthConfig(ctx) 55 authState := &authtest.FakeState{ 56 Identity: "user:someone@example.com", 57 IdentityGroups: []string{luciAnalysisAccessGroup, auditUsersAccessGroup}, 58 } 59 ctx = auth.WithState(ctx, authState) 60 ctx = secrets.Use(ctx, &testsecrets.Store{}) 61 62 // Provides datastore implementation needed for project config. 63 ctx = memory.Use(ctx) 64 65 srv := NewRulesSever() 66 67 ruleManagedBuilder := rules.NewRule(0). 68 WithProject(testProject). 69 WithBug(bugs.BugID{System: "monorail", ID: "monorailproject/111"}) 70 ruleManaged := ruleManagedBuilder.Build() 71 ruleTwoProject := rules.NewRule(1). 72 WithProject(testProject). 73 WithBug(bugs.BugID{System: "monorail", ID: "monorailproject/222"}). 74 WithBugManaged(false). 75 Build() 76 ruleTwoProjectOther := rules.NewRule(2). 77 WithProject("otherproject"). 78 WithBug(bugs.BugID{System: "monorail", ID: "monorailproject/222"}). 79 Build() 80 ruleUnmanagedOther := rules.NewRule(3). 81 WithProject("otherproject"). 82 WithBug(bugs.BugID{System: "monorail", ID: "monorailproject/444"}). 83 WithBugManaged(false). 84 Build() 85 ruleManagedOther := rules.NewRule(4). 86 WithProject("otherproject"). 87 WithBug(bugs.BugID{System: "monorail", ID: "monorailproject/555"}). 88 WithBugManaged(true). 89 Build() 90 ruleBuganizer := rules.NewRule(5). 91 WithProject(testProject). 92 WithBug(bugs.BugID{System: "buganizer", ID: "666"}). 93 Build() 94 95 err := rules.SetForTesting(ctx, []*rules.Entry{ 96 ruleManaged, 97 ruleTwoProject, 98 ruleTwoProjectOther, 99 ruleUnmanagedOther, 100 ruleManagedOther, 101 ruleBuganizer, 102 }) 103 So(err, ShouldBeNil) 104 105 cfg := &configpb.ProjectConfig{ 106 BugManagement: &configpb.BugManagement{ 107 Monorail: &configpb.MonorailProject{ 108 Project: "monorailproject", 109 DisplayPrefix: "mybug.com", 110 MonorailHostname: "monorailhost.com", 111 }, 112 }, 113 } 114 err = config.SetTestProjectConfig(ctx, map[string]*configpb.ProjectConfig{ 115 "testproject": cfg, 116 }) 117 So(err, ShouldBeNil) 118 119 Convey("Unauthorised requests are rejected", func() { 120 // Ensure no access to luci-analysis-access. 121 ctx = auth.WithState(ctx, &authtest.FakeState{ 122 Identity: "user:someone@example.com", 123 // Not a member of luci-analysis-access. 124 IdentityGroups: []string{"other-group"}, 125 }) 126 127 // Make some request (the request should not matter, as 128 // a common decorator is used for all requests.) 129 request := &pb.GetRuleRequest{ 130 Name: fmt.Sprintf("projects/%s/rules/%s", ruleManaged.Project, ruleManaged.RuleID), 131 } 132 133 rule, err := srv.Get(ctx, request) 134 So(err, ShouldBeRPCPermissionDenied, "not a member of luci-analysis-access") 135 So(rule, ShouldBeNil) 136 }) 137 Convey("Get", func() { 138 authState.IdentityPermissions = []authtest.RealmPermission{ 139 { 140 Realm: "testproject:@project", 141 Permission: perms.PermGetRule, 142 }, 143 { 144 Realm: "testproject:@project", 145 Permission: perms.PermGetRuleDefinition, 146 }, 147 } 148 149 Convey("No get rule permission", func() { 150 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, perms.PermGetRule) 151 152 request := &pb.GetRuleRequest{ 153 Name: fmt.Sprintf("projects/%s/rules/%s", ruleManaged.Project, ruleManaged.RuleID), 154 } 155 156 rule, err := srv.Get(ctx, request) 157 So(err, ShouldBeRPCPermissionDenied, "caller does not have permission analysis.rules.get") 158 So(rule, ShouldBeNil) 159 }) 160 Convey("Rule exists", func() { 161 mask := ruleMask{ 162 IncludeDefinition: true, 163 IncludeAuditUsers: true, 164 } 165 Convey("Read rule with Monorail bug", func() { 166 Convey("Baseline", func() { 167 request := &pb.GetRuleRequest{ 168 Name: fmt.Sprintf("projects/%s/rules/%s", ruleManaged.Project, ruleManaged.RuleID), 169 } 170 171 rule, err := srv.Get(ctx, request) 172 So(err, ShouldBeNil) 173 So(rule, ShouldResembleProto, createRulePB(ruleManaged, cfg, mask)) 174 }) 175 Convey("Without get rule definition permission", func() { 176 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, perms.PermGetRuleDefinition) 177 178 request := &pb.GetRuleRequest{ 179 Name: fmt.Sprintf("projects/%s/rules/%s", ruleManaged.Project, ruleManaged.RuleID), 180 } 181 182 rule, err := srv.Get(ctx, request) 183 So(err, ShouldBeNil) 184 mask.IncludeDefinition = false 185 So(rule, ShouldResembleProto, createRulePB(ruleManaged, cfg, mask)) 186 }) 187 Convey("Without get rule audit users permission", func() { 188 authState.IdentityGroups = removeGroup(authState.IdentityGroups, auditUsersAccessGroup) 189 190 request := &pb.GetRuleRequest{ 191 Name: fmt.Sprintf("projects/%s/rules/%s", ruleManaged.Project, ruleManaged.RuleID), 192 } 193 194 rule, err := srv.Get(ctx, request) 195 So(err, ShouldBeNil) 196 mask.IncludeAuditUsers = false 197 So(rule, ShouldResembleProto, createRulePB(ruleManaged, cfg, mask)) 198 }) 199 }) 200 Convey("Read rule with Buganizer bug", func() { 201 request := &pb.GetRuleRequest{ 202 Name: fmt.Sprintf("projects/%s/rules/%s", ruleBuganizer.Project, ruleBuganizer.RuleID), 203 } 204 205 rule, err := srv.Get(ctx, request) 206 So(err, ShouldBeNil) 207 So(rule, ShouldResembleProto, createRulePB(ruleBuganizer, cfg, mask)) 208 }) 209 }) 210 Convey("Rule does not exist", func() { 211 ruleID := strings.Repeat("00", 16) 212 request := &pb.GetRuleRequest{ 213 Name: fmt.Sprintf("projects/%s/rules/%s", ruleManaged.Project, ruleID), 214 } 215 216 rule, err := srv.Get(ctx, request) 217 So(rule, ShouldBeNil) 218 So(err, ShouldBeRPCNotFound) 219 }) 220 }) 221 Convey("List", func() { 222 authState.IdentityPermissions = []authtest.RealmPermission{ 223 { 224 Realm: "testproject:@project", 225 Permission: perms.PermListRules, 226 }, 227 { 228 Realm: "testproject:@project", 229 Permission: perms.PermGetRuleDefinition, 230 }, 231 } 232 233 request := &pb.ListRulesRequest{ 234 Parent: fmt.Sprintf("projects/%s", testProject), 235 } 236 Convey("No list rules permission", func() { 237 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, perms.PermListRules) 238 239 response, err := srv.List(ctx, request) 240 So(err, ShouldBeRPCPermissionDenied, "caller does not have permission analysis.rules.list") 241 So(response, ShouldBeNil) 242 }) 243 Convey("Non-Empty", func() { 244 test := func(mask ruleMask, cfg *configpb.ProjectConfig) { 245 rs := []*rules.Entry{ 246 ruleManaged, 247 ruleBuganizer, 248 rules.NewRule(2).WithProject(testProject).Build(), 249 rules.NewRule(3).WithProject(testProject).Build(), 250 rules.NewRule(4).WithProject(testProject).Build(), 251 // In other project. 252 ruleManagedOther, 253 } 254 err := rules.SetForTesting(ctx, rs) 255 So(err, ShouldBeNil) 256 257 response, err := srv.List(ctx, request) 258 So(err, ShouldBeNil) 259 260 expected := &pb.ListRulesResponse{ 261 Rules: []*pb.Rule{ 262 createRulePB(rs[0], cfg, mask), 263 createRulePB(rs[1], cfg, mask), 264 createRulePB(rs[2], cfg, mask), 265 createRulePB(rs[3], cfg, mask), 266 createRulePB(rs[4], cfg, mask), 267 }, 268 } 269 sort.Slice(expected.Rules, func(i, j int) bool { 270 return expected.Rules[i].RuleId < expected.Rules[j].RuleId 271 }) 272 sort.Slice(response.Rules, func(i, j int) bool { 273 return response.Rules[i].RuleId < response.Rules[j].RuleId 274 }) 275 So(response, ShouldResembleProto, expected) 276 } 277 mask := ruleMask{ 278 IncludeDefinition: true, 279 IncludeAuditUsers: true, 280 } 281 Convey("Baseline", func() { 282 test(mask, cfg) 283 }) 284 Convey("Without get rule definition permission", func() { 285 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, perms.PermGetRuleDefinition) 286 287 mask.IncludeDefinition = false 288 test(mask, cfg) 289 }) 290 Convey("Without get rule audit users permission", func() { 291 authState.IdentityGroups = removeGroup(authState.IdentityGroups, auditUsersAccessGroup) 292 293 mask.IncludeAuditUsers = false 294 test(mask, cfg) 295 }) 296 Convey("With project not configured", func() { 297 err = config.SetTestProjectConfig(ctx, map[string]*configpb.ProjectConfig{}) 298 299 test(mask, config.NewEmptyProject()) 300 }) 301 }) 302 Convey("Empty", func() { 303 err := rules.SetForTesting(ctx, nil) 304 So(err, ShouldBeNil) 305 306 response, err := srv.List(ctx, request) 307 So(err, ShouldBeNil) 308 309 expected := &pb.ListRulesResponse{} 310 So(response, ShouldResembleProto, expected) 311 }) 312 }) 313 Convey("Update", func() { 314 authState.IdentityPermissions = []authtest.RealmPermission{ 315 { 316 Realm: "testproject:@project", 317 Permission: perms.PermUpdateRule, 318 }, 319 { 320 Realm: "testproject:@project", 321 Permission: perms.PermGetRuleDefinition, 322 }, 323 } 324 request := &pb.UpdateRuleRequest{ 325 Rule: &pb.Rule{ 326 Name: fmt.Sprintf("projects/%s/rules/%s", ruleManaged.Project, ruleManaged.RuleID), 327 RuleDefinition: `test = "updated"`, 328 Bug: &pb.AssociatedBug{ 329 System: "monorail", 330 Id: "monorailproject/2", 331 }, 332 IsManagingBug: false, 333 IsManagingBugPriority: false, 334 IsActive: false, 335 }, 336 UpdateMask: &fieldmaskpb.FieldMask{ 337 // On the client side, we use JSON equivalents, i.e. ruleDefinition, 338 // bug, isActive, isManagingBug. The pRPC layer handles the 339 // translation before the request hits our service. 340 Paths: []string{"rule_definition", "bug", "is_active", "is_managing_bug", "is_managing_bug_priority"}, 341 }, 342 Etag: ruleETag(ruleManaged, ruleMask{IncludeDefinition: true, IncludeAuditUsers: false}), 343 } 344 345 Convey("No update rules permission", func() { 346 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, perms.PermUpdateRule) 347 348 rule, err := srv.Update(ctx, request) 349 So(err, ShouldBeRPCPermissionDenied, "caller does not have permission analysis.rules.update") 350 So(rule, ShouldBeNil) 351 }) 352 Convey("No rule definition permission", func() { 353 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, perms.PermGetRuleDefinition) 354 355 rule, err := srv.Update(ctx, request) 356 So(err, ShouldBeRPCPermissionDenied, "caller does not have permission analysis.rules.getDefinition") 357 So(rule, ShouldBeNil) 358 }) 359 Convey("Success", func() { 360 Convey("Predicate updated", func() { 361 // The requested updates should be applied. 362 expectedRule := ruleManagedBuilder.Build().Clone() 363 expectedRule.RuleDefinition = `test = "updated"` 364 expectedRule.BugID = bugs.BugID{System: "monorail", ID: "monorailproject/2"} 365 expectedRule.IsActive = false 366 expectedRule.IsManagingBug = false 367 expectedRule.IsManagingBugPriority = false 368 369 // The notification flags should be reset as the bug was changed. 370 expectedRule.BugManagementState.RuleAssociationNotified = false 371 for _, policyState := range expectedRule.BugManagementState.PolicyState { 372 policyState.ActivationNotified = false 373 } 374 375 Convey("baseline", func() { 376 // Act 377 rule, err := srv.Update(ctx, request) 378 379 // Verify 380 So(err, ShouldBeNil) 381 382 storedRule, err := rules.Read(span.Single(ctx), testProject, ruleManaged.RuleID) 383 So(err, ShouldBeNil) 384 So(storedRule.LastUpdateTime, ShouldNotEqual, ruleManaged.LastUpdateTime) 385 386 // Accept the new last update time (this value is set non-deterministically). 387 expectedRule.LastUpdateTime = storedRule.LastUpdateTime 388 expectedRule.LastAuditableUpdateTime = storedRule.LastUpdateTime 389 expectedRule.IsManagingBugPriorityLastUpdateTime = storedRule.LastUpdateTime 390 expectedRule.PredicateLastUpdateTime = storedRule.LastUpdateTime 391 expectedRule.LastAuditableUpdateUser = "someone@example.com" 392 393 // Verify the rule was updated as expected. 394 So(storedRule, ShouldResembleProto, expectedRule) 395 396 // Verify the returned rule matches what was expected. 397 mask := ruleMask{IncludeDefinition: true, IncludeAuditUsers: true} 398 So(rule, ShouldResembleProto, createRulePB(expectedRule, cfg, mask)) 399 }) 400 Convey("No audit users permission", func() { 401 authState.IdentityGroups = removeGroup(authState.IdentityGroups, auditUsersAccessGroup) 402 403 // Act 404 rule, err := srv.Update(ctx, request) 405 406 // Verify 407 So(err, ShouldBeNil) 408 409 storedRule, err := rules.Read(span.Single(ctx), testProject, ruleManaged.RuleID) 410 So(err, ShouldBeNil) 411 So(storedRule.LastUpdateTime, ShouldNotEqual, ruleManaged.LastUpdateTime) 412 413 // Accept the new last update time (this value is set non-deterministically). 414 expectedRule.LastUpdateTime = storedRule.LastUpdateTime 415 expectedRule.LastAuditableUpdateTime = storedRule.LastUpdateTime 416 expectedRule.PredicateLastUpdateTime = storedRule.LastUpdateTime 417 expectedRule.IsManagingBugPriorityLastUpdateTime = storedRule.LastUpdateTime 418 expectedRule.LastAuditableUpdateUser = "someone@example.com" 419 420 So(storedRule, ShouldResembleProto, expectedRule) 421 422 // Verify audit users are omitted from the result. 423 mask := ruleMask{IncludeDefinition: true, IncludeAuditUsers: false} 424 So(rule, ShouldResembleProto, createRulePB(expectedRule, cfg, mask)) 425 }) 426 }) 427 Convey("Predicate not updated", func() { 428 request.UpdateMask.Paths = []string{"bug"} 429 request.Rule.Bug = &pb.AssociatedBug{ 430 System: "buganizer", 431 Id: "99999999", 432 } 433 434 rule, err := srv.Update(ctx, request) 435 So(err, ShouldBeNil) 436 437 storedRule, err := rules.Read(span.Single(ctx), testProject, ruleManaged.RuleID) 438 So(err, ShouldBeNil) 439 440 // Check the rule was updated, but that predicate last 441 // updated time was NOT updated. 442 So(storedRule.LastUpdateTime, ShouldNotEqual, ruleManaged.LastUpdateTime) 443 444 // Verify the rule was correctly updated in the database: 445 // - The requested updates should be applied. 446 expectedRule := ruleManagedBuilder.Build() 447 expectedRule.BugID = bugs.BugID{System: "buganizer", ID: "99999999"} 448 449 // - The notification flags should be reset as the bug was changed. 450 expectedRule.BugManagementState.RuleAssociationNotified = false 451 for _, policyState := range expectedRule.BugManagementState.PolicyState { 452 policyState.ActivationNotified = false 453 } 454 455 // - Accept the new last update time (this value is set non-deterministically). 456 expectedRule.LastUpdateTime = storedRule.LastUpdateTime 457 expectedRule.LastAuditableUpdateTime = storedRule.LastUpdateTime 458 expectedRule.LastAuditableUpdateUser = "someone@example.com" 459 460 So(storedRule, ShouldResembleProto, expectedRule) 461 462 // Verify the returned rule matches what was expected. 463 mask := ruleMask{IncludeDefinition: true, IncludeAuditUsers: true} 464 So(rule, ShouldResembleProto, createRulePB(expectedRule, cfg, mask)) 465 }) 466 Convey("Managing bug priority updated", func() { 467 request.UpdateMask.Paths = []string{"is_managing_bug_priority"} 468 request.Rule.IsManagingBugPriority = false 469 470 rule, err := srv.Update(ctx, request) 471 So(err, ShouldBeNil) 472 473 storedRule, err := rules.Read(span.Single(ctx), testProject, ruleManaged.RuleID) 474 So(err, ShouldBeNil) 475 476 // Check the rule was updated. 477 So(storedRule.LastUpdateTime, ShouldNotEqual, ruleManaged.LastUpdateTime) 478 479 // Verify the rule was correctly updated in the database: 480 // - The requested update should be applied. 481 expectedRule := ruleManagedBuilder.Build() 482 expectedRule.IsManagingBugPriority = false 483 484 // - Accept the new last update time (this value is set non-deterministically). 485 expectedRule.IsManagingBugPriorityLastUpdateTime = storedRule.LastUpdateTime 486 expectedRule.LastUpdateTime = storedRule.LastUpdateTime 487 expectedRule.LastAuditableUpdateTime = storedRule.LastUpdateTime 488 expectedRule.LastAuditableUpdateUser = "someone@example.com" 489 490 So(storedRule, ShouldResembleProto, expectedRule) 491 492 // Verify the returned rule matches what was expected. 493 mask := ruleMask{IncludeDefinition: true, IncludeAuditUsers: true} 494 So(rule, ShouldResembleProto, createRulePB(expectedRule, cfg, mask)) 495 }) 496 Convey("Re-use of bug managed by another project", func() { 497 request.UpdateMask.Paths = []string{"bug"} 498 request.Rule.Bug = &pb.AssociatedBug{ 499 System: ruleManagedOther.BugID.System, 500 Id: ruleManagedOther.BugID.ID, 501 } 502 503 rule, err := srv.Update(ctx, request) 504 So(err, ShouldBeNil) 505 506 storedRule, err := rules.Read(span.Single(ctx), testProject, ruleManaged.RuleID) 507 So(err, ShouldBeNil) 508 509 // Check the rule was updated. 510 So(storedRule.LastUpdateTime, ShouldNotEqual, ruleManaged.LastUpdateTime) 511 512 // Verify the rule was correctly updated in the database: 513 // - The requested update should be applied. 514 expectedRule := ruleManagedBuilder.Build() 515 expectedRule.BugID = ruleManagedOther.BugID 516 517 // - The notification flags should be reset as the bug was changed. 518 expectedRule.BugManagementState.RuleAssociationNotified = false 519 for _, policyState := range expectedRule.BugManagementState.PolicyState { 520 policyState.ActivationNotified = false 521 } 522 523 // - IsManagingBug should be silently set to false, because ruleManagedOther 524 // already controls the bug. 525 expectedRule.IsManagingBug = false 526 527 // - Accept the new last update time (this value is set non-deterministically). 528 expectedRule.LastUpdateTime = storedRule.LastUpdateTime 529 expectedRule.LastAuditableUpdateTime = storedRule.LastUpdateTime 530 expectedRule.LastAuditableUpdateUser = "someone@example.com" 531 532 So(storedRule, ShouldResembleProto, expectedRule) 533 534 // Verify the returned rule matches what was expected. 535 mask := ruleMask{IncludeDefinition: true, IncludeAuditUsers: true} 536 So(rule, ShouldResembleProto, createRulePB(expectedRule, cfg, mask)) 537 }) 538 }) 539 Convey("Concurrent Modification", func() { 540 _, err := srv.Update(ctx, request) 541 So(err, ShouldBeNil) 542 543 // Attempt the same modification again without 544 // requerying. 545 rule, err := srv.Update(ctx, request) 546 So(rule, ShouldBeNil) 547 So(err, ShouldBeRPCAborted) 548 }) 549 Convey("Rule does not exist", func() { 550 ruleID := strings.Repeat("00", 16) 551 request.Rule.Name = fmt.Sprintf("projects/%s/rules/%s", testProject, ruleID) 552 553 rule, err := srv.Update(ctx, request) 554 So(rule, ShouldBeNil) 555 So(err, ShouldBeRPCNotFound) 556 }) 557 Convey("Validation error", func() { 558 Convey("Invalid bug monorail project", func() { 559 request.Rule.Bug.Id = "otherproject/2" 560 561 rule, err := srv.Update(ctx, request) 562 So(rule, ShouldBeNil) 563 So(err, ShouldBeRPCInvalidArgument, "bug not in expected monorail project (monorailproject)") 564 }) 565 Convey("Monorail bug on project that does not support it", func() { 566 cfg.BugManagement.Monorail = nil 567 568 err = config.SetTestProjectConfig(ctx, map[string]*configpb.ProjectConfig{ 569 "testproject": cfg, 570 }) 571 572 rule, err := srv.Update(ctx, request) 573 So(rule, ShouldBeNil) 574 So(err, ShouldBeRPCInvalidArgument, "monorail bug system not enabled for this LUCI project") 575 }) 576 Convey("Re-use of same bug in same project", func() { 577 // Use the same bug as another rule. 578 request.Rule.Bug = &pb.AssociatedBug{ 579 System: ruleTwoProject.BugID.System, 580 Id: ruleTwoProject.BugID.ID, 581 } 582 583 rule, err := srv.Update(ctx, request) 584 So(rule, ShouldBeNil) 585 So(err, ShouldBeRPCInvalidArgument, 586 fmt.Sprintf("bug already used by a rule in the same project (%s/%s)", 587 ruleTwoProject.Project, ruleTwoProject.RuleID)) 588 }) 589 Convey("Bug managed by another rule", func() { 590 // Select a bug already managed by another rule. 591 request.Rule.Bug = &pb.AssociatedBug{ 592 System: ruleManagedOther.BugID.System, 593 Id: ruleManagedOther.BugID.ID, 594 } 595 // Request we manage this bug. 596 request.Rule.IsManagingBug = true 597 request.UpdateMask.Paths = []string{"bug", "is_managing_bug"} 598 599 rule, err := srv.Update(ctx, request) 600 So(rule, ShouldBeNil) 601 So(err, ShouldBeRPCInvalidArgument, 602 fmt.Sprintf("bug already managed by a rule in another project (%s/%s)", 603 ruleManagedOther.Project, ruleManagedOther.RuleID)) 604 }) 605 Convey("Invalid rule definition", func() { 606 // Use an invalid failure association rule. 607 request.Rule.RuleDefinition = "" 608 609 rule, err := srv.Update(ctx, request) 610 So(rule, ShouldBeNil) 611 So(err, ShouldBeRPCInvalidArgument, `rule definition: syntax error: 1:1: unexpected token "<EOF>"`) 612 }) 613 }) 614 }) 615 Convey("Create", func() { 616 authState.IdentityPermissions = []authtest.RealmPermission{ 617 { 618 Realm: "testproject:@project", 619 Permission: perms.PermCreateRule, 620 }, 621 { 622 Realm: "testproject:@project", 623 Permission: perms.PermGetRuleDefinition, 624 }, 625 } 626 request := &pb.CreateRuleRequest{ 627 Parent: fmt.Sprintf("projects/%s", testProject), 628 Rule: &pb.Rule{ 629 RuleDefinition: `test = "create"`, 630 Bug: &pb.AssociatedBug{ 631 System: "monorail", 632 Id: "monorailproject/2", 633 }, 634 IsActive: false, 635 IsManagingBug: true, 636 IsManagingBugPriority: true, 637 SourceCluster: &pb.ClusterId{ 638 Algorithm: testname.AlgorithmName, 639 Id: strings.Repeat("aa", 16), 640 }, 641 }, 642 } 643 644 Convey("No create rule permission", func() { 645 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, perms.PermCreateRule) 646 647 rule, err := srv.Create(ctx, request) 648 So(err, ShouldBeRPCPermissionDenied, "caller does not have permission analysis.rules.create") 649 So(rule, ShouldBeNil) 650 }) 651 Convey("No create rule definition permission", func() { 652 authState.IdentityPermissions = removePermission(authState.IdentityPermissions, perms.PermGetRuleDefinition) 653 654 rule, err := srv.Create(ctx, request) 655 So(err, ShouldBeRPCPermissionDenied, "caller does not have permission analysis.rules.getDefinition") 656 So(rule, ShouldBeNil) 657 }) 658 Convey("Success", func() { 659 expectedRuleBuilder := rules.NewRule(0). 660 WithProject(testProject). 661 WithRuleDefinition(`test = "create"`). 662 WithActive(false). 663 WithBug(bugs.BugID{System: "monorail", ID: "monorailproject/2"}). 664 WithBugManaged(true). 665 WithBugPriorityManaged(true). 666 WithCreateUser("someone@example.com"). 667 WithLastAuditableUpdateUser("someone@example.com"). 668 WithSourceCluster(clustering.ClusterID{ 669 Algorithm: testname.AlgorithmName, 670 ID: strings.Repeat("aa", 16), 671 }). 672 WithBugManagementState(&bugspb.BugManagementState{}) 673 674 Convey("Bug not managed by another rule", func() { 675 // Re-use the same bug as a rule in another project, 676 // where the other rule is not managing the bug. 677 request.Rule.Bug = &pb.AssociatedBug{ 678 System: ruleUnmanagedOther.BugID.System, 679 Id: ruleUnmanagedOther.BugID.ID, 680 } 681 682 rule, err := srv.Create(ctx, request) 683 So(err, ShouldBeNil) 684 685 storedRule, err := rules.Read(span.Single(ctx), testProject, rule.RuleId) 686 So(err, ShouldBeNil) 687 688 expectedRule := expectedRuleBuilder. 689 // Accept the randomly generated rule ID. 690 WithRuleID(rule.RuleId). 691 WithBug(ruleUnmanagedOther.BugID). 692 // Accept whatever CreationTime was assigned, as it 693 // is determined by Spanner commit time. 694 // Rule spanner data access code tests already validate 695 // this is populated correctly. 696 WithCreateTime(storedRule.CreateTime). 697 WithLastAuditableUpdateTime(storedRule.CreateTime). 698 WithLastUpdateTime(storedRule.CreateTime). 699 WithPredicateLastUpdateTime(storedRule.CreateTime). 700 WithBugPriorityManagedLastUpdateTime(storedRule.CreateTime). 701 Build() 702 703 // Verify the rule was correctly created in the database. 704 So(storedRule, ShouldResembleProto, expectedRuleBuilder.Build()) 705 706 // Verify the returned rule matches our expectations. 707 mask := ruleMask{IncludeDefinition: true, IncludeAuditUsers: true} 708 So(rule, ShouldResembleProto, createRulePB(expectedRule, cfg, mask)) 709 }) 710 Convey("Bug managed by another rule", func() { 711 // Re-use the same bug as a rule in another project, 712 // where that rule is managing the bug. 713 request.Rule.Bug = &pb.AssociatedBug{ 714 System: ruleManagedOther.BugID.System, 715 Id: ruleManagedOther.BugID.ID, 716 } 717 718 rule, err := srv.Create(ctx, request) 719 So(err, ShouldBeNil) 720 721 storedRule, err := rules.Read(span.Single(ctx), testProject, rule.RuleId) 722 So(err, ShouldBeNil) 723 724 expectedRule := expectedRuleBuilder. 725 // Accept the randomly generated rule ID. 726 WithRuleID(rule.RuleId). 727 WithBug(ruleManagedOther.BugID). 728 // Because another rule is managing the bug, this rule 729 // should be silenlty stopped from managing the bug. 730 WithBugManaged(false). 731 // Accept whatever CreationTime was assigned. 732 WithCreateTime(storedRule.CreateTime). 733 WithLastAuditableUpdateTime(storedRule.CreateTime). 734 WithLastUpdateTime(storedRule.CreateTime). 735 WithPredicateLastUpdateTime(storedRule.CreateTime). 736 WithBugPriorityManagedLastUpdateTime(storedRule.CreateTime). 737 Build() 738 739 // Verify the rule was correctly created in the database. 740 So(storedRule, ShouldResembleProto, expectedRuleBuilder.Build()) 741 742 // Verify the returned rule matches our expectations. 743 mask := ruleMask{IncludeDefinition: true, IncludeAuditUsers: true} 744 So(rule, ShouldResembleProto, createRulePB(expectedRule, cfg, mask)) 745 }) 746 Convey("Buganizer", func() { 747 request.Rule.Bug = &pb.AssociatedBug{ 748 System: "buganizer", 749 Id: "1111111111", 750 } 751 752 rule, err := srv.Create(ctx, request) 753 So(err, ShouldBeNil) 754 755 storedRule, err := rules.Read(span.Single(ctx), testProject, rule.RuleId) 756 So(err, ShouldBeNil) 757 758 expectedRule := expectedRuleBuilder. 759 // Accept the randomly generated rule ID. 760 WithRuleID(rule.RuleId). 761 WithBug(bugs.BugID{System: "buganizer", ID: "1111111111"}). 762 // Accept whatever CreationTime was assigned, as it 763 // is determined by Spanner commit time. 764 // Rule spanner data access code tests already validate 765 // this is populated correctly. 766 WithCreateTime(storedRule.CreateTime). 767 WithLastAuditableUpdateTime(storedRule.CreateTime). 768 WithLastUpdateTime(storedRule.CreateTime). 769 WithPredicateLastUpdateTime(storedRule.CreateTime). 770 WithBugPriorityManagedLastUpdateTime(storedRule.CreateTime). 771 Build() 772 773 // Verify the rule was correctly created in the database. 774 So(storedRule, ShouldResembleProto, expectedRuleBuilder.Build()) 775 776 // Verify the returned rule matches our expectations. 777 mask := ruleMask{IncludeDefinition: true, IncludeAuditUsers: true} 778 So(rule, ShouldResembleProto, createRulePB(expectedRule, cfg, mask)) 779 }) 780 Convey("No audit users permission", func() { 781 authState.IdentityGroups = removeGroup(authState.IdentityGroups, auditUsersAccessGroup) 782 783 rule, err := srv.Create(ctx, request) 784 So(err, ShouldBeNil) 785 786 storedRule, err := rules.Read(span.Single(ctx), testProject, rule.RuleId) 787 So(err, ShouldBeNil) 788 789 expectedRule := expectedRuleBuilder. 790 // Accept the randomly generated rule ID. 791 WithRuleID(rule.RuleId). 792 // Accept whatever CreationTime was assigned, as it 793 // is determined by Spanner commit time. 794 // Rule spanner data access code tests already validate 795 // this is populated correctly. 796 WithCreateTime(storedRule.CreateTime). 797 WithLastAuditableUpdateTime(storedRule.CreateTime). 798 WithLastUpdateTime(storedRule.CreateTime). 799 WithPredicateLastUpdateTime(storedRule.CreateTime). 800 WithBugPriorityManagedLastUpdateTime(storedRule.CreateTime). 801 Build() 802 803 // Verify the rule was correctly created in the database. 804 So(storedRule, ShouldResembleProto, expectedRuleBuilder.Build()) 805 806 // Verify the returned rule matches our expectations. 807 mask := ruleMask{IncludeDefinition: true, IncludeAuditUsers: false} 808 So(rule, ShouldResembleProto, createRulePB(expectedRule, cfg, mask)) 809 }) 810 }) 811 Convey("Validation error", func() { 812 Convey("Invalid bug monorail project", func() { 813 request.Rule.Bug.Id = "otherproject/2" 814 815 rule, err := srv.Create(ctx, request) 816 So(rule, ShouldBeNil) 817 So(err, ShouldBeRPCInvalidArgument, 818 "bug not in expected monorail project (monorailproject)") 819 }) 820 Convey("Re-use of same bug in same project", func() { 821 // Use the same bug as another rule, in the same project. 822 request.Rule.Bug = &pb.AssociatedBug{ 823 System: ruleTwoProject.BugID.System, 824 Id: ruleTwoProject.BugID.ID, 825 } 826 827 rule, err := srv.Create(ctx, request) 828 So(rule, ShouldBeNil) 829 So(err, ShouldBeRPCInvalidArgument, 830 fmt.Sprintf("bug already used by a rule in the same project (%s/%s)", 831 ruleTwoProject.Project, ruleTwoProject.RuleID)) 832 }) 833 Convey("Invalid rule definition", func() { 834 // Use an invalid failure association rule. 835 request.Rule.RuleDefinition = "" 836 837 rule, err := srv.Create(ctx, request) 838 So(rule, ShouldBeNil) 839 So(err, ShouldBeRPCInvalidArgument, `rule definition: syntax error: 1:1: unexpected token "<EOF>"`) 840 }) 841 }) 842 }) 843 Convey("LookupBug", func() { 844 authState.IdentityPermissions = []authtest.RealmPermission{ 845 { 846 Realm: "testproject:@project", 847 Permission: perms.PermListRules, 848 }, 849 { 850 Realm: "otherproject:@project", 851 Permission: perms.PermListRules, 852 }, 853 } 854 855 Convey("Exists None", func() { 856 request := &pb.LookupBugRequest{ 857 System: "monorail", 858 Id: "notexists/1", 859 } 860 861 response, err := srv.LookupBug(ctx, request) 862 So(err, ShouldBeNil) 863 So(response, ShouldResembleProto, &pb.LookupBugResponse{ 864 Rules: []string{}, 865 }) 866 }) 867 Convey("Exists One", func() { 868 request := &pb.LookupBugRequest{ 869 System: ruleManaged.BugID.System, 870 Id: ruleManaged.BugID.ID, 871 } 872 873 response, err := srv.LookupBug(ctx, request) 874 So(err, ShouldBeNil) 875 So(response, ShouldResembleProto, &pb.LookupBugResponse{ 876 Rules: []string{ 877 fmt.Sprintf("projects/%s/rules/%s", 878 ruleManaged.Project, ruleManaged.RuleID), 879 }, 880 }) 881 882 Convey("If no permission in relevant project", func() { 883 authState.IdentityPermissions = nil 884 885 response, err := srv.LookupBug(ctx, request) 886 So(err, ShouldBeNil) 887 So(response, ShouldResembleProto, &pb.LookupBugResponse{ 888 Rules: []string{}, 889 }) 890 }) 891 }) 892 Convey("Exists Many", func() { 893 request := &pb.LookupBugRequest{ 894 System: ruleTwoProject.BugID.System, 895 Id: ruleTwoProject.BugID.ID, 896 } 897 898 response, err := srv.LookupBug(ctx, request) 899 So(err, ShouldBeNil) 900 So(response, ShouldResembleProto, &pb.LookupBugResponse{ 901 Rules: []string{ 902 // Rules are returned alphabetically by project. 903 fmt.Sprintf("projects/otherproject/rules/%s", ruleTwoProjectOther.RuleID), 904 fmt.Sprintf("projects/testproject/rules/%s", ruleTwoProject.RuleID), 905 }, 906 }) 907 908 Convey("If list permission exists in only some projects", func() { 909 authState.IdentityPermissions = []authtest.RealmPermission{ 910 { 911 Realm: "testproject:@project", 912 Permission: perms.PermListRules, 913 }, 914 } 915 916 response, err := srv.LookupBug(ctx, request) 917 So(err, ShouldBeNil) 918 So(response, ShouldResembleProto, &pb.LookupBugResponse{ 919 Rules: []string{ 920 fmt.Sprintf("projects/testproject/rules/%s", ruleTwoProject.RuleID), 921 }, 922 }) 923 }) 924 }) 925 }) 926 }) 927 Convey("createRulePB", t, func() { 928 // The behaviour of createRulePB is assumed by many test cases. 929 // This verifies that behaviour. 930 931 rule := rules.NewRule(1). 932 WithProject(testProject). 933 WithBug(bugs.BugID{System: "monorail", ID: "monorailproject/111"}).Build() 934 935 expectedRule := &pb.Rule{ 936 Name: "projects/testproject/rules/8109e4f8d9c218e9a2e33a7a21395455", 937 Project: "testproject", 938 RuleId: "8109e4f8d9c218e9a2e33a7a21395455", 939 RuleDefinition: "reason LIKE \"%exit code 5%\" AND test LIKE \"tast.arc.%\"", 940 IsActive: true, 941 PredicateLastUpdateTime: timestamppb.New(time.Date(1904, 4, 4, 4, 4, 4, 1, time.UTC)), 942 Bug: &pb.AssociatedBug{ 943 System: "monorail", 944 Id: "monorailproject/111", 945 LinkText: "mybug.com/111", 946 Url: "https://monorailhost.com/p/monorailproject/issues/detail?id=111", 947 }, 948 IsManagingBug: true, 949 IsManagingBugPriority: true, 950 IsManagingBugPriorityLastUpdateTime: timestamppb.New(time.Date(1905, 5, 5, 5, 5, 5, 1, time.UTC)), 951 SourceCluster: &pb.ClusterId{ 952 Algorithm: "clusteralg1-v9", 953 Id: "696431", 954 }, 955 BugManagementState: &pb.BugManagementState{ 956 PolicyState: []*pb.BugManagementState_PolicyState{ 957 { 958 PolicyId: "policy-a", 959 IsActive: true, 960 LastActivationTime: timestamppb.New(time.Date(1908, 8, 8, 8, 8, 8, 1, time.UTC)), 961 }, 962 }, 963 }, 964 CreateTime: timestamppb.New(time.Date(1900, 1, 2, 3, 4, 5, 1, time.UTC)), 965 CreateUser: "system", 966 LastAuditableUpdateTime: timestamppb.New(time.Date(1907, 7, 7, 7, 7, 7, 1, time.UTC)), //FIX ME 967 LastAuditableUpdateUser: "user@google.com", 968 LastUpdateTime: timestamppb.New(time.Date(1909, 9, 9, 9, 9, 9, 1, time.UTC)), 969 Etag: `W/"+d+u/1909-09-09T09:09:09.000000001Z"`, 970 } 971 cfg := &configpb.ProjectConfig{ 972 BugManagement: &configpb.BugManagement{ 973 Monorail: &configpb.MonorailProject{ 974 Project: "monorailproject", 975 DisplayPrefix: "mybug.com", 976 MonorailHostname: "monorailhost.com", 977 }, 978 }, 979 } 980 mask := ruleMask{ 981 IncludeDefinition: true, 982 IncludeAuditUsers: true, 983 } 984 Convey("With all fields", func() { 985 So(createRulePB(rule, cfg, mask), ShouldResembleProto, expectedRule) 986 }) 987 Convey("Without definition field", func() { 988 mask.IncludeDefinition = false 989 expectedRule.RuleDefinition = "" 990 expectedRule.Etag = `W/"+u/1909-09-09T09:09:09.000000001Z"` 991 So(createRulePB(rule, cfg, mask), ShouldResembleProto, expectedRule) 992 }) 993 Convey("Without audit users", func() { 994 mask.IncludeAuditUsers = false 995 expectedRule.CreateUser = "" 996 expectedRule.LastAuditableUpdateUser = "" 997 expectedRule.Etag = `W/"+d/1909-09-09T09:09:09.000000001Z"` 998 So(createRulePB(rule, cfg, mask), ShouldResembleProto, expectedRule) 999 }) 1000 }) 1001 Convey("isETagValid", t, func() { 1002 rule := rules.NewRule(0). 1003 WithProject(testProject). 1004 WithBug(bugs.BugID{System: "monorail", ID: "monorailproject/111"}).Build() 1005 1006 Convey("Should match ETags for same rule version", func() { 1007 masks := []ruleMask{ 1008 {IncludeDefinition: true, IncludeAuditUsers: true}, 1009 {IncludeDefinition: true, IncludeAuditUsers: false}, 1010 {IncludeDefinition: false, IncludeAuditUsers: true}, 1011 {IncludeDefinition: false, IncludeAuditUsers: false}, 1012 } 1013 for _, mask := range masks { 1014 So(isETagMatching(rule, ruleETag(rule, mask)), ShouldBeTrue) 1015 } 1016 }) 1017 Convey("Should not match ETag for different version of rule", func() { 1018 mask := ruleMask{IncludeDefinition: true, IncludeAuditUsers: true} 1019 etag := ruleETag(rule, mask) 1020 1021 rule.LastUpdateTime = time.Date(2021, 5, 4, 3, 2, 1, 0, time.UTC) 1022 So(isETagMatching(rule, etag), ShouldBeFalse) 1023 }) 1024 }) 1025 Convey("formatRule", t, func() { 1026 rule := rules.NewRule(0). 1027 WithProject(testProject). 1028 WithBug(bugs.BugID{System: "monorail", ID: "monorailproject/123456"}). 1029 WithRuleDefinition(`test = "create"`). 1030 WithActive(false). 1031 WithBugManaged(true). 1032 WithSourceCluster(clustering.ClusterID{ 1033 Algorithm: testname.AlgorithmName, 1034 ID: strings.Repeat("aa", 16), 1035 }).Build() 1036 expectedRule := `{ 1037 RuleDefinition: "test = \"create\"", 1038 BugID: "monorail:monorailproject/123456", 1039 IsActive: false, 1040 IsManagingBug: true, 1041 IsManagingBugPriority: true, 1042 SourceCluster: "testname-v4:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 1043 LastAuditableUpdate: "1907-07-07T07:07:07Z" 1044 LastUpdated: "1909-09-09T09:09:09Z" 1045 }` 1046 So(formatRule(rule), ShouldEqual, expectedRule) 1047 }) 1048 } 1049 1050 func removeGroup(groups []string, group string) []string { 1051 var result []string 1052 for _, g := range groups { 1053 if g != group { 1054 result = append(result, g) 1055 } 1056 } 1057 return result 1058 }