package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "net/http/httptest" "strings" "testing" "time" ) const secret = "test-secret" func sign(body string) string { mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(body)) return hex.EncodeToString(mac.Sum(nil)) } type call struct{ repo, sha, by string } func TestHandler(t *testing.T) { merged := `{"action":"closed","pull_request":{"merged":true,"base_branch":"main","merge_commit_sha":"0bfb14ee4067"},"repository":{"full_name":"bajdev/posts"},"sender":{"username":"bajdev"}}` // Every release is of origin/main, whatever merge_commit_sha says: Gogs // puts the head branch's commit there, not the merge commit. withHead := call{"bajdev/posts", "origin/main", "PR merged by bajdev (head 0bfb14e)"} tests := []struct { name, method, event, body, sig string status int want *call // nil: no release }{ {"merged into main", "POST", "pull_request", merged, "", 202, &withHead}, {"no merge commit sha", "POST", "pull_request", strings.Replace(merged, `"0bfb14ee4067"`, `""`, 1), "", 202, &call{"bajdev/posts", "origin/main", "PR merged by bajdev"}}, {"bad signature", "POST", "pull_request", merged, "deadbeef", 401, nil}, {"missing signature", "POST", "pull_request", merged, "-", 401, nil}, {"GET", "GET", "pull_request", merged, "", 405, nil}, {"push event", "POST", "push", merged, "", 204, nil}, {"opened", "POST", "pull_request", strings.Replace(merged, `"closed"`, `"opened"`, 1), "", 204, nil}, {"closed without merge", "POST", "pull_request", strings.Replace(merged, `"merged":true`, `"merged":false`, 1), "", 204, nil}, {"merged into another branch", "POST", "pull_request", strings.Replace(merged, `"main"`, `"dev"`, 1), "", 204, nil}, {"no repository", "POST", "pull_request", strings.Replace(merged, `"bajdev/posts"`, `""`, 1), "", 204, nil}, {"not JSON", "POST", "pull_request", "not json", "", 204, nil}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { calls := make(chan call, 1) h := handler([]byte(secret), func(repo, sha, by string) { calls <- call{repo, sha, by} }) req := httptest.NewRequest(tt.method, "/deploy", strings.NewReader(tt.body)) req.Header.Set("X-Gogs-Event", tt.event) switch tt.sig { case "": req.Header.Set("X-Gogs-Signature", sign(tt.body)) case "-": default: req.Header.Set("X-Gogs-Signature", tt.sig) } rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != tt.status { t.Fatalf("status = %d, want %d", rec.Code, tt.status) } select { case c := <-calls: if tt.want == nil { t.Fatalf("unexpected release %+v", c) } if c != *tt.want { t.Errorf("release called with %+v, want %+v", c, *tt.want) } case <-time.After(200 * time.Millisecond): if tt.want != nil { t.Fatal("release was not called") } } }) } } func TestReleasesRunOneAtATime(t *testing.T) { body := `{"action":"closed","pull_request":{"merged":true,"base_branch":"main","merge_commit_sha":"x"},"repository":{"full_name":"a/b"}}` running, maxRunning := 0, 0 done := make(chan struct{}, 3) h := handler([]byte(secret), func(repo, sha, by string) { // The handler's mutex is held here, so these updates don't race. running++ if running > maxRunning { maxRunning = running } time.Sleep(20 * time.Millisecond) running-- done <- struct{}{} }) for i := 0; i < 3; i++ { req := httptest.NewRequest("POST", "/deploy", strings.NewReader(body)) req.Header.Set("X-Gogs-Event", "pull_request") req.Header.Set("X-Gogs-Signature", sign(body)) h.ServeHTTP(httptest.NewRecorder(), req) } for i := 0; i < 3; i++ { <-done } if maxRunning != 1 { t.Errorf("max concurrent releases = %d, want 1", maxRunning) } }