github.com/quickfeed/quickfeed@v0.0.0-20240507093252-ed8ca812a09c/web/hooks/github_mock_test.go (about)

     1  package hooks_test
     2  
     3  import (
     4  	"net/http"
     5  	"sync"
     6  	"sync/atomic"
     7  
     8  	"github.com/google/go-github/v45/github"
     9  	"github.com/quickfeed/quickfeed/web/hooks"
    10  	"go.uber.org/zap"
    11  )
    12  
    13  // maxConcurrentTestRuns is the maximum number of concurrent test runs.
    14  const maxConcurrentTestRuns = 5
    15  
    16  // GitHubWebHook holds references and data for handling webhook events.
    17  type MockWebHook struct {
    18  	logger                *zap.SugaredLogger
    19  	secret                string
    20  	sem                   chan struct{} // counting semaphore: limit concurrent test runs to maxConcurrentTestRuns
    21  	dup                   *hooks.Duplicates
    22  	totalCnt              int32
    23  	currentConcurrencyCnt int32
    24  	wg                    *sync.WaitGroup
    25  }
    26  
    27  // NewMockWebHook creates a new webhook to handle POST requests to the QuickFeed server.
    28  func NewMockWebHook(logger *zap.SugaredLogger, secret string) *MockWebHook {
    29  	return &MockWebHook{
    30  		logger: logger,
    31  		secret: secret,
    32  		sem:    make(chan struct{}, maxConcurrentTestRuns),
    33  		dup:    hooks.NewDuplicateMap(),
    34  		wg:     &sync.WaitGroup{},
    35  	}
    36  }
    37  
    38  // Handle take POST requests from GitHub, representing Push events
    39  // associated with course repositories, which then triggers various
    40  // actions on the QuickFeed backend.
    41  func (wh *MockWebHook) Handle() http.HandlerFunc {
    42  	return func(w http.ResponseWriter, r *http.Request) {
    43  		payload, err := github.ValidatePayload(r, []byte(wh.secret))
    44  		if err != nil {
    45  			wh.logger.Errorf("Error in request body: %v", err)
    46  			w.WriteHeader(http.StatusUnauthorized)
    47  			return
    48  		}
    49  		defer r.Body.Close()
    50  
    51  		event, err := github.ParseWebHook(github.WebHookType(r), payload)
    52  		if err != nil {
    53  			wh.logger.Errorf("Could not parse github webhook: %v", err)
    54  			w.WriteHeader(http.StatusBadRequest)
    55  			return
    56  		}
    57  
    58  		switch e := event.(type) {
    59  		case *github.PushEvent:
    60  			commitID := e.GetHeadCommit().GetID()
    61  			wh.logger.Debugf("Received push event: %s", commitID)
    62  			if wh.dup.Duplicate(commitID) {
    63  				wh.logger.Debugf("Ignoring duplicate push event: %s", commitID)
    64  				return
    65  			}
    66  			// The counting semaphore limits concurrency to maxConcurrentTestRuns.
    67  			// This should also allow webhook events to return quickly to GitHub, avoiding timeouts.
    68  			wh.wg.Add(1)
    69  			go func() {
    70  				wh.sem <- struct{}{} // acquire semaphore
    71  				atomic.AddInt32(&wh.currentConcurrencyCnt, 1)
    72  				wh.handlePush(e)
    73  				<-wh.sem // release semaphore
    74  				atomic.AddInt32(&wh.currentConcurrencyCnt, -1)
    75  				wh.dup.Remove(commitID)
    76  				wh.wg.Done()
    77  			}()
    78  		default:
    79  			wh.logger.Debugf("Ignored event type %s", github.WebHookType(r))
    80  		}
    81  	}
    82  }
    83  
    84  func (wh *MockWebHook) handlePush(payload *github.PushEvent) {
    85  	curCnt := atomic.AddInt32(&wh.totalCnt, 1)
    86  	wh.logger.Debugf("Received push event on %s / %d", payload.GetRepo().GetName(), curCnt)
    87  }