@@ -3,7 +3,7 @@ package webhook
33import "testing"
44
55func TestRepoAllowlist_ExactMatch (t * testing.T ) {
6- al := NewRepoAllowlist ([]string {"org/svc" })
6+ al := NewRepoFilter ([]string {"org/svc" })
77
88 if ! al .IsAllowed ("org/svc" ) {
99 t .Error ("expected org/svc to be allowed" )
@@ -14,7 +14,7 @@ func TestRepoAllowlist_ExactMatch(t *testing.T) {
1414}
1515
1616func TestRepoAllowlist_WildcardOrg (t * testing.T ) {
17- al := NewRepoAllowlist ([]string {"org/*" })
17+ al := NewRepoFilter ([]string {"org/*" })
1818
1919 if ! al .IsAllowed ("org/svc" ) {
2020 t .Error ("expected org/svc to be allowed" )
@@ -28,7 +28,7 @@ func TestRepoAllowlist_WildcardOrg(t *testing.T) {
2828}
2929
3030func TestRepoAllowlist_Negation (t * testing.T ) {
31- al := NewRepoAllowlist ([]string {"org/*" , "!org/private" })
31+ al := NewRepoFilter ([]string {"org/*" , "!org/private" })
3232
3333 if ! al .IsAllowed ("org/svc" ) {
3434 t .Error ("expected org/svc to be allowed" )
@@ -39,7 +39,7 @@ func TestRepoAllowlist_Negation(t *testing.T) {
3939}
4040
4141func TestRepoAllowlist_EmptyAllowsNothing (t * testing.T ) {
42- al := NewRepoAllowlist ([]string {})
42+ al := NewRepoFilter ([]string {})
4343
4444 if al .IsAllowed ("org/svc" ) {
4545 t .Error ("expected org/svc to be denied when allowlist is empty" )
@@ -50,7 +50,7 @@ func TestRepoAllowlist_EmptyAllowsNothing(t *testing.T) {
5050}
5151
5252func TestRepoAllowlist_MultipleRules (t * testing.T ) {
53- al := NewRepoAllowlist ([]string {
53+ al := NewRepoFilter ([]string {
5454 "acme/*" ,
5555 "!acme/internal" ,
5656 "external/shared" ,
@@ -80,7 +80,7 @@ func TestRepoAllowlist_MultipleRules(t *testing.T) {
8080}
8181
8282func TestRepoAllowlist_GlobalWildcard (t * testing.T ) {
83- al := NewRepoAllowlist ([]string {"*/*" })
83+ al := NewRepoFilter ([]string {"*/*" })
8484
8585 if ! al .IsAllowed ("any-org/any-repo" ) {
8686 t .Error ("expected any-org/any-repo to be allowed with */*" )
@@ -94,7 +94,7 @@ func TestRepoAllowlist_GlobalWildcard(t *testing.T) {
9494}
9595
9696func TestRepoAllowlist_GlobalWildcardWithNegation (t * testing.T ) {
97- al := NewRepoAllowlist ([]string {"*/*" , "!secret/private" })
97+ al := NewRepoFilter ([]string {"*/*" , "!secret/private" })
9898
9999 if ! al .IsAllowed ("org/repo" ) {
100100 t .Error ("expected org/repo to be allowed" )
@@ -103,3 +103,137 @@ func TestRepoAllowlist_GlobalWildcardWithNegation(t *testing.T) {
103103 t .Error ("expected secret/private to be denied by negation" )
104104 }
105105}
106+
107+ func TestRepoFilter_PerRepoBranch_ExactRepo (t * testing.T ) {
108+ f := NewRepoFilterFromRules ([]RepoRule {
109+ {Pattern : "org/api" , Branches : []string {"main" , "develop" }},
110+ })
111+
112+ if ! f .IsAllowedRef ("org/api" , "refs/heads/main" ) {
113+ t .Error ("expected org/api + refs/heads/main to be allowed" )
114+ }
115+ if ! f .IsAllowedRef ("org/api" , "refs/heads/develop" ) {
116+ t .Error ("expected org/api + refs/heads/develop to be allowed" )
117+ }
118+ if f .IsAllowedRef ("org/api" , "refs/heads/feature/x" ) {
119+ t .Error ("expected org/api + refs/heads/feature/x to be denied" )
120+ }
121+ if f .IsAllowedRef ("org/other" , "refs/heads/main" ) {
122+ t .Error ("expected org/other to be denied (not in rules)" )
123+ }
124+ }
125+
126+ func TestRepoFilter_PerRepoBranch_WildcardRepo (t * testing.T ) {
127+ f := NewRepoFilterFromRules ([]RepoRule {
128+ {Pattern : "org/*" , Branches : []string {"main" }},
129+ })
130+
131+ if ! f .IsAllowedRef ("org/svc" , "refs/heads/main" ) {
132+ t .Error ("expected org/svc + main to be allowed" )
133+ }
134+ if f .IsAllowedRef ("org/svc" , "refs/heads/develop" ) {
135+ t .Error ("expected org/svc + develop to be denied" )
136+ }
137+ if f .IsAllowedRef ("other/svc" , "refs/heads/main" ) {
138+ t .Error ("expected other/svc to be denied" )
139+ }
140+ }
141+
142+ func TestRepoFilter_PerRepoBranch_DefaultBranches (t * testing.T ) {
143+ f := NewRepoFilterFromRules ([]RepoRule {
144+ {Pattern : "org/*" },
145+ })
146+
147+ if ! f .IsAllowedRef ("org/svc" , "refs/heads/main" ) {
148+ t .Error ("expected default branch main to be allowed" )
149+ }
150+ if ! f .IsAllowedRef ("org/svc" , "refs/heads/master" ) {
151+ t .Error ("expected default branch master to be allowed" )
152+ }
153+ if f .IsAllowedRef ("org/svc" , "refs/heads/develop" ) {
154+ t .Error ("expected develop to be denied when no branches configured" )
155+ }
156+ }
157+
158+ func TestRepoFilter_PerRepoBranch_GlobPattern (t * testing.T ) {
159+ f := NewRepoFilterFromRules ([]RepoRule {
160+ {Pattern : "org/web" , Branches : []string {"release/*" }},
161+ })
162+
163+ if ! f .IsAllowedRef ("org/web" , "refs/heads/release/v1.0" ) {
164+ t .Error ("expected release/v1.0 to match release/*" )
165+ }
166+ if ! f .IsAllowedRef ("org/web" , "refs/heads/release/hotfix" ) {
167+ t .Error ("expected release/hotfix to match release/*" )
168+ }
169+ if f .IsAllowedRef ("org/web" , "refs/heads/main" ) {
170+ t .Error ("expected main to be denied when only release/* configured" )
171+ }
172+ }
173+
174+ func TestRepoFilter_PerRepoBranch_FirstMatchWins (t * testing.T ) {
175+ f := NewRepoFilterFromRules ([]RepoRule {
176+ {Pattern : "org/api" , Branches : []string {"develop" }},
177+ {Pattern : "org/*" , Branches : []string {"main" }},
178+ })
179+
180+ if ! f .IsAllowedRef ("org/api" , "refs/heads/develop" ) {
181+ t .Error ("expected org/api to use first match: develop" )
182+ }
183+ if f .IsAllowedRef ("org/api" , "refs/heads/main" ) {
184+ t .Error ("expected org/api NOT to fall through to org/* rule" )
185+ }
186+ if ! f .IsAllowedRef ("org/web" , "refs/heads/main" ) {
187+ t .Error ("expected org/web to match org/* rule with main" )
188+ }
189+ }
190+
191+ func TestRepoFilter_PerRepoBranch_Negation (t * testing.T ) {
192+ f := NewRepoFilterFromRules ([]RepoRule {
193+ {Pattern : "!org/private" },
194+ {Pattern : "org/*" , Branches : []string {"main" }},
195+ })
196+
197+ if f .IsAllowedRef ("org/private" , "refs/heads/main" ) {
198+ t .Error ("expected org/private to be denied by negation" )
199+ }
200+ if ! f .IsAllowedRef ("org/svc" , "refs/heads/main" ) {
201+ t .Error ("expected org/svc + main to be allowed" )
202+ }
203+ }
204+
205+ func TestRepoFilter_PerRepoBranch_EmptyRules (t * testing.T ) {
206+ f := NewRepoFilterFromRules ([]RepoRule {})
207+
208+ if f .IsAllowedRef ("org/svc" , "refs/heads/main" ) {
209+ t .Error ("expected empty rules to deny everything" )
210+ }
211+ }
212+
213+ func TestParseRepoRules (t * testing.T ) {
214+ tests := []struct {
215+ input string
216+ want RepoRule
217+ }{
218+ {"org/api:main,develop" , RepoRule {Pattern : "org/api" , Branches : []string {"main" , "develop" }}},
219+ {"org/*" , RepoRule {Pattern : "org/*" , Branches : nil }},
220+ {"!org/private" , RepoRule {Pattern : "!org/private" , Branches : nil }},
221+ {"org/web:release/*" , RepoRule {Pattern : "org/web" , Branches : []string {"release/*" }}},
222+ }
223+
224+ for _ , tt := range tests {
225+ got := ParseRepoRule (tt .input )
226+ if got .Pattern != tt .want .Pattern {
227+ t .Errorf ("ParseRepoRule(%q).Pattern = %q, want %q" , tt .input , got .Pattern , tt .want .Pattern )
228+ }
229+ if len (got .Branches ) != len (tt .want .Branches ) {
230+ t .Errorf ("ParseRepoRule(%q).Branches = %v, want %v" , tt .input , got .Branches , tt .want .Branches )
231+ continue
232+ }
233+ for i , b := range got .Branches {
234+ if b != tt .want .Branches [i ] {
235+ t .Errorf ("ParseRepoRule(%q).Branches[%d] = %q, want %q" , tt .input , i , b , tt .want .Branches [i ])
236+ }
237+ }
238+ }
239+ }
0 commit comments