Skip to content

Commit f8992de

Browse files
theakshaypantchmouel
authored andcommitted
test(gitlab): add comprehensive ACL test coverage
Add table-driven tests for GitLab ACL functions achieving 100% coverage for all functions in acl.go, ensuring robust security validation for GitLab membership and OWNERS file checks. Signed-off-by: Akshay Pant <akshay.akshaypant@gmail.com>
1 parent 269d2a7 commit f8992de

File tree

1 file changed

+321
-0
lines changed

1 file changed

+321
-0
lines changed

pkg/provider/gitlab/acl_test.go

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,3 +261,324 @@ func TestMembershipAPIFailureDoesNotCacheApiError(t *testing.T) {
261261
t.Fatalf("expected membership API to be called again after retry, got %d total calls (initial %d)", calls, initialCallCount)
262262
}
263263
}
264+
265+
func TestIsAllowedOwnersFile(t *testing.T) {
266+
tests := []struct {
267+
name string
268+
targetProjectID int
269+
sender string
270+
defaultBranch string
271+
ownersFile string
272+
ownersAliasesFile string
273+
ownersFileError bool
274+
ownersAliasesError bool
275+
ownersAliasesStatusCode int
276+
wantAllowed bool
277+
wantErr bool
278+
}{
279+
{
280+
name: "no owners file",
281+
targetProjectID: 5000,
282+
sender: "testuser",
283+
defaultBranch: "main",
284+
ownersFile: "",
285+
wantAllowed: false,
286+
wantErr: false,
287+
},
288+
{
289+
name: "owners file allows user",
290+
targetProjectID: 5000,
291+
sender: "testuser",
292+
defaultBranch: "main",
293+
ownersFile: "---\napprovers:\n - testuser\n",
294+
wantAllowed: true,
295+
wantErr: false,
296+
},
297+
{
298+
name: "owners file denies user",
299+
targetProjectID: 5000,
300+
sender: "testuser",
301+
defaultBranch: "main",
302+
ownersFile: "---\napprovers:\n - someoneelse\n",
303+
wantAllowed: false,
304+
wantErr: false,
305+
},
306+
{
307+
name: "owners file with aliases not found",
308+
targetProjectID: 5000,
309+
sender: "testuser",
310+
defaultBranch: "main",
311+
ownersFile: "---\napprovers:\n - testuser\n",
312+
ownersAliasesFile: "",
313+
wantAllowed: true,
314+
wantErr: false,
315+
},
316+
{
317+
name: "owners file with aliases file exists",
318+
targetProjectID: 5000,
319+
sender: "testuser",
320+
defaultBranch: "main",
321+
ownersFile: "---\napprovers:\n - team-lead\n",
322+
ownersAliasesFile: "---\naliases:\n team-lead:\n - testuser\n",
323+
wantAllowed: true,
324+
wantErr: false,
325+
},
326+
{
327+
name: "owners aliases returns error status",
328+
targetProjectID: 5000,
329+
sender: "testuser",
330+
defaultBranch: "main",
331+
ownersFile: "---\napprovers:\n - testuser\n",
332+
ownersAliasesError: true,
333+
ownersAliasesStatusCode: http.StatusUnauthorized,
334+
wantAllowed: false,
335+
wantErr: true,
336+
},
337+
{
338+
name: "owners aliases returns internal server error",
339+
targetProjectID: 5000,
340+
sender: "testuser",
341+
defaultBranch: "main",
342+
ownersFile: "---\napprovers:\n - testuser\n",
343+
ownersAliasesError: true,
344+
ownersAliasesStatusCode: http.StatusInternalServerError,
345+
wantAllowed: false,
346+
wantErr: true,
347+
},
348+
}
349+
350+
for _, tt := range tests {
351+
t.Run(tt.name, func(t *testing.T) {
352+
ctx, _ := rtesting.SetupFakeContext(t)
353+
354+
v := &Provider{
355+
targetProjectID: tt.targetProjectID,
356+
}
357+
358+
client, mux, tearDown := thelp.Setup(t)
359+
defer tearDown()
360+
v.gitlabClient = client
361+
362+
// Setup OWNERS file
363+
if tt.ownersFile != "" {
364+
thelp.MuxGetFile(mux, tt.targetProjectID, "OWNERS", tt.ownersFile, tt.ownersFileError)
365+
} else {
366+
// Return empty for missing OWNERS file
367+
thelp.MuxGetFile(mux, tt.targetProjectID, "OWNERS", "", false)
368+
}
369+
370+
// Setup OWNERS_ALIASES file
371+
switch {
372+
case tt.ownersAliasesError:
373+
// Setup error response for OWNERS_ALIASES
374+
path := fmt.Sprintf("/projects/%d/repository/files/OWNERS_ALIASES/raw", tt.targetProjectID)
375+
mux.HandleFunc(path, func(rw http.ResponseWriter, _ *http.Request) {
376+
rw.WriteHeader(tt.ownersAliasesStatusCode)
377+
_, _ = rw.Write([]byte(`{"error": "test error"}`))
378+
})
379+
case tt.ownersAliasesFile != "":
380+
thelp.MuxGetFile(mux, tt.targetProjectID, "OWNERS_ALIASES", tt.ownersAliasesFile, false)
381+
default:
382+
// Return 404 for missing OWNERS_ALIASES file
383+
path := fmt.Sprintf("/projects/%d/repository/files/OWNERS_ALIASES/raw", tt.targetProjectID)
384+
mux.HandleFunc(path, func(rw http.ResponseWriter, _ *http.Request) {
385+
rw.WriteHeader(http.StatusNotFound)
386+
_, _ = rw.Write([]byte(`{"error": "not found"}`))
387+
})
388+
}
389+
390+
ev := &info.Event{
391+
Sender: tt.sender,
392+
DefaultBranch: tt.defaultBranch,
393+
}
394+
395+
// Execute IsAllowedOwnersFile
396+
allowed, err := v.IsAllowedOwnersFile(ctx, ev)
397+
398+
// Verify error
399+
if (err != nil) != tt.wantErr {
400+
t.Errorf("IsAllowedOwnersFile() error = %v, wantErr %v", err, tt.wantErr)
401+
return
402+
}
403+
404+
// Verify result
405+
if allowed != tt.wantAllowed {
406+
t.Errorf("IsAllowedOwnersFile() = %v, want %v", allowed, tt.wantAllowed)
407+
}
408+
})
409+
}
410+
}
411+
412+
func TestCheckMembership(t *testing.T) {
413+
tests := []struct {
414+
name string
415+
targetProjectID int
416+
userID int
417+
apiAllowMember bool
418+
apiFailure bool
419+
ownersFile string
420+
sender string
421+
wantResult bool
422+
wantCached bool
423+
wantCachedValue bool
424+
verifyCacheNotSet bool
425+
verifyRetry bool
426+
}{
427+
{
428+
name: "gitlab member + owners allowed",
429+
targetProjectID: 5000,
430+
userID: 1000,
431+
apiAllowMember: true,
432+
ownersFile: "---\napprovers:\n - testuser\n",
433+
sender: "testuser",
434+
wantResult: true,
435+
wantCached: true,
436+
wantCachedValue: true,
437+
},
438+
{
439+
name: "gitlab member + owners denied",
440+
targetProjectID: 5000,
441+
userID: 1000,
442+
apiAllowMember: true,
443+
ownersFile: "---\napprovers:\n - someoneelse\n",
444+
sender: "testuser",
445+
wantResult: true,
446+
wantCached: true,
447+
wantCachedValue: true,
448+
},
449+
{
450+
name: "gitlab not member + owners allowed",
451+
targetProjectID: 5000,
452+
userID: 1000,
453+
apiAllowMember: false,
454+
ownersFile: "---\napprovers:\n - testuser\n",
455+
sender: "testuser",
456+
wantResult: true,
457+
wantCached: true,
458+
wantCachedValue: true,
459+
},
460+
{
461+
name: "gitlab not member + owners denied",
462+
targetProjectID: 5000,
463+
userID: 1000,
464+
apiAllowMember: false,
465+
ownersFile: "---\napprovers:\n - someoneelse\n",
466+
sender: "testuser",
467+
wantResult: false,
468+
wantCached: true,
469+
wantCachedValue: false,
470+
},
471+
{
472+
name: "api failure + owners allowed",
473+
targetProjectID: 5000,
474+
userID: 1000,
475+
apiFailure: true,
476+
ownersFile: "---\napprovers:\n - testuser\n",
477+
sender: "testuser",
478+
wantResult: true,
479+
verifyCacheNotSet: true,
480+
},
481+
{
482+
name: "api failure + owners denied",
483+
targetProjectID: 5000,
484+
userID: 1000,
485+
apiFailure: true,
486+
ownersFile: "---\napprovers:\n - someoneelse\n",
487+
sender: "testuser",
488+
wantResult: false,
489+
verifyCacheNotSet: true,
490+
verifyRetry: true,
491+
},
492+
{
493+
name: "cache initialization",
494+
targetProjectID: 5000,
495+
userID: 1000,
496+
apiAllowMember: true,
497+
sender: "testuser",
498+
wantResult: true,
499+
wantCached: true,
500+
wantCachedValue: true,
501+
},
502+
}
503+
504+
for _, tt := range tests {
505+
t.Run(tt.name, func(t *testing.T) {
506+
ctx, _ := rtesting.SetupFakeContext(t)
507+
508+
v := &Provider{
509+
targetProjectID: tt.targetProjectID,
510+
userID: tt.userID,
511+
memberCache: nil, // Start with nil cache to test lazy initialization
512+
}
513+
514+
client, mux, tearDown := thelp.Setup(t)
515+
defer tearDown()
516+
v.gitlabClient = client
517+
518+
var callCount int
519+
// Setup API response
520+
switch {
521+
case tt.apiFailure:
522+
path := fmt.Sprintf("/projects/%d/members/all/%d", tt.targetProjectID, tt.userID)
523+
mux.HandleFunc(path, func(rw http.ResponseWriter, _ *http.Request) {
524+
callCount++
525+
rw.WriteHeader(http.StatusInternalServerError)
526+
_, _ = rw.Write([]byte(`{"error": "internal server error"}`))
527+
})
528+
case tt.apiAllowMember:
529+
thelp.MuxAllowUserID(mux, tt.targetProjectID, tt.userID)
530+
default:
531+
thelp.MuxDisallowUserID(mux, tt.targetProjectID, tt.userID)
532+
}
533+
534+
// Setup OWNERS file
535+
if tt.ownersFile != "" {
536+
thelp.MuxGetFile(mux, tt.targetProjectID, "OWNERS", tt.ownersFile, false)
537+
}
538+
539+
ev := &info.Event{
540+
Sender: tt.sender,
541+
DefaultBranch: "main",
542+
}
543+
544+
// Execute checkMembership
545+
result := v.checkMembership(ctx, ev, tt.userID)
546+
547+
// Verify result
548+
if result != tt.wantResult {
549+
t.Errorf("checkMembership() = %v, want %v", result, tt.wantResult)
550+
}
551+
552+
// Verify cache behavior
553+
if tt.verifyCacheNotSet {
554+
if _, ok := v.memberCache[tt.userID]; ok {
555+
t.Errorf("expected result NOT to be cached when API fails")
556+
}
557+
} else if tt.wantCached {
558+
cached, ok := v.memberCache[tt.userID]
559+
if !ok {
560+
t.Errorf("expected result to be cached")
561+
} else if cached != tt.wantCachedValue {
562+
t.Errorf("cached value = %v, want %v", cached, tt.wantCachedValue)
563+
}
564+
}
565+
566+
// Verify cache was initialized
567+
if v.memberCache == nil {
568+
t.Errorf("expected memberCache to be initialized")
569+
}
570+
571+
// Verify retry behavior for API failures
572+
if tt.verifyRetry {
573+
initialCallCount := callCount
574+
result = v.checkMembership(ctx, ev, tt.userID)
575+
if result != tt.wantResult {
576+
t.Errorf("checkMembership() on retry = %v, want %v", result, tt.wantResult)
577+
}
578+
if callCount <= initialCallCount {
579+
t.Errorf("expected API to be called again (not cached), but call count did not increase")
580+
}
581+
}
582+
})
583+
}
584+
}

0 commit comments

Comments
 (0)