Skip to content

Commit 5c931be

Browse files
authored
Add Invalid Owners Check (#34)
1 parent f3a459b commit 5c931be

5 files changed

Lines changed: 317 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.DS_Store

checkers/invalidowner.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package checkers
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
"strings"
7+
8+
"github.com/fmenezes/codeowners"
9+
)
10+
11+
const invalidOwnerCheckerName string = "InvalidOwner"
12+
13+
func init() {
14+
codeowners.RegisterChecker(invalidOwnerCheckerName, InvalidOwner{})
15+
}
16+
17+
// InvalidOwner represents checker to decide validate owners in each of CODEOWNERS lines
18+
type InvalidOwner struct{}
19+
20+
func ownerValid(owner string) bool {
21+
var emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
22+
if emailRegex.MatchString(owner) {
23+
return true
24+
}
25+
26+
if owner[0] != byte('@') { // should be @company/user or @user
27+
return false
28+
}
29+
30+
parts := strings.Split(owner[1:], "/")
31+
if len(parts) > 2 { // should be user or company/user
32+
return false
33+
}
34+
35+
var githubUsernameRegex = regexp.MustCompile("^[A-Za-z0-9](?:-?[A-Za-z0-9])*$")
36+
for _, username := range parts {
37+
if len(username) > 39 || !githubUsernameRegex.MatchString(username) {
38+
return false
39+
}
40+
}
41+
42+
return true
43+
}
44+
45+
// CheckLine runs this InvalidOwner's check against each line
46+
func (c InvalidOwner) CheckLine(lineNo int, line string) []codeowners.CheckResult {
47+
var results []codeowners.CheckResult
48+
49+
_, owners := codeowners.ParseLine(line)
50+
51+
for _, owner := range owners {
52+
if ownerValid(owner) {
53+
continue
54+
}
55+
result := codeowners.CheckResult{
56+
Position: codeowners.Position{
57+
StartLine: lineNo,
58+
EndLine: lineNo,
59+
StartColumn: strings.Index(line, owner) + 1,
60+
},
61+
Message: fmt.Sprintf("Owner '%s' is invalid", owner),
62+
Severity: codeowners.Error,
63+
CheckName: invalidOwnerCheckerName,
64+
}
65+
result.Position.EndColumn = result.Position.StartColumn + len(owner)
66+
67+
if results == nil {
68+
results = []codeowners.CheckResult{result}
69+
} else {
70+
results = append(results, result)
71+
}
72+
}
73+
74+
return results
75+
}

