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 }