From 5082e5d3f300ffaf05c27a9c2369eef47a674473 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Tue, 29 Jun 2021 11:59:49 +0200 Subject: [PATCH] Use OOP-ish style for SonarQube webhook handling Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/webhook_handler/main_test.go | 14 +++++++ .../sonarqube.go | 30 ++++++++++++--- .../sonarqube_test.go | 21 ++++++---- internal/webhooks/sonarqube/webhook.go | 38 ++++++++----------- internal/webhooks/sonarqube/webhook_test.go | 5 +-- 5 files changed, 70 insertions(+), 38 deletions(-) create mode 100644 internal/webhook_handler/main_test.go rename internal/{webhooks/sonarqube => webhook_handler}/sonarqube.go (66%) rename internal/{webhooks/sonarqube => webhook_handler}/sonarqube_test.go (83%) diff --git a/internal/webhook_handler/main_test.go b/internal/webhook_handler/main_test.go new file mode 100644 index 0000000..9814e29 --- /dev/null +++ b/internal/webhook_handler/main_test.go @@ -0,0 +1,14 @@ +package webhook_handler + +import ( + "log" + "io/ioutil" + "os" + "testing" +) + +// SETUP: mute logs +func TestMain(m *testing.M) { + log.SetOutput(ioutil.Discard) + os.Exit(m.Run()) +} diff --git a/internal/webhooks/sonarqube/sonarqube.go b/internal/webhook_handler/sonarqube.go similarity index 66% rename from internal/webhooks/sonarqube/sonarqube.go rename to internal/webhook_handler/sonarqube.go index 996d85d..6422ec7 100644 --- a/internal/webhooks/sonarqube/sonarqube.go +++ b/internal/webhook_handler/sonarqube.go @@ -1,4 +1,4 @@ -package sonarqube +package webhook_handler import ( "fmt" @@ -9,9 +9,16 @@ import ( "strings" "gitea-sonarqube-pr-bot/internal/settings" + webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" ) -func inProjectsMapping(p []settings.Project, n string) bool { +type fetchDetailsType func(w *webhook.Webhook) + +type SonarQubeWebhookHandler struct { + fetchDetails fetchDetailsType +} + +func (_ *SonarQubeWebhookHandler) inProjectsMapping(p []settings.Project, n string) bool { for _, proj := range p { if proj.SonarQube.Key == n { return true @@ -21,11 +28,11 @@ func inProjectsMapping(p []settings.Project, n string) bool { return false } -func HandleWebhook(rw http.ResponseWriter, r *http.Request) { +func (h *SonarQubeWebhookHandler) Handle(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-Type", "application/json") project := r.Header.Get("X-SonarQube-Project") - if !inProjectsMapping(settings.Projects, project) { + if !h.inProjectsMapping(settings.Projects, project) { log.Printf("Received hook for project '%s' which is not configured. Request ignored.", project) rw.WriteHeader(http.StatusOK) @@ -44,7 +51,7 @@ func HandleWebhook(rw http.ResponseWriter, r *http.Request) { return } - w, ok := NewWebhook(raw) + w, ok := webhook.New(raw) if !ok { rw.WriteHeader(http.StatusUnprocessableEntity) io.WriteString(rw, `{"message": "Error parsing POST body."}`) @@ -60,5 +67,16 @@ func HandleWebhook(rw http.ResponseWriter, r *http.Request) { return } - log.Printf("%s", w) + h.fetchDetails(w) +} + + +func fetchDetails(w *webhook.Webhook) { + log.Printf("Hello from the original one: %s", w) +} + +func NewSonarQubeWebhookHandler() *SonarQubeWebhookHandler { + return &SonarQubeWebhookHandler{ + fetchDetails: fetchDetails, + } } diff --git a/internal/webhooks/sonarqube/sonarqube_test.go b/internal/webhook_handler/sonarqube_test.go similarity index 83% rename from internal/webhooks/sonarqube/sonarqube_test.go rename to internal/webhook_handler/sonarqube_test.go index 60fc9f0..78c0bd0 100644 --- a/internal/webhooks/sonarqube/sonarqube_test.go +++ b/internal/webhook_handler/sonarqube_test.go @@ -1,16 +1,23 @@ -package sonarqube +package webhook_handler import ( "bytes" + "log" "net/http" "net/http/httptest" "testing" - "gitea-sonarqube-pr-bot/internal/settings" "github.com/stretchr/testify/assert" + "gitea-sonarqube-pr-bot/internal/settings" + webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" ) func withValidRequestData(t *testing.T) (*http.Request, *httptest.ResponseRecorder, http.HandlerFunc) { + webhookHandler := NewSonarQubeWebhookHandler() + webhookHandler.fetchDetails = func(w *webhook.Webhook) { + log.Printf("Overridden fetchDetails") + } + jsonBody := []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`) req, err := http.NewRequest("POST", "/hooks/sonarqube", bytes.NewBuffer(jsonBody)) if err != nil { @@ -19,12 +26,12 @@ func withValidRequestData(t *testing.T) (*http.Request, *httptest.ResponseRecord req.Header.Set("X-SonarQube-Project", "pr-bot") rr := httptest.NewRecorder() - handler := http.HandlerFunc(HandleWebhook) + handler := http.HandlerFunc(webhookHandler.Handle) return req, rr, handler } -func TestHandleWebhookProjectMapped(t *testing.T) { +func TestHandleSonarQubeWebhookProjectMapped(t *testing.T) { settings.Projects = []settings.Project{ settings.Project{ SonarQube: struct{Key string}{ @@ -39,7 +46,7 @@ func TestHandleWebhookProjectMapped(t *testing.T) { assert.Equal(t, `{"message": "Processing data. See bot logs for details."}`, rr.Body.String()) } -func TestHandleWebhookProjectNotMapped(t *testing.T) { +func TestHandleSonarQubeWebhookProjectNotMapped(t *testing.T) { settings.Projects = []settings.Project{ settings.Project{ SonarQube: struct{Key string}{ @@ -54,7 +61,7 @@ func TestHandleWebhookProjectNotMapped(t *testing.T) { assert.Equal(t, `{"message": "Project 'pr-bot' not in configured list. Request ignored."}`, rr.Body.String()) } -func TestHandleWebhookInvalidJSONBody(t *testing.T) { +func TestHandleSonarQubeWebhookInvalidJSONBody(t *testing.T) { settings.Projects = []settings.Project{ settings.Project{ SonarQube: struct{Key string}{ @@ -71,7 +78,7 @@ func TestHandleWebhookInvalidJSONBody(t *testing.T) { req.Header.Set("X-SonarQube-Project", "pr-bot") rr := httptest.NewRecorder() - handler := http.HandlerFunc(HandleWebhook) + handler := http.HandlerFunc(NewSonarQubeWebhookHandler().Handle) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusUnprocessableEntity, rr.Code) diff --git a/internal/webhooks/sonarqube/webhook.go b/internal/webhooks/sonarqube/webhook.go index acf34d4..16010b3 100644 --- a/internal/webhooks/sonarqube/webhook.go +++ b/internal/webhooks/sonarqube/webhook.go @@ -10,38 +10,32 @@ import ( type Webhook struct { ServerUrl string `mapstructure:"serverUrl"` Revision string - Branch branch - QualityGate qualityGate `mapstructure:"qualityGate"` + Branch struct { + Name string + Type string + Url string + } + QualityGate struct { + Status string + Conditions []struct { + Metric string + Status string + } + } `mapstructure:"qualityGate"` } -type branch struct { - Name string - Type string - Url string -} - -type qualityGate struct { - Status string - Conditions []condition -} - -type condition struct { - Metric string - Status string -} - -func NewWebhook(raw []byte) (*Webhook, bool) { +func New(raw []byte) (*Webhook, bool) { v := viper.New() v.SetConfigType("json") v.ReadConfig(bytes.NewBuffer(raw)) - w := Webhook{} + w := &Webhook{} err := v.Unmarshal(&w) if err != nil { log.Printf("Error parsing SonarQube webhook: %s", err.Error()) - return nil, false + return w, false } - return &w, true + return w, true } diff --git a/internal/webhooks/sonarqube/webhook_test.go b/internal/webhooks/sonarqube/webhook_test.go index 30164f8..020b543 100644 --- a/internal/webhooks/sonarqube/webhook_test.go +++ b/internal/webhooks/sonarqube/webhook_test.go @@ -8,7 +8,7 @@ import ( func TestNewWebhook(t *testing.T) { raw := []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`) - response, ok := NewWebhook(raw) + response, ok := New(raw) assert.NotNil(t, response) assert.True(t, ok) @@ -16,8 +16,7 @@ func TestNewWebhook(t *testing.T) { func TestNewWebhookInvalidJSON(t *testing.T) { raw := []byte(`{ "serverUrl": ["invalid-server-url-content"] }`) - response, ok := NewWebhook(raw) + _, ok := New(raw) - assert.Nil(t, response) assert.False(t, ok) }