drone/app/services/protection/set_test.go
Darko Draskovic cad0fbdf98 feat: [CODE-3256]: Add merge verify rule info related to minimum required approvals count for default reviewers (#3483)
* Merge remote-tracking branch 'origin/main' into dd/merge-verify-rules
* Replace princiapl ids with infos in merge verify response
* Populate DefaultReviewerApprovalsResponse with principal ids and current count
* Add MergeVerifyRulesResponse type
2025-02-26 17:56:59 +00:00

523 lines
14 KiB
Go

// Copyright 2023 Harness, Inc.
//
// 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 protection
import (
"context"
"reflect"
"testing"
"github.com/harness/gitness/app/services/codeowners"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
)
// nolint:gocognit // it's a unit test
func TestRuleSet_MergeVerify(t *testing.T) {
tests := []struct {
name string
rules []types.RuleInfoInternal
input MergeVerifyInput
expOut MergeVerifyOutput
expViol []types.RuleViolations
}{
{
name: "empty-with-merge-method",
rules: []types.RuleInfoInternal{},
input: MergeVerifyInput{
Actor: &types.Principal{ID: 1},
Method: enum.MergeMethodRebase,
TargetRepo: &types.RepositoryCore{ID: 1, DefaultBranch: "main"},
PullReq: &types.PullReq{ID: 1, SourceBranch: "pr", TargetBranch: "main"},
},
expOut: MergeVerifyOutput{
DeleteSourceBranch: false,
AllowedMethods: enum.MergeMethods,
},
expViol: nil,
},
{
name: "empty-no-merge-method-specified",
rules: []types.RuleInfoInternal{},
input: MergeVerifyInput{
Actor: &types.Principal{ID: 1},
TargetRepo: &types.RepositoryCore{ID: 1, DefaultBranch: "main"},
PullReq: &types.PullReq{ID: 1, SourceBranch: "pr", TargetBranch: "main"},
},
expOut: MergeVerifyOutput{
DeleteSourceBranch: false,
AllowedMethods: enum.MergeMethods,
},
expViol: nil,
},
{
name: "two-rules-delete-source-branch",
rules: []types.RuleInfoInternal{
{
RuleInfo: types.RuleInfo{
SpacePath: "",
RepoPath: "space/repo",
ID: 1,
Identifier: "rule1",
Type: TypeBranch,
State: enum.RuleStateActive,
},
Pattern: []byte(`{"default":true}`),
Definition: []byte(`{"pullreq":{"merge":{"strategies_allowed":["merge"],"delete_branch":true}}}`),
},
{
RuleInfo: types.RuleInfo{
SpacePath: "space",
RepoPath: "",
ID: 2,
Identifier: "rule2",
Type: TypeBranch,
State: enum.RuleStateActive,
},
Pattern: []byte(`{"default":true}`),
Definition: []byte(`{"pullreq":{"approvals":{"require_minimum_count":1}}}`),
},
},
input: MergeVerifyInput{
Actor: &types.Principal{ID: 1},
TargetRepo: &types.RepositoryCore{ID: 1, DefaultBranch: "main"},
PullReq: &types.PullReq{ID: 1, SourceBranch: "pr", TargetBranch: "main"},
Method: enum.MergeMethodRebase,
},
expOut: MergeVerifyOutput{
DeleteSourceBranch: true,
MinimumRequiredApprovalsCount: 1,
AllowedMethods: []enum.MergeMethod{enum.MergeMethodMerge},
},
expViol: []types.RuleViolations{
{
Rule: types.RuleInfo{
SpacePath: "",
RepoPath: "space/repo",
ID: 1,
Identifier: "rule1",
Type: TypeBranch,
State: enum.RuleStateActive,
},
Bypassed: false,
Violations: []types.Violation{
{Code: codePullReqMergeStrategiesAllowed},
},
},
{
Rule: types.RuleInfo{
SpacePath: "space",
RepoPath: "",
ID: 2,
Identifier: "rule2",
Type: TypeBranch,
State: enum.RuleStateActive,
},
Bypassed: false,
Violations: []types.Violation{
{Code: codePullReqApprovalReqMinCount},
},
},
},
},
{
name: "two-rules-merge-strategies",
rules: []types.RuleInfoInternal{
{
RuleInfo: types.RuleInfo{
SpacePath: "",
RepoPath: "space/repo",
ID: 1,
Identifier: "rule1",
Type: TypeBranch,
State: enum.RuleStateActive,
},
Pattern: []byte(`{"default":true}`),
Definition: []byte(`{"pullreq":{"merge":{"strategies_allowed":["merge","rebase"]}}}`),
},
{
RuleInfo: types.RuleInfo{
SpacePath: "space",
RepoPath: "",
ID: 2,
Identifier: "rule2",
Type: TypeBranch,
State: enum.RuleStateActive,
},
Pattern: []byte(`{"default":true}`),
Definition: []byte(`{"pullreq":{"merge":{"strategies_allowed":["rebase"]}}}`),
},
},
input: MergeVerifyInput{
Actor: &types.Principal{ID: 1},
TargetRepo: &types.RepositoryCore{ID: 1, DefaultBranch: "main"},
PullReq: &types.PullReq{ID: 1, SourceBranch: "pr", TargetBranch: "main"},
},
expOut: MergeVerifyOutput{
DeleteSourceBranch: false,
AllowedMethods: []enum.MergeMethod{enum.MergeMethodRebase},
},
expViol: []types.RuleViolations{},
},
{
name: "combine-definition-values",
rules: []types.RuleInfoInternal{
{
RuleInfo: types.RuleInfo{
SpacePath: "",
RepoPath: "space/repo",
ID: 1,
Identifier: "rule1",
Type: TypeBranch,
State: enum.RuleStateActive,
},
Pattern: []byte(`{"default":true}`),
Definition: []byte(`{
"pullreq": {
"approvals": {
"require_code_owners": false,
"require_minimum_count": 2,
"require_no_change_request": false,
"require_latest_commit": true
},
"comments":{
"require_resolve_all": false
},
"merge":{
"delete_branch": true,
"strategies_allowed": ["merge","rebase"]
}
}
}`),
},
{
RuleInfo: types.RuleInfo{
SpacePath: "space",
RepoPath: "",
ID: 2,
Identifier: "rule2",
Type: TypeBranch,
State: enum.RuleStateActive,
},
Pattern: []byte(`{"default":true}`),
Definition: []byte(`{
"pullreq": {
"approvals": {
"require_code_owners": true,
"require_minimum_count": 3,
"require_no_change_request": true,
"require_latest_commit": true
},
"comments":{
"require_resolve_all": true
},
"merge":{
"delete_branch": true,
"strategies_allowed": ["rebase","squash"]
}
}
}`),
},
{
RuleInfo: types.RuleInfo{
SpacePath: "",
RepoPath: "space/repo",
ID: 3,
Identifier: "rule3",
Type: TypeBranch,
State: enum.RuleStateActive,
},
Pattern: []byte(`{"default":true}`),
Definition: []byte(`{
"pullreq": {
"approvals": {
"require_code_owners": true,
"require_minimum_count": 2,
"require_no_change_request": false,
"require_latest_commit": false
},
"comments":{
"require_resolve_all": false
},
"merge":{
"delete_branch": false,
"strategies_allowed": ["rebase"]
}
}
}`),
},
},
input: MergeVerifyInput{
Actor: &types.Principal{ID: 1},
TargetRepo: &types.RepositoryCore{ID: 1, DefaultBranch: "main"},
PullReq: &types.PullReq{ID: 1, SourceBranch: "pr", TargetBranch: "main"},
CodeOwners: &codeowners.Evaluation{},
Reviewers: []*types.PullReqReviewer{},
},
expOut: MergeVerifyOutput{
AllowedMethods: []enum.MergeMethod{enum.MergeMethodRebase},
DeleteSourceBranch: true,
MinimumRequiredApprovalsCount: 2,
MinimumRequiredApprovalsCountLatest: 3,
RequiresCodeOwnersApproval: true,
RequiresCodeOwnersApprovalLatest: true,
RequiresCommentResolution: true,
RequiresNoChangeRequests: true,
},
expViol: []types.RuleViolations{
{
Rule: types.RuleInfo{
SpacePath: "",
RepoPath: "space/repo",
ID: 1,
Identifier: "rule1",
Type: TypeBranch,
State: enum.RuleStateActive,
},
Bypassed: false,
Violations: []types.Violation{
{Code: codePullReqApprovalReqMinCountLatest},
},
},
{
Rule: types.RuleInfo{
SpacePath: "space",
RepoPath: "",
ID: 2,
Identifier: "rule2",
Type: TypeBranch,
State: enum.RuleStateActive,
},
Bypassed: false,
Violations: []types.Violation{
{Code: codePullReqApprovalReqMinCountLatest},
},
},
{
Rule: types.RuleInfo{
SpacePath: "",
RepoPath: "space/repo",
ID: 3,
Identifier: "rule3",
Type: TypeBranch,
State: enum.RuleStateActive,
},
Bypassed: false,
Violations: []types.Violation{
{Code: codePullReqApprovalReqMinCount},
},
},
},
},
}
ctx := context.Background()
m := NewManager(nil)
_ = m.Register(TypeBranch, func() Definition {
return &Branch{}
})
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
set := ruleSet{
rules: test.rules,
manager: m,
}
out, violations, err := set.MergeVerify(ctx, test.input)
if err != nil {
t.Errorf("got error: %s", err.Error())
}
if want, got := test.expOut, out; !reflect.DeepEqual(want, got) {
t.Errorf("output: want=%+v got=%+v", want, got)
}
if want, got := len(test.expViol), len(violations); want != got {
t.Errorf("violations count: want=%d got=%d", want, got)
return
}
for i := range test.expViol {
if want, got := test.expViol[i].Rule, violations[i].Rule; want != got {
t.Errorf("violation %d rule: want=%+v got=%+v", i, want, got)
}
if want, got := test.expViol[i].Bypassed, violations[i].Bypassed; want != got {
t.Errorf("violation %d bypassed: want=%t got=%t", i, want, got)
}
if want, got := len(test.expViol[i].Violations), len(violations[i].Violations); want != got {
t.Errorf("violation %d violations count: want=%d got=%d", i, want, got)
continue
}
for j := range test.expViol[i].Violations {
if want, got := test.expViol[i].Violations[j].Code, violations[i].Violations[j].Code; want != got {
t.Errorf("violation %d violation %d code: want=%s got=%s", i, j, want, got)
}
}
}
})
}
}
func TestRuleSet_RequiredChecks(t *testing.T) {
tests := []struct {
name string
rules []types.RuleInfoInternal
input RequiredChecksInput
expOut RequiredChecksOutput
}{
{
name: "empty",
rules: []types.RuleInfoInternal{},
input: RequiredChecksInput{
Actor: &types.Principal{ID: 1},
Repo: &types.RepositoryCore{ID: 1, DefaultBranch: "main"},
PullReq: &types.PullReq{ID: 1, SourceBranch: "pr", TargetBranch: "main"},
},
expOut: RequiredChecksOutput{
RequiredIdentifiers: map[string]struct{}{},
BypassableIdentifiers: map[string]struct{}{},
},
},
{
name: "two-rules",
rules: []types.RuleInfoInternal{
{
RuleInfo: types.RuleInfo{
SpacePath: "",
RepoPath: "space/repo",
ID: 1,
Identifier: "rule1",
Type: TypeBranch,
State: enum.RuleStateActive,
},
Pattern: []byte(`{"default":true}`),
Definition: []byte(`{
"bypass":{"repo_owners":true},
"pullreq":{"status_checks":{"require_identifiers":["a", "b"]}}
}`),
},
{
RuleInfo: types.RuleInfo{
SpacePath: "space",
RepoPath: "",
ID: 2,
Identifier: "rule2",
Type: TypeBranch,
State: enum.RuleStateActive,
},
Pattern: []byte(`{"default":true}`),
Definition: []byte(`{"pullreq":{"status_checks":{"require_identifiers":["b","c"]}}}`),
},
},
input: RequiredChecksInput{
Actor: &types.Principal{ID: 1},
IsRepoOwner: true,
Repo: &types.RepositoryCore{ID: 1, DefaultBranch: "main"},
PullReq: &types.PullReq{ID: 1, SourceBranch: "pr", TargetBranch: "main"},
},
expOut: RequiredChecksOutput{
RequiredIdentifiers: map[string]struct{}{"b": {}, "c": {}},
BypassableIdentifiers: map[string]struct{}{"a": {}},
},
},
}
ctx := context.Background()
m := NewManager(nil)
_ = m.Register(TypeBranch, func() Definition {
return &Branch{}
})
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
set := ruleSet{
rules: test.rules,
manager: m,
}
out, err := set.RequiredChecks(ctx, test.input)
if err != nil {
t.Errorf("got error: %s", err.Error())
}
if want, got := test.expOut, out; !reflect.DeepEqual(want, got) {
t.Errorf("output: want=%+v got=%+v", want, got)
}
})
}
}
func TestIntersectSorted(t *testing.T) {
tests := []struct {
name string
a, b []int
exp []int
}{
{
name: "empty",
a: []int{},
b: []int{},
exp: []int{},
},
{
name: "remove last",
a: []int{3, 4},
b: []int{2, 3},
exp: []int{3},
},
{
name: "remove first",
a: []int{3, 4, 6},
b: []int{4, 5, 6},
exp: []int{4, 6},
},
{
name: "remove all",
a: []int{3, 4},
b: []int{},
exp: []int{},
},
{
name: "leave all",
a: []int{3, 4},
b: []int{1, 2, 3, 4, 5, 6},
exp: []int{3, 4},
},
{
name: "remove first and last",
a: []int{3, 4, 4, 4, 5},
b: []int{4, 6},
exp: []int{4, 4, 4},
},
{
name: "remove duplicated",
a: []int{3, 4},
b: []int{3, 3, 3, 5, 5},
exp: []int{3},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if want, got := test.exp, intersectSorted(test.a, test.b); !reflect.DeepEqual(want, got) {
t.Errorf("want=%v got=%v", want, got)
}
})
}
}