drone/app/services/codeowners/service_test.go

675 lines
18 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 codeowners
import (
"reflect"
"testing"
"github.com/harness/gitness/app/store"
"github.com/harness/gitness/git"
)
func TestService_ParseCodeOwner(t *testing.T) {
type fields struct {
repoStore store.RepoStore
git git.Interface
Config Config
}
type args struct {
codeOwnersContent string
}
tests := []struct {
name string
fields fields
args args
want []Entry
wantErr bool
}{
{
name: "Code owners Single",
args: args{`**/contracts/openapi/v1/ user1@harness.io user2@harness.io`},
want: []Entry{
{
LineNumber: 1,
Pattern: "**/contracts/openapi/v1/",
Owners: []string{"user1@harness.io", "user2@harness.io"},
},
},
},
{
name: "Code owners Multiple",
args: args{`
**/contracts/openapi/v1/ user1@harness.io user2@harness.io
/scripts/api user3@harness.io user4@harness.io
`},
want: []Entry{
{
LineNumber: 2,
Pattern: "**/contracts/openapi/v1/",
Owners: []string{"user1@harness.io", "user2@harness.io"},
},
{
LineNumber: 3,
Pattern: "/scripts/api",
Owners: []string{"user3@harness.io", "user4@harness.io"},
},
},
},
{
name: " Code owners with full line comments",
args: args{`
# codeowner file
**/contracts/openapi/v1/ user1@harness.io user2@harness.io
#
/scripts/api user1@harness.io user2@harness.io
`},
want: []Entry{
{
LineNumber: 3,
Pattern: "**/contracts/openapi/v1/",
Owners: []string{"user1@harness.io", "user2@harness.io"},
},
{
LineNumber: 5,
Pattern: "/scripts/api",
Owners: []string{"user1@harness.io", "user2@harness.io"},
},
},
},
{
name: " Code owners with reset",
args: args{`
* user1@harness.io
/scripts/api
`},
want: []Entry{
{
LineNumber: 2,
Pattern: "*",
Owners: []string{"user1@harness.io"},
},
{
LineNumber: 3,
Pattern: "/scripts/api",
Owners: []string{},
},
},
},
{
name: " Code owners with escaped characters in pattern",
args: args{`
# escaped escape character
\\ user1@harness.io
# escaped control characters (are unescaped)
\ \ \# user2@harness.io
# escaped special pattern syntax characters (stay escaped)
\*\?\[\]\{\}\-\!\^ user3@harness.io
# mix of escapes
\\\ \*\\\\\? user4@harness.io
`},
want: []Entry{
{
LineNumber: 3,
Pattern: "\\\\",
Owners: []string{"user1@harness.io"},
},
{
LineNumber: 5,
Pattern: " #",
Owners: []string{"user2@harness.io"},
},
{
LineNumber: 7,
Pattern: "\\*\\?\\[\\]\\{\\}\\-\\!\\^",
Owners: []string{"user3@harness.io"},
},
{
LineNumber: 9,
Pattern: "\\\\ \\*\\\\\\\\\\?",
Owners: []string{"user4@harness.io"},
},
},
},
{
name: " Code owners with multiple spaces as divider",
args: args{`
* user1@harness.io user2@harness.io
`},
want: []Entry{
{
LineNumber: 2,
Pattern: "*",
Owners: []string{"user1@harness.io", "user2@harness.io"},
},
},
},
{
name: " Code owners with invalid escaping standalone '\\'",
args: args{`
\
`},
wantErr: true,
},
{
name: " Code owners with invalid escaping unsupported char",
args: args{`
\a
`},
wantErr: true,
},
{
name: " Code owners with utf8",
args: args{`
D∆NCE user@h∆rness.io
`},
want: []Entry{
{
LineNumber: 2,
Pattern: "D∆NCE",
Owners: []string{"user@h∆rness.io"},
},
},
},
{
name: " Code owners with tabs and spaces",
args: args{`
a\ user1@harness.io user2@harness.io user3@harness.io
`},
want: []Entry{
{
LineNumber: 2,
Pattern: "a ",
Owners: []string{"user1@harness.io", "user2@harness.io", "user3@harness.io"},
},
},
},
{
name: " Code owners with inline comments",
args: args{`
a #user1@harness.io
b # user1@harness.io
c #
d#
e# user1@harness.io
f user1@harness.io#user2@harness.io
g user1@harness.io # user2@harness.io
`},
want: []Entry{
{
LineNumber: 2,
Pattern: "a",
Owners: []string{},
},
{
LineNumber: 3,
Pattern: "b",
Owners: []string{},
},
{
LineNumber: 4,
Pattern: "c",
Owners: []string{},
},
{
LineNumber: 5,
Pattern: "d",
Owners: []string{},
},
{
LineNumber: 6,
Pattern: "e",
Owners: []string{},
},
{
LineNumber: 7,
Pattern: "f",
Owners: []string{"user1@harness.io"},
},
{
LineNumber: 8,
Pattern: "g",
Owners: []string{"user1@harness.io"},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &Service{
repoStore: tt.fields.repoStore,
git: tt.fields.git,
config: tt.fields.Config,
}
got, err := s.parseCodeOwner(tt.args.codeOwnersContent)
if (err != nil) != tt.wantErr {
t.Errorf("ParseCodeOwner() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ParseCodeOwner() got = %v, want %v", got, tt.want)
}
})
}
}
func Test_match(t *testing.T) {
type args struct {
pattern string
matchingTargets []string
nonMatchingTargets []string
}
tests := []struct {
name string
args args
}{
{
name: "root",
args: args{
pattern: "/",
matchingTargets: []string{"a.txt", "x/a.txt", "x/y/a.txt"},
nonMatchingTargets: []string{},
},
},
{
name: "file exact match",
args: args{
pattern: "/a.txt",
matchingTargets: []string{"a.txt", "a.txt/b.go", "a.txt/b.go/c.ar"},
nonMatchingTargets: []string{"a.txt2", "b.txt", "a.go", "x/a.txt"},
},
},
{
name: "file exact match with directory",
args: args{
pattern: "/x/a.txt",
matchingTargets: []string{"x/a.txt", "x/a.txt/b.txt", "x/a.txt/b.go/c.ar"},
nonMatchingTargets: []string{"a.txt", "x/a.txt2", "x/b.txt", "x/a.go"},
},
},
{
name: "file relative match",
args: args{
pattern: "a.txt",
matchingTargets: []string{"a.txt", "x/a.txt", "x/y/a.txt", "x/y/a.txt/b.go/c.ar"},
nonMatchingTargets: []string{"a.txt2", "b.txt", "a.go", "x/a.txt2"},
},
},
{
name: "file relative match with directory",
args: args{
pattern: "x/a.txt",
matchingTargets: []string{"x/a.txt", "x/a.txt/b.go", "x/a.txt/b.go/c.ar"},
nonMatchingTargets: []string{"a.txt2", "b.txt", "a.go", "x/a.txt2", "v/x/a.txt", "y/a.txt"},
},
},
{
name: "folder exact match",
args: args{
pattern: "/x/",
matchingTargets: []string{"x/a.txt", "x/b.go", "x/y/a.txt"},
nonMatchingTargets: []string{"x", "a.txt", "y/a.txt", "w/x/a.txt"},
},
},
{
name: "folder relative match",
args: args{
pattern: "x/",
matchingTargets: []string{"x/a.txt", "x/b.txt", "w/x/a.txt", "w/x/y/a.txt"},
nonMatchingTargets: []string{"x", "w/x", "a.txt", "y/a.txt"},
},
},
{
name: "match-all",
args: args{
pattern: "*",
matchingTargets: []string{"a", "a.txt", "x/a.txt", "x/y/a.txt"},
nonMatchingTargets: []string{},
},
},
{
name: "match-all in relative dir",
args: args{
pattern: "x/*",
matchingTargets: []string{"x/a.txt"},
nonMatchingTargets: []string{"x", "y/a.txt", "w/x/b.go", "x/a.txt/b.txt"},
},
},
{
name: "match-all in absolute dir",
args: args{
pattern: "/x/*",
matchingTargets: []string{"x/a.txt", "x/b.go"},
nonMatchingTargets: []string{"x", "y/a.txt", "w/x/a.txt", "x/a.txt/b.go"},
},
},
{
name: "file match-all type",
args: args{
pattern: "*.txt",
matchingTargets: []string{"a.txt", "x/a.txt", "x/y/a.txt", "x/y/a.txt/b.go", "x/y/a.txt/c.ar"},
nonMatchingTargets: []string{"a.txt2", "a.go"},
},
},
{
name: "file match-all type in root folder",
args: args{
pattern: "/*.txt",
matchingTargets: []string{"a.txt", "a.txt/b.go", "a.txt/b.go/c.ar"},
nonMatchingTargets: []string{"a.txt2", "a.go", "x/a.txt", "x/y/a.txt"},
},
},
{
name: "file match-all type in absolute sub folder",
args: args{
pattern: "/x/*.txt",
matchingTargets: []string{"x/a.txt", "x/a.txt/b.go", "x/a.txt/b.go/c.ar"},
nonMatchingTargets: []string{"a.txt", "x/a.txt2", "x/a.go", "w/x/a.txt", "y/a.txt"},
},
},
{
name: "file match-all types in relative sub folder",
args: args{
pattern: "x/*.txt",
matchingTargets: []string{"x/a.txt", "x/a.txt/b.go", "x/a.txt/b.go/c.ar"},
nonMatchingTargets: []string{"a.txt", "x/a.txt2", "x/a.go", "w/x/a.txt", "y/a.txt"},
},
},
{
name: "inner match-all",
args: args{
pattern: "/x/*/a.txt",
matchingTargets: []string{"x/y/a.txt", "x/y/a.txt/b.go", "x/y/a.txt/b.go/c.ar"},
nonMatchingTargets: []string{"a.txt", "x/a.txt", "w/x/y/a.txt", "x/y/z/a.txt"},
},
},
{
name: "escaped match-all",
args: args{
pattern: "\\*",
matchingTargets: []string{"*", "x/y/*", "x/y/*/b.go/c.ar"},
nonMatchingTargets: []string{"a.txt"},
},
},
/*
TODO: Fix bug in doublestar library, currently doesn't match `a.` ...
{
name: "trailing match-all on string",
args: args{
pattern: "a.*",
matchingTargets: []string{"a.", "a.txt", "x/a.txt", "x/a.txt/b.go", "x/a.txt/b.go/c.ar"},
nonMatchingTargets: []string{"atxt", "b.txt"},
},
},
*/
{
name: "globstar",
args: args{
pattern: "**",
matchingTargets: []string{"a", "a.txt", "x/a.txt", "x/y/a.txt"},
nonMatchingTargets: []string{},
},
},
{
name: "trailing globstar absolute path",
args: args{
pattern: "/x/**",
matchingTargets: []string{"x/a.txt", "x/b.txt", "x/y/a.txt"},
nonMatchingTargets: []string{"a.txt", "x", "y/a.txt", "w/x/a.txt"},
},
},
{
name: "trailing globstar relative path",
args: args{
pattern: "x/**",
matchingTargets: []string{"x/a.txt", "x/b.txt", "x/y/a.txt"},
nonMatchingTargets: []string{"a.txt", "x", "y/a.txt", "w/x/a.txt"},
},
},
/*
TODO: Fix bug in doublestar library, currently doesn't match `a.` ...
{
name: "trailing globstar on string",
args: args{
pattern: "a.**",
matchingTargets: []string{"a.", "a.txt", "x/a.txt", "x/a.txt/b.go", "x/a.txt/b.go/c.ar"},
nonMatchingTargets: []string{"atxt", "b.txt"},
},
},
*/
{
name: "leading globstar",
args: args{
pattern: "**/a.txt",
matchingTargets: []string{"a.txt", "x/a.txt", "x/y/a.txt", "x/y/a.txt/b.go", "x/y/a.txt/b.go/c.ar"},
nonMatchingTargets: []string{"b.txt", "a.txt2"},
},
},
{
name: "surrounding globstar",
args: args{
pattern: "**/x/**",
matchingTargets: []string{"x/a.txt", "w/x/a.txt", "x/y/a.txt", "w/x/y/a.txt"},
nonMatchingTargets: []string{"a.txt", "x", "w/x"},
},
},
{
name: "inner globstar",
args: args{
pattern: "/x/**/a.txt",
matchingTargets: []string{
"x/a.txt", "x/y/a.txt", "x/y/z/a.txt", "x/y/z/a.txt/b.go", "x/y/z/a.txt/b.go/c.ar"},
nonMatchingTargets: []string{"a.txt", "w/x/a.txt", "y/a.txt"},
},
},
{
name: "multi-inner globstar",
args: args{
pattern: "/x/**/z/**/a.txt",
matchingTargets: []string{
"x/z/a.txt",
"x/y/z/l/a.txt",
"x/y/yy/z/l/ll/a.txt",
"x/y/yy/z/l/ll/a.txt/b.go",
"x/y/yy/z/l/ll/a.txt/b.go/c.ar",
},
nonMatchingTargets: []string{"a.txt", "x/a.txt", "z/a.txt", "w/x/a.txt", "y/a.txt"},
},
},
{
name: "dirty globstar",
args: args{
pattern: "/a**.txt",
matchingTargets: []string{"a.txt", "abc.txt", "a.txt/b.go", "a.txt/b.go/c.ar"},
nonMatchingTargets: []string{"a/b/.txt"},
},
},
{
name: "escaped globstar",
args: args{
pattern: "\\*\\*",
matchingTargets: []string{"**", "x/**", "**/y", "x/**/y"},
nonMatchingTargets: []string{"x"},
},
},
{
name: "partially escaped globstar",
args: args{
pattern: "*\\*",
matchingTargets: []string{"*", "**", "a*", "x/*", "x/a*", "*/y", "a*/", "x/*/y", "x/a*/y"},
nonMatchingTargets: []string{"x"},
},
},
{
name: "single wildchar",
args: args{
pattern: "/a.?xt",
matchingTargets: []string{"a.txt", "a.xxt/b.go", "a.xxt/b.go/c.ar"},
nonMatchingTargets: []string{"x/a.txt", "z/a.txt", "w/x/a.txt", "y/a.txt", "a./xt"},
},
},
{
name: "escaped single wildchar",
args: args{
pattern: "/a.\\?xt",
matchingTargets: []string{"a.?xt", "a.?xt/b.go", "a.?xt/b.go/c.ar"},
nonMatchingTargets: []string{"a.\\?xt", "a.txt", "x/a.?xt"},
},
},
{
name: "class",
args: args{
pattern: "/[abc].txt",
matchingTargets: []string{"a.txt", "b.txt", "c.txt"},
nonMatchingTargets: []string{"[a-c].txt", "d.txt", "A.txt"},
},
},
{
name: "range class",
args: args{
pattern: "/[a-c].txt",
matchingTargets: []string{"a.txt", "b.txt", "c.txt"},
nonMatchingTargets: []string{"[a-c].txt", "d.txt", "A.txt"},
},
},
{
name: "escaped class",
args: args{
pattern: "/\\[a-c\\].txt",
matchingTargets: []string{"[a-c].txt"},
nonMatchingTargets: []string{"\\[a-c\\].txt", "a.txt", "b.txt", "c.txt"},
},
},
{
name: "class escaped control chars",
args: args{
pattern: "/[\\!\\^\\-a-c].txt",
matchingTargets: []string{"a.txt", "b.txt", "c.txt", "^.txt", "!.txt", "-.txt"},
nonMatchingTargets: []string{"d.txt", "[\\!\\^\\-a-c].txt", "[!^-a-c].txt"},
},
},
{
name: "inverted class ^",
args: args{
pattern: "/[^a-c].txt",
matchingTargets: []string{"d.txt", "B.txt"},
nonMatchingTargets: []string{"a.txt", "b.txt", "c.txt", "[^a-c].txt", "[a-c].txt"},
},
},
{
name: "escaped inverted class ^",
args: args{
pattern: "/\\[^a-c\\].txt",
matchingTargets: []string{"[^a-c].txt"},
nonMatchingTargets: []string{"\\[^a-c\\].txt", "a.txt", "b.txt", "c.txt", "d.txt", "[a-c].txt"},
},
},
{
name: "inverted class !",
args: args{
pattern: "/[!a-c].txt",
matchingTargets: []string{"d.txt", "B.txt"},
nonMatchingTargets: []string{"a.txt", "b.txt", "c.txt", "[!a-c].txt", "[a-c].txt"},
},
},
{
name: "escaped inverted class !",
args: args{
pattern: "/\\[!a-c\\].txt",
matchingTargets: []string{"[!a-c].txt"},
nonMatchingTargets: []string{"\\[!a-c\\].txt", "a.txt", "b.txt", "c.txt", "d.txt", "[a-c].txt"},
},
},
{
name: "alternate matches",
args: args{
pattern: "/{a,b,[c-d],e?,f\\*}.txt",
matchingTargets: []string{
"a.txt", "b.txt", "c.txt", "d.txt", "e2.txt", "f*.txt", "a.txt/b.go", "a.txt/b.go/c.ar"},
nonMatchingTargets: []string{
"{a,b,[c-d],e?,f\\*}.txt", "{a,b,[c-d],e?,f*}.txt", "e.txt", "f.txt", "g.txt", "ab.txt"},
},
},
{
name: "space",
args: args{
pattern: "/a b.txt",
matchingTargets: []string{"a b.txt"},
nonMatchingTargets: []string{"a.txt", "b.txt", "ab.txt", "a b.txt"},
},
},
{
name: "tab",
args: args{
pattern: "/a b.txt",
matchingTargets: []string{"a b.txt"},
nonMatchingTargets: []string{"a.txt", "b.txt", "ab.txt", "a b.txt", "a b.txt"},
},
},
{
// Note: it's debatable which behavior is correct - for now keep doublestar default behavior on this.
// Keeping UT to ensure we don't accidentally change behavior.
name: "escaped backslash",
args: args{
pattern: "/a\\\\/b.txt",
matchingTargets: []string{"a\\/b.txt"},
nonMatchingTargets: []string{"a\\\\/b.txt", "a/b.txt", "a/b.txt/c.ar"},
},
},
{
// Note: it's debatable which behavior is correct - for now keep doublestar default behavior on this.
// Keeping UT to ensure we don't accidentally change behavior.
name: "escaped path separator",
args: args{
pattern: "/a\\/b.txt",
matchingTargets: []string{"a/b.txt", "a/b.txt/c.ar"},
nonMatchingTargets: []string{"a\\/b.txt"},
},
},
}
testMatch := func(pattern string, target string, want bool) {
got, err := match(pattern, target)
if err != nil {
t.Errorf("failed with error: %s", err)
} else if got != want {
t.Errorf("match(%q, %q) = %t but wanted %t)", pattern, target, got, want)
}
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for _, target := range tt.args.matchingTargets {
if len(target) > 0 && target[0] == '/' {
t.Errorf("target shouldn't start with leading '/'")
}
testMatch(tt.args.pattern, target, true)
testMatch(tt.args.pattern, "/"+target, true)
}
for _, target := range tt.args.nonMatchingTargets {
if len(target) > 0 && target[0] == '/' {
t.Errorf("target shouldn't start with leading '/'")
}
testMatch(tt.args.pattern, target, false)
testMatch(tt.args.pattern, "/"+target, false)
}
})
}
}