@@ -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 : "---\n approvers:\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 : "---\n approvers:\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 : "---\n approvers:\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 : "---\n approvers:\n - team-lead\n " ,
322+ ownersAliasesFile : "---\n aliases:\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 : "---\n approvers:\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 : "---\n approvers:\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 : "---\n approvers:\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 : "---\n approvers:\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 : "---\n approvers:\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 : "---\n approvers:\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 : "---\n approvers:\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 : "---\n approvers:\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