Use OOP-ish style for SonarQube webhook handling

Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com>
This commit is contained in:
justusbunsi 2021-06-29 11:59:49 +02:00
parent 86a644f31f
commit 5082e5d3f3
No known key found for this signature in database
GPG key ID: 990B348ECAC9C7DB
5 changed files with 70 additions and 38 deletions

View file

@ -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())
}

View file

@ -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,
}
}

View file

@ -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)

View file

@ -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
}

View file

@ -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)
}