blob: be3a28b3d5f0acd1a0ac9ebe4ff9ac118a7e82f0 [file] [log] [blame]
// Copyright 2022 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rpc
import (
"fmt"
"sort"
"strings"
"testing"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
"go.chromium.org/luci/gae/impl/memory"
"go.chromium.org/luci/server/auth"
"go.chromium.org/luci/server/auth/authtest"
"go.chromium.org/luci/server/secrets"
"go.chromium.org/luci/server/secrets/testsecrets"
"go.chromium.org/luci/server/span"
"google.golang.org/protobuf/types/known/fieldmaskpb"
"google.golang.org/protobuf/types/known/timestamppb"
"go.chromium.org/luci/analysis/internal/bugs"
"go.chromium.org/luci/analysis/internal/clustering"
"go.chromium.org/luci/analysis/internal/clustering/algorithms/testname"
"go.chromium.org/luci/analysis/internal/clustering/rules"
"go.chromium.org/luci/analysis/internal/config"
"go.chromium.org/luci/analysis/internal/perms"
"go.chromium.org/luci/analysis/internal/testutil"
configpb "go.chromium.org/luci/analysis/proto/config"
pb "go.chromium.org/luci/analysis/proto/v1"
)
func TestRules(t *testing.T) {
Convey("With Server", t, func() {
ctx := testutil.IntegrationTestContext(t)
// For user identification.
ctx = authtest.MockAuthConfig(ctx)
authState := &authtest.FakeState{
Identity: "user:someone@example.com",
IdentityGroups: []string{"luci-analysis-access"},
}
ctx = auth.WithState(ctx, authState)
ctx = secrets.Use(ctx, &testsecrets.Store{})
// Provides datastore implementation needed for project config.
ctx = memory.Use(ctx)
srv := NewRulesSever()
ruleManagedBuilder := rules.NewRule(0).
WithProject(testProject).
WithBug(bugs.BugID{System: "monorail", ID: "monorailproject/111"})
ruleManaged := ruleManagedBuilder.Build()
ruleTwoProject := rules.NewRule(1).
WithProject(testProject).
WithBug(bugs.BugID{System: "monorail", ID: "monorailproject/222"}).
WithBugManaged(false).
Build()
ruleTwoProjectOther := rules.NewRule(2).
WithProject("otherproject").
WithBug(bugs.BugID{System: "monorail", ID: "monorailproject/222"}).
Build()
ruleUnmanagedOther := rules.NewRule(3).
WithProject("otherproject").
WithBug(bugs.BugID{System: "monorail", ID: "monorailproject/444"}).
WithBugManaged(false).
Build()
ruleManagedOther := rules.NewRule(4).
WithProject("otherproject").
WithBug(bugs.BugID{System: "monorail", ID: "monorailproject/555"}).
WithBugManaged(true).
Build()
ruleBuganizer := rules.NewRule(5).
WithProject(testProject).
WithBug(bugs.BugID{System: "buganizer", ID: "666"}).
Build()
err := rules.SetRulesForTesting(ctx, []*rules.FailureAssociationRule{
ruleManaged,
ruleTwoProject,
ruleTwoProjectOther,
ruleUnmanagedOther,
ruleManagedOther,
ruleBuganizer,
})
So(err, ShouldBeNil)
cfg := &configpb.ProjectConfig{
Monorail: &configpb.MonorailProject{
Project: "monorailproject",
DisplayPrefix: "mybug.com",
MonorailHostname: "monorailhost.com",
},
}
err = config.SetTestProjectConfig(ctx, map[string]*configpb.ProjectConfig{
"testproject": cfg,
})
So(err, ShouldBeNil)
Convey("Unauthorised requests are rejected", func() {
// Ensure no access to luci-analysis-access.
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: "user:someone@example.com",
// Not a member of luci-analysis-access.
IdentityGroups: []string{"other-group"},
})
// Make some request (the request should not matter, as
// a common decorator is used for all requests.)
request := &pb.GetRuleRequest{
Name: fmt.Sprintf("projects/%s/rules/%s", ruleManaged.Project, ruleManaged.RuleID),
}
rule, err := srv.Get(ctx, request)
So(err, ShouldBeRPCPermissionDenied, "not a member of luci-analysis-access")
So(rule, ShouldBeNil)
})
Convey("Get", func() {
authState.IdentityPermissions = []authtest.RealmPermission{
{
Realm: "testproject:@root",
Permission: perms.PermGetRule,
},
{
Realm: "testproject:@root",
Permission: perms.PermGetRuleDefinition,
},
}
Convey("No get rule permission", func() {
authState.IdentityPermissions = removePermission(authState.IdentityPermissions, perms.PermGetRule)
request := &pb.GetRuleRequest{
Name: fmt.Sprintf("projects/%s/rules/%s", ruleManaged.Project, ruleManaged.RuleID),
}
rule, err := srv.Get(ctx, request)
So(err, ShouldBeRPCPermissionDenied, "caller does not have permission analysis.rules.get")
So(rule, ShouldBeNil)
})
Convey("Rule exists", func() {
Convey("Read rule with Monorail bug", func() {
expectedRule := &pb.Rule{
Name: fmt.Sprintf("projects/%s/rules/%s", ruleManaged.Project, ruleManaged.RuleID),
Project: ruleManaged.Project,
RuleId: ruleManaged.RuleID,
RuleDefinition: ruleManaged.RuleDefinition,
Bug: &pb.AssociatedBug{
System: "monorail",
Id: "monorailproject/111",
LinkText: "mybug.com/111",
Url: "https://monorailhost.com/p/monorailproject/issues/detail?id=111",
},
IsActive: true,
IsManagingBug: true,
IsManagingBugPriority: true,
SourceCluster: &pb.ClusterId{
Algorithm: ruleManaged.SourceCluster.Algorithm,
Id: ruleManaged.SourceCluster.ID,
},
CreateTime: timestamppb.New(ruleManaged.CreationTime),
CreateUser: ruleManaged.CreationUser,
LastUpdateTime: timestamppb.New(ruleManaged.LastUpdated),
LastUpdateUser: ruleManaged.LastUpdatedUser,
PredicateLastUpdateTime: timestamppb.New(ruleManaged.PredicateLastUpdated),
}
Convey("With get rule definition permission", func() {
request := &pb.GetRuleRequest{
Name: fmt.Sprintf("projects/%s/rules/%s", ruleManaged.Project, ruleManaged.RuleID),
}
rule, err := srv.Get(ctx, request)
So(err, ShouldBeNil)
includeDefinition := true
So(rule, ShouldResembleProto, createRulePB(ruleManaged, cfg, includeDefinition))
// Also verify createRulePB works as expected, so we do not need
// to test that again in later tests.
expectedRule.Etag = ruleETag(ruleManaged, includeDefinition)
So(rule, ShouldResembleProto, expectedRule)
})
Convey("Without get rule definition permission", func() {
authState.IdentityPermissions = removePermission(authState.IdentityPermissions, perms.PermGetRuleDefinition)
request := &pb.GetRuleRequest{
Name: fmt.Sprintf("projects/%s/rules/%s", ruleManaged.Project, ruleManaged.RuleID),
}
rule, err := srv.Get(ctx, request)
So(err, ShouldBeNil)
includeDefinition := false
So(rule, ShouldResembleProto, createRulePB(ruleManaged, cfg, includeDefinition))
// Also verify createRulePB works as expected, so we do not need
// to test that again in later tests.
expectedRule.RuleDefinition = ""
expectedRule.Etag = ruleETag(ruleManaged, includeDefinition)
So(rule, ShouldResembleProto, expectedRule)
})
})
Convey("Read rule with Buganizer bug", func() {
request := &pb.GetRuleRequest{
Name: fmt.Sprintf("projects/%s/rules/%s", ruleBuganizer.Project, ruleBuganizer.RuleID),
}
rule, err := srv.Get(ctx, request)
So(err, ShouldBeNil)
includeDefinition := true
So(rule, ShouldResembleProto, createRulePB(ruleBuganizer, cfg, includeDefinition))
// Also verify createRulePB works as expected, so we do not need
// to test that again in later tests.
So(rule, ShouldResembleProto, &pb.Rule{
Name: fmt.Sprintf("projects/%s/rules/%s", ruleBuganizer.Project, ruleBuganizer.RuleID),
Project: ruleBuganizer.Project,
RuleId: ruleBuganizer.RuleID,
RuleDefinition: ruleBuganizer.RuleDefinition,
Bug: &pb.AssociatedBug{
System: "buganizer",
Id: "666",
LinkText: "b/666",
Url: "https://issuetracker.google.com/issues/666",
},
IsActive: true,
IsManagingBug: true,
IsManagingBugPriority: true,
SourceCluster: &pb.ClusterId{
Algorithm: ruleBuganizer.SourceCluster.Algorithm,
Id: ruleBuganizer.SourceCluster.ID,
},
CreateTime: timestamppb.New(ruleBuganizer.CreationTime),
CreateUser: ruleBuganizer.CreationUser,
LastUpdateTime: timestamppb.New(ruleBuganizer.LastUpdated),
LastUpdateUser: ruleBuganizer.LastUpdatedUser,
PredicateLastUpdateTime: timestamppb.New(ruleBuganizer.PredicateLastUpdated),
Etag: ruleETag(ruleBuganizer, includeDefinition),
})
})
})
Convey("Rule does not exist", func() {
ruleID := strings.Repeat("00", 16)
request := &pb.GetRuleRequest{
Name: fmt.Sprintf("projects/%s/rules/%s", ruleManaged.Project, ruleID),
}
rule, err := srv.Get(ctx, request)
So(rule, ShouldBeNil)
So(err, ShouldBeRPCNotFound)
})
})
Convey("List", func() {
authState.IdentityPermissions = []authtest.RealmPermission{
{
Realm: "testproject:@root",
Permission: perms.PermListRules,
},
{
Realm: "testproject:@root",
Permission: perms.PermGetRuleDefinition,
},
}
request := &pb.ListRulesRequest{
Parent: fmt.Sprintf("projects/%s", testProject),
}
Convey("No list rules permission", func() {
authState.IdentityPermissions = removePermission(authState.IdentityPermissions, perms.PermListRules)
response, err := srv.List(ctx, request)
So(err, ShouldBeRPCPermissionDenied, "caller does not have permission analysis.rules.list")
So(response, ShouldBeNil)
})
Convey("Non-Empty", func() {
test := func(includeDefinition bool) {
rs := []*rules.FailureAssociationRule{
ruleManaged,
ruleBuganizer,
rules.NewRule(2).WithProject(testProject).Build(),
rules.NewRule(3).WithProject(testProject).Build(),
rules.NewRule(4).WithProject(testProject).Build(),
// In other project.
ruleManagedOther,
}
err := rules.SetRulesForTesting(ctx, rs)
So(err, ShouldBeNil)
response, err := srv.List(ctx, request)
So(err, ShouldBeNil)
expected := &pb.ListRulesResponse{
Rules: []*pb.Rule{
createRulePB(rs[0], cfg, includeDefinition),
createRulePB(rs[1], cfg, includeDefinition),
createRulePB(rs[2], cfg, includeDefinition),
createRulePB(rs[3], cfg, includeDefinition),
createRulePB(rs[4], cfg, includeDefinition),
},
}
sort.Slice(expected.Rules, func(i, j int) bool {
return expected.Rules[i].RuleId < expected.Rules[j].RuleId
})
sort.Slice(response.Rules, func(i, j int) bool {
return response.Rules[i].RuleId < response.Rules[j].RuleId
})
So(response, ShouldResembleProto, expected)
}
Convey("With get rule definition permission", func() {
includeDefinition := true
test(includeDefinition)
})
Convey("Without get rule definition permission", func() {
authState.IdentityPermissions = removePermission(authState.IdentityPermissions, perms.PermGetRuleDefinition)
includeDefinition := false
test(includeDefinition)
})
})
Convey("Empty", func() {
err := rules.SetRulesForTesting(ctx, nil)
So(err, ShouldBeNil)
response, err := srv.List(ctx, request)
So(err, ShouldBeNil)
expected := &pb.ListRulesResponse{}
So(response, ShouldResembleProto, expected)
})
Convey("With project not configured", func() {
err = config.SetTestProjectConfig(ctx, map[string]*configpb.ProjectConfig{})
So(err, ShouldBeNil)
// Run
response, err := srv.List(ctx, request)
// Verify
So(err, ShouldBeRPCFailedPrecondition, "project does not exist in LUCI Analysis")
So(response, ShouldBeNil)
})
})
Convey("Update", func() {
authState.IdentityPermissions = []authtest.RealmPermission{
{
Realm: "testproject:@root",
Permission: perms.PermUpdateRule,
},
{
Realm: "testproject:@root",
Permission: perms.PermGetRuleDefinition,
},
}
request := &pb.UpdateRuleRequest{
Rule: &pb.Rule{
Name: fmt.Sprintf("projects/%s/rules/%s", ruleManaged.Project, ruleManaged.RuleID),
RuleDefinition: `test = "updated"`,
Bug: &pb.AssociatedBug{
System: "monorail",
Id: "monorailproject/2",
},
IsManagingBug: false,
IsManagingBugPriority: false,
IsActive: false,
},
UpdateMask: &fieldmaskpb.FieldMask{
// On the client side, we use JSON equivalents, i.e. ruleDefinition,
// bug, isActive, isManagingBug.
Paths: []string{"rule_definition", "bug", "is_active", "is_managing_bug", "is_managing_bug_priority"},
},
Etag: ruleETag(ruleManaged, true /*includeDefinition*/),
}
Convey("No update rules permission", func() {
authState.IdentityPermissions = removePermission(authState.IdentityPermissions, perms.PermUpdateRule)
rule, err := srv.Update(ctx, request)
So(err, ShouldBeRPCPermissionDenied, "caller does not have permission analysis.rules.update")
So(rule, ShouldBeNil)
})
Convey("No rule definition permission", func() {
authState.IdentityPermissions = removePermission(authState.IdentityPermissions, perms.PermGetRuleDefinition)
rule, err := srv.Update(ctx, request)
So(err, ShouldBeRPCPermissionDenied, "caller does not have permission analysis.rules.getDefinition")
So(rule, ShouldBeNil)
})
Convey("Success", func() {
Convey("Predicate updated", func() {
rule, err := srv.Update(ctx, request)
So(err, ShouldBeNil)
storedRule, err := rules.Read(span.Single(ctx), testProject, ruleManaged.RuleID)
So(err, ShouldBeNil)
So(storedRule.LastUpdated, ShouldNotEqual, ruleManaged.LastUpdated)
expectedRule := ruleManagedBuilder.
WithRuleDefinition(`test = "updated"`).
WithBug(bugs.BugID{System: "monorail", ID: "monorailproject/2"}).
WithActive(false).
WithBugManaged(false).
WithBugPriorityManaged(false).
// Accept whatever the new last updated time is.
WithLastUpdated(storedRule.LastUpdated).
WithLastUpdatedUser("someone@example.com").
// The predicate last updated time should be the same as
// the last updated time.
WithPredicateLastUpdated(storedRule.LastUpdated).
Build()
// Verify the rule was correctly updated in the database.
So(storedRule, ShouldResemble, expectedRule)
// Verify the returned rule matches what was expected.
So(rule, ShouldResembleProto, createRulePB(expectedRule, cfg, true /*includeDefinition*/))
})
Convey("Predicate not updated", func() {
request.UpdateMask.Paths = []string{"bug"}
request.Rule.Bug = &pb.AssociatedBug{
System: "buganizer",
Id: "99999999",
}
rule, err := srv.Update(ctx, request)
So(err, ShouldBeNil)
storedRule, err := rules.Read(span.Single(ctx), testProject, ruleManaged.RuleID)
So(err, ShouldBeNil)
// Check the rule was updated, but that predicate last
// updated time was NOT updated.
So(storedRule.LastUpdated, ShouldNotEqual, ruleManaged.LastUpdated)
expectedRule := ruleManagedBuilder.
WithBug(bugs.BugID{System: "buganizer", ID: "99999999"}).
// Accept whatever the new last updated time is.
WithLastUpdated(storedRule.LastUpdated).
WithLastUpdatedUser("someone@example.com").
Build()
// Verify the rule was correctly updated in the database.
So(storedRule, ShouldResemble, expectedRule)
// Verify the returned rule matches what was expected.
So(rule, ShouldResembleProto, createRulePB(expectedRule, cfg, true /*includeDefinition*/))
})
Convey("Re-use of bug managed by another project", func() {
request.UpdateMask.Paths = []string{"bug"}
request.Rule.Bug = &pb.AssociatedBug{
System: ruleManagedOther.BugID.System,
Id: ruleManagedOther.BugID.ID,
}
rule, err := srv.Update(ctx, request)
So(err, ShouldBeNil)
storedRule, err := rules.Read(span.Single(ctx), testProject, ruleManaged.RuleID)
So(err, ShouldBeNil)
// Check the rule was updated.
So(storedRule.LastUpdated, ShouldNotEqual, ruleManaged.LastUpdated)
expectedRule := ruleManagedBuilder.
// Verify the bug was updated, but that IsManagingBug
// was silently set to false, because ruleManagedOther
// already controls the bug.
WithBug(ruleManagedOther.BugID).
WithBugManaged(false).
// Accept whatever the new last updated time is.
WithLastUpdated(storedRule.LastUpdated).
WithLastUpdatedUser("someone@example.com").
Build()
// Verify the rule was correctly updated in the database.
So(storedRule, ShouldResemble, expectedRule)
// Verify the returned rule matches what was expected.
So(rule, ShouldResembleProto, createRulePB(expectedRule, cfg, true /*includeDefinition*/))
})
})
Convey("Concurrent Modification", func() {
_, err := srv.Update(ctx, request)
So(err, ShouldBeNil)
// Attempt the same modification again without
// requerying.
rule, err := srv.Update(ctx, request)
So(rule, ShouldBeNil)
So(err, ShouldBeRPCAborted)
})
Convey("Rule does not exist", func() {
ruleID := strings.Repeat("00", 16)
request.Rule.Name = fmt.Sprintf("projects/%s/rules/%s", testProject, ruleID)
rule, err := srv.Update(ctx, request)
So(rule, ShouldBeNil)
So(err, ShouldBeRPCNotFound)
})
Convey("Validation error", func() {
Convey("Invalid bug monorail project", func() {
request.Rule.Bug.Id = "otherproject/2"
rule, err := srv.Update(ctx, request)
So(rule, ShouldBeNil)
So(err, ShouldBeRPCInvalidArgument, "bug not in expected monorail project (monorailproject)")
})
Convey("Re-use of same bug in same project", func() {
// Use the same bug as another rule.
request.Rule.Bug = &pb.AssociatedBug{
System: ruleTwoProject.BugID.System,
Id: ruleTwoProject.BugID.ID,
}
rule, err := srv.Update(ctx, request)
So(rule, ShouldBeNil)
So(err, ShouldBeRPCInvalidArgument,
fmt.Sprintf("bug already used by a rule in the same project (%s/%s)",
ruleTwoProject.Project, ruleTwoProject.RuleID))
})
Convey("Bug managed by another rule", func() {
// Select a bug already managed by another rule.
request.Rule.Bug = &pb.AssociatedBug{
System: ruleManagedOther.BugID.System,
Id: ruleManagedOther.BugID.ID,
}
// Request we manage this bug.
request.Rule.IsManagingBug = true
request.Rule.IsManagingBugPriority = true
request.UpdateMask.Paths = []string{"bug", "is_managing_bug"}
rule, err := srv.Update(ctx, request)
So(rule, ShouldBeNil)
So(err, ShouldBeRPCInvalidArgument,
fmt.Sprintf("bug already managed by a rule in another project (%s/%s)",
ruleManagedOther.Project, ruleManagedOther.RuleID))
})
Convey("Invalid rule definition", func() {
// Use an invalid failure association rule.
request.Rule.RuleDefinition = ""
rule, err := srv.Update(ctx, request)
So(rule, ShouldBeNil)
So(err, ShouldBeRPCInvalidArgument, "rule definition is not valid")
})
})
})
Convey("Create", func() {
authState.IdentityPermissions = []authtest.RealmPermission{
{
Realm: "testproject:@root",
Permission: perms.PermCreateRule,
},
}
request := &pb.CreateRuleRequest{
Parent: fmt.Sprintf("projects/%s", testProject),
Rule: &pb.Rule{
RuleDefinition: `test = "create"`,
Bug: &pb.AssociatedBug{
System: "monorail",
Id: "monorailproject/2",
},
IsActive: false,
IsManagingBug: true,
IsManagingBugPriority: true,
SourceCluster: &pb.ClusterId{
Algorithm: testname.AlgorithmName,
Id: strings.Repeat("aa", 16),
},
},
}
Convey("No create rule permission", func() {
authState.IdentityPermissions = removePermission(authState.IdentityPermissions, perms.PermCreateRule)
rule, err := srv.Create(ctx, request)
So(err, ShouldBeRPCPermissionDenied, "caller does not have permission analysis.rules.create")
So(rule, ShouldBeNil)
})
Convey("Success", func() {
expectedRuleBuilder := rules.NewRule(0).
WithProject(testProject).
WithRuleDefinition(`test = "create"`).
WithActive(false).
WithBugManaged(true).
WithBugPriorityManaged(true).
WithCreationUser("someone@example.com").
WithLastUpdatedUser("someone@example.com").
WithSourceCluster(clustering.ClusterID{
Algorithm: testname.AlgorithmName,
ID: strings.Repeat("aa", 16),
})
Convey("Bug not managed by another rule", func() {
// Re-use the same bug as a rule in another project,
// where the other rule is not managing the bug.
request.Rule.Bug = &pb.AssociatedBug{
System: ruleUnmanagedOther.BugID.System,
Id: ruleUnmanagedOther.BugID.ID,
}
rule, err := srv.Create(ctx, request)
So(err, ShouldBeNil)
storedRule, err := rules.Read(span.Single(ctx), testProject, rule.RuleId)
So(err, ShouldBeNil)
expectedRule := expectedRuleBuilder.
// Accept the randomly generated rule ID.
WithRuleID(rule.RuleId).
WithBug(ruleUnmanagedOther.BugID).
// Accept whatever CreationTime was assigned, as it
// is determined by Spanner commit time.
// Rule spanner data access code tests already validate
// this is populated correctly.
WithCreationTime(storedRule.CreationTime).
WithLastUpdated(storedRule.CreationTime).
WithPredicateLastUpdated(storedRule.CreationTime).
Build()
// Verify the rule was correctly created in the database.
So(storedRule, ShouldResemble, expectedRuleBuilder.Build())
// Verify the returned rule matches our expectations.
So(rule, ShouldResembleProto, createRulePB(expectedRule, cfg, true /*includeDefinition*/))
})
Convey("Bug managed by another rule", func() {
// Re-use the same bug as a rule in another project,
// where that rule is managing the bug.
request.Rule.Bug = &pb.AssociatedBug{
System: ruleManagedOther.BugID.System,
Id: ruleManagedOther.BugID.ID,
}
rule, err := srv.Create(ctx, request)
So(err, ShouldBeNil)
storedRule, err := rules.Read(span.Single(ctx), testProject, rule.RuleId)
So(err, ShouldBeNil)
expectedRule := expectedRuleBuilder.
// Accept the randomly generated rule ID.
WithRuleID(rule.RuleId).
WithBug(ruleManagedOther.BugID).
// Because another rule is managing the bug, this rule
// should be silenlty stopped from managing the bug.
WithBugManaged(false).
// Accept whatever CreationTime was assigned.
WithCreationTime(storedRule.CreationTime).
WithLastUpdated(storedRule.CreationTime).
WithPredicateLastUpdated(storedRule.CreationTime).
Build()
// Verify the rule was correctly created in the database.
So(storedRule, ShouldResemble, expectedRuleBuilder.Build())
// Verify the returned rule matches our expectations.
So(rule, ShouldResembleProto, createRulePB(expectedRule, cfg, true /*includeDefinition*/))
})
Convey("Buganizer", func() {
request.Rule.Bug = &pb.AssociatedBug{
System: "buganizer",
Id: "1111111111",
}
rule, err := srv.Create(ctx, request)
So(err, ShouldBeNil)
storedRule, err := rules.Read(span.Single(ctx), testProject, rule.RuleId)
So(err, ShouldBeNil)
expectedRule := expectedRuleBuilder.
// Accept the randomly generated rule ID.
WithRuleID(rule.RuleId).
WithBug(bugs.BugID{System: "buganizer", ID: "1111111111"}).
// Accept whatever CreationTime was assigned, as it
// is determined by Spanner commit time.
// Rule spanner data access code tests already validate
// this is populated correctly.
WithCreationTime(storedRule.CreationTime).
WithLastUpdated(storedRule.CreationTime).
WithPredicateLastUpdated(storedRule.CreationTime).
Build()
// Verify the rule was correctly created in the database.
So(storedRule, ShouldResemble, expectedRuleBuilder.Build())
// Verify the returned rule matches our expectations.
So(rule, ShouldResembleProto, createRulePB(expectedRule, cfg, true /*includeDefinition*/))
})
})
Convey("Validation error", func() {
Convey("Invalid bug monorail project", func() {
request.Rule.Bug.Id = "otherproject/2"
rule, err := srv.Create(ctx, request)
So(rule, ShouldBeNil)
So(err, ShouldBeRPCInvalidArgument,
"bug not in expected monorail project (monorailproject)")
})
Convey("Re-use of same bug in same project", func() {
// Use the same bug as another rule, in the same project.
request.Rule.Bug = &pb.AssociatedBug{
System: ruleTwoProject.BugID.System,
Id: ruleTwoProject.BugID.ID,
}
rule, err := srv.Create(ctx, request)
So(rule, ShouldBeNil)
So(err, ShouldBeRPCInvalidArgument,
fmt.Sprintf("bug already used by a rule in the same project (%s/%s)",
ruleTwoProject.Project, ruleTwoProject.RuleID))
})
Convey("Invalid rule definition", func() {
// Use an invalid failure association rule.
request.Rule.RuleDefinition = ""
rule, err := srv.Create(ctx, request)
So(rule, ShouldBeNil)
So(err, ShouldBeRPCInvalidArgument, "rule definition is not valid")
})
})
})
Convey("LookupBug", func() {
authState.IdentityPermissions = []authtest.RealmPermission{
{
Realm: "testproject:@root",
Permission: perms.PermListRules,
},
{
Realm: "otherproject:@root",
Permission: perms.PermListRules,
},
}
Convey("Exists None", func() {
request := &pb.LookupBugRequest{
System: "monorail",
Id: "notexists/1",
}
response, err := srv.LookupBug(ctx, request)
So(err, ShouldBeNil)
So(response, ShouldResembleProto, &pb.LookupBugResponse{
Rules: []string{},
})
})
Convey("Exists One", func() {
request := &pb.LookupBugRequest{
System: ruleManaged.BugID.System,
Id: ruleManaged.BugID.ID,
}
response, err := srv.LookupBug(ctx, request)
So(err, ShouldBeNil)
So(response, ShouldResembleProto, &pb.LookupBugResponse{
Rules: []string{
fmt.Sprintf("projects/%s/rules/%s",
ruleManaged.Project, ruleManaged.RuleID),
},
})
Convey("If no permission in relevant project", func() {
authState.IdentityPermissions = nil
response, err := srv.LookupBug(ctx, request)
So(err, ShouldBeNil)
So(response, ShouldResembleProto, &pb.LookupBugResponse{
Rules: []string{},
})
})
})
Convey("Exists Many", func() {
request := &pb.LookupBugRequest{
System: ruleTwoProject.BugID.System,
Id: ruleTwoProject.BugID.ID,
}
response, err := srv.LookupBug(ctx, request)
So(err, ShouldBeNil)
So(response, ShouldResembleProto, &pb.LookupBugResponse{
Rules: []string{
// Rules are returned alphabetically by project.
fmt.Sprintf("projects/otherproject/rules/%s", ruleTwoProjectOther.RuleID),
fmt.Sprintf("projects/testproject/rules/%s", ruleTwoProject.RuleID),
},
})
Convey("If list permission exists in only some projects", func() {
authState.IdentityPermissions = []authtest.RealmPermission{
{
Realm: "testproject:@root",
Permission: perms.PermListRules,
},
}
response, err := srv.LookupBug(ctx, request)
So(err, ShouldBeNil)
So(response, ShouldResembleProto, &pb.LookupBugResponse{
Rules: []string{
fmt.Sprintf("projects/testproject/rules/%s", ruleTwoProject.RuleID),
},
})
})
})
})
})
Convey("formatRule", t, func() {
rule := rules.NewRule(0).
WithProject(testProject).
WithBug(bugs.BugID{System: "monorail", ID: "monorailproject/123456"}).
WithRuleDefinition(`test = "create"`).
WithActive(false).
WithBugManaged(true).
WithSourceCluster(clustering.ClusterID{
Algorithm: testname.AlgorithmName,
ID: strings.Repeat("aa", 16),
}).Build()
expectedRule := `{
RuleDefinition: "test = \"create\"",
BugID: "monorail:monorailproject/123456",
IsActive: false,
IsManagingBug: true,
IsManagingBugPriority: true,
SourceCluster: "testname-v3:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
LastUpdated: "1900-01-02T03:04:07Z"
}`
So(formatRule(rule), ShouldEqual, expectedRule)
})
}