checkers/invalidowner_test.go

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
package checkers_test
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
7+
"github.com/fmenezes/codeowners"
8+
"github.com/fmenezes/codeowners/checkers"
9+
)
10+
11+
func TestInvalidOwnerCheckInvalidLongUsername(t *testing.T) {
12+
input := struct {
13+
lineNo int
14+
line string
15+
}{
16+
lineNo: 1,
17+
line: "filepattern myusernamemyusernamemyusernamemyusernamemyusername",
18+
}
19+
want := []codeowners.CheckResult{
20+
{
21+
Position: codeowners.Position{
22+
StartLine: 1,
23+
StartColumn: 13,
24+
EndLine: 1,
25+
EndColumn: 63,
26+
},
27+
Message: "Owner 'myusernamemyusernamemyusernamemyusernamemyusername' is invalid",
28+
Severity: codeowners.Error,
29+
CheckName: "InvalidOwner",
30+
},
31+
}
32+
33+
checker := checkers.InvalidOwner{}
34+
got := checker.CheckLine(input.lineNo, input.line)
35+
if !reflect.DeepEqual(got, want) {
36+
t.Errorf("Input: %v, Want: %v, Got: %v", input, want, got)
37+
}
38+
}
39+
40+
func TestInvalidOwnerCheckInvalidNoAt(t *testing.T) {
41+
input := struct {
42+
lineNo int
43+
line string
44+
}{
45+
lineNo: 1,
46+
line: "filepattern invalid-owner",
47+
}
48+
want := []codeowners.CheckResult{
49+
{
50+
Position: codeowners.Position{
51+
StartLine: 1,
52+
StartColumn: 13,
53+
EndLine: 1,
54+
EndColumn: 26,
55+
},
56+
Message: "Owner 'invalid-owner' is invalid",
57+
Severity: codeowners.Error,
58+
CheckName: "InvalidOwner",
59+
},
60+
}
61+
62+
checker := checkers.InvalidOwner{}
63+
got := checker.CheckLine(input.lineNo, input.line)
64+
if !reflect.DeepEqual(got, want) {
65+
t.Errorf("Input: %v, Want: %v, Got: %v", input, want, got)
66+
}
67+
}
68+
69+
func TestInvalidOwnerCheckInvalidHyphens(t *testing.T) {
70+
input := struct {
71+
lineNo int
72+
line string
73+
}{
74+
lineNo: 1,
75+
line: "filepattern @invalid--owner",
76+
}
77+
want := []codeowners.CheckResult{
78+
{
79+
Position: codeowners.Position{
80+
StartLine: 1,
81+
StartColumn: 13,
82+
EndLine: 1,
83+
EndColumn: 28,
84+
},
85+
Message: "Owner '@invalid--owner' is invalid",
86+
Severity: codeowners.Error,
87+
CheckName: "InvalidOwner",
88+
},
89+
}
90+
91+
checker := checkers.InvalidOwner{}
92+
got := checker.CheckLine(input.lineNo, input.line)
93+
if !reflect.DeepEqual(got, want) {
94+
t.Errorf("Input: %v, Want: %v, Got: %v", input, want, got)
95+
}
96+
}
97+
98+
func TestInvalidOwnerCheckInvalidFormat(t *testing.T) {
99+
input := struct {
100+
lineNo int
101+
line string
102+
}{
103+
lineNo: 1,
104+
line: "filepattern @org/invalid/owner",
105+
}
106+
want := []codeowners.CheckResult{
107+
{
108+
Position: codeowners.Position{
109+
StartLine: 1,
110+
StartColumn: 13,
111+
EndLine: 1,
112+
EndColumn: 31,
113+
},
114+
Message: "Owner '@org/invalid/owner' is invalid",
115+
Severity: codeowners.Error,
116+
CheckName: "InvalidOwner",
117+
},
118+
}
119+
120+
checker := checkers.InvalidOwner{}
121+
got := checker.CheckLine(input.lineNo, input.line)
122+
if !reflect.DeepEqual(got, want) {
123+
t.Errorf("Input: %v, Want: %v, Got: %v", input, want, got)
124+
}
125+
}
126+
func TestInvalidOwnerCheckInvalidTrailingHyphen(t *testing.T) {
127+
input := struct {
128+
lineNo int
129+
line string
130+
}{
131+
lineNo: 1,
132+
line: "filepattern @invalid-owner-",
133+
}
134+
want := []codeowners.CheckResult{
135+
{
136+
Position: codeowners.Position{
137+
StartLine: 1,
138+
StartColumn: 13,
139+
EndLine: 1,
140+
EndColumn: 28,
141+
},
142+
Message: "Owner '@invalid-owner-' is invalid",
143+
Severity: codeowners.Error,
144+
CheckName: "InvalidOwner",
145+
},
146+
}
147+
148+
checker := checkers.InvalidOwner{}
149+
got := checker.CheckLine(input.lineNo, input.line)
150+
if !reflect.DeepEqual(got, want) {
151+
t.Errorf("Input: %v, Want: %v, Got: %v", input, want, got)
152+
}
153+
}
154+
155+
func TestInvalidOwnerCheckMultipleInvalid(t *testing.T) {
156+
input := struct {
157+
lineNo int
158+
line string
159+
}{
160+
lineNo: 1,
161+
line: "filepattern invalid-owner another-invalid-owner",
162+
}
163+
want := []codeowners.CheckResult{
164+
{
165+
Position: codeowners.Position{
166+
StartLine: 1,
167+
StartColumn: 13,
168+
EndLine: 1,
169+
EndColumn: 26,
170+
},
171+
Message: "Owner 'invalid-owner' is invalid",
172+
Severity: codeowners.Error,
173+
CheckName: "InvalidOwner",
174+
},
175+
{
176+
Position: codeowners.Position{
177+
StartLine: 1,
178+
StartColumn: 27,
179+
EndLine: 1,
180+
EndColumn: 48,
181+
},
182+
Message: "Owner 'another-invalid-owner' is invalid",
183+
Severity: codeowners.Error,
184+
CheckName: "InvalidOwner",
185+
},
186+
}
187+
188+
checker := checkers.InvalidOwner{}
189+
got := checker.CheckLine(input.lineNo, input.line)
190+
if !reflect.DeepEqual(got, want) {
191+
t.Errorf("Input: %v, Want: %v, Got: %v", input, want, got)
192+
}
193+
}
194+
195+
func TestInvalidOwnerCheckPassUser(t *testing.T) {
196+
input := struct {
197+
lineNo int
198+
line string
199+
}{
200+
lineNo: 1,
201+
line: "filepattern @valid-owner",
202+
}
203+
204+
checker := checkers.InvalidOwner{}
205+
got := checker.CheckLine(input.lineNo, input.line)
206+
if got != nil {
207+
t.Errorf("Input: %v, Want: %v, Got: %v", input, nil, got)
208+
}
209+
}
210+
211+
func TestInvalidOwnerCheckPassEmail(t *testing.T) {
212+
input := struct {
213+
lineNo int
214+
line string
215+
}{
216+
lineNo: 1,
217+
line: "filepattern email@server.com",
218+
}
219+
220+
checker := checkers.InvalidOwner{}
221+
got := checker.CheckLine(input.lineNo, input.line)
222+
if got != nil {
223+
t.Errorf("Input: %v, Want: %v, Got: %v", input, nil, got)
224+
}
225+
}
226+
227+
func TestInvalidOwnerCheckPassUserOrg(t *testing.T) {
228+
input := struct {
229+
lineNo int
230+
line string
231+
}{
232+
lineNo: 1,
233+
line: "filepattern @org/valid-owner",
234+
}
235+
236+
checker := checkers.InvalidOwner{}
237+
got := checker.CheckLine(input.lineNo, input.line)
238+
if got != nil {
239+
t.Errorf("Input: %v, Want: %v, Got: %v", input, nil, got)
240+
}
241+
}

test/.DS_Store

-6 KB
Binary file not shown.

test/data/.DS_Store

-6 KB
Binary file not shown.

0 commit comments

Comments
 (0)