From e203034228ff0bd947cf0b8f029c091137556e59 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sat, 21 May 2022 18:21:05 +0200 Subject: [PATCH] Rewrite API entrypoint to be testable (#22) The current code base regarding API entrypoint is not testable as it directly connects to Gitea when creating the API endpoints. This prevented my from writing tests in the past for that part. As the SonarQube quality gate broke due to changes in the API entrypoint logic, tests are now required to satisfy the quality gate. Therefore, the instantiation of the API handlers is now decoupled from building the bot API endpoints and follows the same interface wrapper strategy as used for the Gitea API client. This makes it testable. Now, tests are written for the most parts of the API entrypoint. I've also noticed that there was much overhead within the tests for a non-implemented function `fetchDetails`. So I dropped that function for now. Signed-off-by: Steven Kriegler Co-authored-by: justusbunsi Reviewed-on: https://codeberg.org/justusbunsi/gitea-sonarqube-bot/pulls/22 --- cmd/gitea-sonarqube-bot/main.go | 16 +++- internal/api/gitea.go | 7 +- internal/api/main.go | 82 +++++++++----------- internal/api/main_test.go | 130 +++++++++++++++++++++++++++++--- internal/api/sonarqube.go | 34 ++++----- internal/api/sonarqube_test.go | 22 ++---- sonar-project.properties | 3 + 7 files changed, 204 insertions(+), 90 deletions(-) diff --git a/cmd/gitea-sonarqube-bot/main.go b/cmd/gitea-sonarqube-bot/main.go index 8c08d20..5c9eb4d 100644 --- a/cmd/gitea-sonarqube-bot/main.go +++ b/cmd/gitea-sonarqube-bot/main.go @@ -1,13 +1,17 @@ package main import ( + "fmt" "log" "os" "path" "gitea-sonarqube-pr-bot/internal/api" + giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" + sonarQubeSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" "gitea-sonarqube-pr-bot/internal/settings" + "github.com/fvbock/endless" "github.com/urfave/cli/v2" ) @@ -27,7 +31,7 @@ func main() { Name: "gitea-sonarqube-pr-bot", Usage: "Improve your experience with SonarQube and Gitea", Description: `By default, gitea-sonarqube-pr-bot will start running the webserver if no arguments are passed.`, - Action: api.Serve, + Action: serveApi, } err := app.Run(os.Args) @@ -35,3 +39,13 @@ func main() { log.Fatal(err) } } + +func serveApi(c *cli.Context) error { + fmt.Println("Hi! I'm the Gitea-SonarQube-PR bot. At your service.") + + giteaHandler := api.NewGiteaWebhookHandler(giteaSdk.New(), sonarQubeSdk.New()) + sqHandler := api.NewSonarQubeWebhookHandler(giteaSdk.New(), sonarQubeSdk.New()) + server := api.New(giteaHandler, sqHandler) + + return endless.ListenAndServe(":3000", server.Engine) +} diff --git a/internal/api/gitea.go b/internal/api/gitea.go index c96ffbc..13660b1 100644 --- a/internal/api/gitea.go +++ b/internal/api/gitea.go @@ -12,6 +12,11 @@ import ( webhook "gitea-sonarqube-pr-bot/internal/webhooks/gitea" ) +type GiteaWebhookHandlerInferface interface { + HandleSynchronize(rw http.ResponseWriter, r *http.Request) + HandleComment(rw http.ResponseWriter, r *http.Request) +} + type GiteaWebhookHandler struct { giteaSdk giteaSdk.GiteaSdkInterface sqSdk sqSdk.SonarQubeSdkInterface @@ -88,7 +93,7 @@ func (h *GiteaWebhookHandler) HandleComment(rw http.ResponseWriter, r *http.Requ w.ProcessData(h.giteaSdk, h.sqSdk) } -func NewGiteaWebhookHandler(g giteaSdk.GiteaSdkInterface, sq sqSdk.SonarQubeSdkInterface) *GiteaWebhookHandler { +func NewGiteaWebhookHandler(g giteaSdk.GiteaSdkInterface, sq sqSdk.SonarQubeSdkInterface) GiteaWebhookHandlerInferface { return &GiteaWebhookHandler{ giteaSdk: g, sqSdk: sq, diff --git a/internal/api/main.go b/internal/api/main.go index aa92dfe..7ae869f 100644 --- a/internal/api/main.go +++ b/internal/api/main.go @@ -1,32 +1,38 @@ package api import ( - "fmt" "net/http" - giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" - sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" - - "github.com/fvbock/endless" "github.com/gin-gonic/gin" - "github.com/urfave/cli/v2" ) -func addPingApi(r *gin.Engine) { - r.GET("/ping", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "message": "pong", - }) - }) -} - type validSonarQubeEndpointHeader struct { SonarQubeProject string `header:"X-SonarQube-Project" binding:"required"` } -func addSonarQubeEndpoint(r *gin.Engine) { - webhookHandler := NewSonarQubeWebhookHandler(giteaSdk.New(), sqSdk.New()) - r.POST("/hooks/sonarqube", func(c *gin.Context) { +type validGiteaEndpointHeader struct { + GiteaEvent string `header:"X-Gitea-Event" binding:"required"` +} + +type ApiServer struct { + Engine *gin.Engine + sonarQubeWebhookHandler SonarQubeWebhookHandlerInferface + giteaWebhookHandler GiteaWebhookHandlerInferface +} + +func (s *ApiServer) setup() { + s.Engine.Use(gin.Recovery()) + s.Engine.Use(gin.LoggerWithConfig(gin.LoggerConfig{ + SkipPaths: []string{"/ping", "/favicon.ico"}, + })) + + s.Engine.GET("/favicon.ico", func(c *gin.Context) { + c.Status(http.StatusNoContent) + }).GET("/ping", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "pong", + }) + }).POST("/hooks/sonarqube", func(c *gin.Context) { h := validSonarQubeEndpointHeader{} if err := c.ShouldBindHeader(&h); err != nil { @@ -34,17 +40,8 @@ func addSonarQubeEndpoint(r *gin.Engine) { return } - webhookHandler.Handle(c.Writer, c.Request) - }) -} - -type validGiteaEndpointHeader struct { - GiteaEvent string `header:"X-Gitea-Event" binding:"required"` -} - -func addGiteaEndpoint(r *gin.Engine) { - webhookHandler := NewGiteaWebhookHandler(giteaSdk.New(), sqSdk.New()) - r.POST("/hooks/gitea", func(c *gin.Context) { + s.sonarQubeWebhookHandler.Handle(c.Writer, c.Request) + }).POST("/hooks/gitea", func(c *gin.Context) { h := validGiteaEndpointHeader{} if err := c.ShouldBindHeader(&h); err != nil { @@ -54,9 +51,9 @@ func addGiteaEndpoint(r *gin.Engine) { switch h.GiteaEvent { case "pull_request": - webhookHandler.HandleSynchronize(c.Writer, c.Request) + s.giteaWebhookHandler.HandleSynchronize(c.Writer, c.Request) case "issue_comment": - webhookHandler.HandleComment(c.Writer, c.Request) + s.giteaWebhookHandler.HandleComment(c.Writer, c.Request) default: c.JSON(http.StatusOK, gin.H{ "message": "ignore unknown event", @@ -65,23 +62,14 @@ func addGiteaEndpoint(r *gin.Engine) { }) } -func Serve(c *cli.Context) error { - fmt.Println("Hi! I'm the Gitea-SonarQube-PR bot. At your service.") +func New(giteaHandler GiteaWebhookHandlerInferface, sonarQubeHandler SonarQubeWebhookHandlerInferface) *ApiServer { + s := &ApiServer{ + Engine: gin.New(), + giteaWebhookHandler: giteaHandler, + sonarQubeWebhookHandler: sonarQubeHandler, + } - r := gin.New() + s.setup() - r.Use(gin.Recovery()) - r.Use(gin.LoggerWithConfig(gin.LoggerConfig{ - SkipPaths: []string{"/ping", "/favicon.ico"}, - })) - - addPingApi(r) - addSonarQubeEndpoint(r) - addGiteaEndpoint(r) - - r.GET("/favicon.ico", func(c *gin.Context) { - c.Status(http.StatusNoContent) - }) - - return endless.ListenAndServe(":3000", r) + return s } diff --git a/internal/api/main_test.go b/internal/api/main_test.go index 8b27e47..046972a 100644 --- a/internal/api/main_test.go +++ b/internal/api/main_test.go @@ -1,26 +1,41 @@ package api import ( + "bytes" "io/ioutil" "log" + "net/http" + "net/http/httptest" "os" "testing" giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" "gitea-sonarqube-pr-bot/internal/settings" - webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) -// Default SDK mocking -type HandlerPartialMock struct { +type SonarQubeHandlerMock struct { mock.Mock } -func (h *HandlerPartialMock) fetchDetails(w *webhook.Webhook) { - h.Called(w) +func (h *SonarQubeHandlerMock) Handle(rw http.ResponseWriter, r *http.Request) { + h.Called(rw, r) +} + +type GiteaHandlerMock struct { + mock.Mock +} + +func (h *GiteaHandlerMock) HandleSynchronize(rw http.ResponseWriter, r *http.Request) { + h.Called(rw, r) +} + +func (h *GiteaHandlerMock) HandleComment(rw http.ResponseWriter, r *http.Request) { + h.Called(rw, r) } type GiteaSdkMock struct { @@ -65,12 +80,109 @@ func (h *SQSdkMock) ComposeGiteaComment(data *sqSdk.CommentComposeData) (string, return "", nil } -func defaultMockPreparation(h *HandlerPartialMock) { - h.On("fetchDetails", mock.Anything).Return(nil) -} - // SETUP: mute logs func TestMain(m *testing.M) { + gin.SetMode(gin.TestMode) log.SetOutput(ioutil.Discard) os.Exit(m.Run()) } + +func TestNonAPIRoutes(t *testing.T) { + router := New(new(GiteaHandlerMock), new(SonarQubeHandlerMock)) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/favicon.ico", nil) + router.Engine.ServeHTTP(w, req) + assert.Equal(t, http.StatusNoContent, w.Code) + + w = httptest.NewRecorder() + req, _ = http.NewRequest("GET", "/ping", nil) + router.Engine.ServeHTTP(w, req) + assert.Equal(t, http.StatusOK, w.Code) +} + +func TestSonarQubeAPIRouteMissingProjectHeader(t *testing.T) { + router := New(new(GiteaHandlerMock), new(SonarQubeHandlerMock)) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/hooks/sonarqube", bytes.NewBuffer([]byte(`{}`))) + router.Engine.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) +} + +func TestSonarQubeAPIRouteProcessing(t *testing.T) { + sonarQubeHandlerMock := new(SonarQubeHandlerMock) + sonarQubeHandlerMock.On("Handle", mock.Anything, mock.Anything).Return(nil) + + router := New(new(GiteaHandlerMock), sonarQubeHandlerMock) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/hooks/sonarqube", bytes.NewBuffer([]byte(`{}`))) + req.Header.Add("X-SonarQube-Project", "gitea-sonarqube-bot") + router.Engine.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + sonarQubeHandlerMock.AssertNumberOfCalls(t, "Handle", 1) +} + +func TestGiteaAPIRouteMissingEventHeader(t *testing.T) { + router := New(new(GiteaHandlerMock), new(SonarQubeHandlerMock)) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/hooks/gitea", bytes.NewBuffer([]byte(`{}`))) + router.Engine.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) +} + +func TestGiteaAPIRouteSynchronizeProcessing(t *testing.T) { + giteaHandlerMock := new(GiteaHandlerMock) + giteaHandlerMock.On("HandleSynchronize", mock.Anything, mock.Anything).Return(nil) + giteaHandlerMock.On("HandleComment", mock.Anything, mock.Anything).Return(nil) + + router := New(giteaHandlerMock, new(SonarQubeHandlerMock)) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/hooks/gitea", bytes.NewBuffer([]byte(`{}`))) + req.Header.Add("X-Gitea-Event", "pull_request") + router.Engine.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + giteaHandlerMock.AssertNumberOfCalls(t, "HandleSynchronize", 1) + giteaHandlerMock.AssertNumberOfCalls(t, "HandleComment", 0) +} + +func TestGiteaAPIRouteCommentProcessing(t *testing.T) { + giteaHandlerMock := new(GiteaHandlerMock) + giteaHandlerMock.On("HandleSynchronize", mock.Anything, mock.Anything).Return(nil) + giteaHandlerMock.On("HandleComment", mock.Anything, mock.Anything).Return(nil) + + router := New(giteaHandlerMock, new(SonarQubeHandlerMock)) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/hooks/gitea", bytes.NewBuffer([]byte(`{}`))) + req.Header.Add("X-Gitea-Event", "issue_comment") + router.Engine.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + giteaHandlerMock.AssertNumberOfCalls(t, "HandleSynchronize", 0) + giteaHandlerMock.AssertNumberOfCalls(t, "HandleComment", 1) +} + +func TestGiteaAPIRouteUnknownEvent(t *testing.T) { + giteaHandlerMock := new(GiteaHandlerMock) + giteaHandlerMock.On("HandleSynchronize", mock.Anything, mock.Anything).Return(nil) + giteaHandlerMock.On("HandleComment", mock.Anything, mock.Anything).Return(nil) + + router := New(giteaHandlerMock, new(SonarQubeHandlerMock)) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/hooks/gitea", bytes.NewBuffer([]byte(`{}`))) + req.Header.Add("X-Gitea-Event", "unknown") + router.Engine.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + giteaHandlerMock.AssertNumberOfCalls(t, "HandleSynchronize", 0) + giteaHandlerMock.AssertNumberOfCalls(t, "HandleComment", 0) +} diff --git a/internal/api/sonarqube.go b/internal/api/sonarqube.go index 77fbf30..6a73d49 100644 --- a/internal/api/sonarqube.go +++ b/internal/api/sonarqube.go @@ -14,10 +14,13 @@ import ( webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" ) +type SonarQubeWebhookHandlerInferface interface { + Handle(rw http.ResponseWriter, r *http.Request) +} + type SonarQubeWebhookHandler struct { - fetchDetails func(w *webhook.Webhook) - giteaSdk giteaSdk.GiteaSdkInterface - sqSdk sqSdk.SonarQubeSdkInterface + giteaSdk giteaSdk.GiteaSdkInterface + sqSdk sqSdk.SonarQubeSdkInterface } func (*SonarQubeWebhookHandler) inProjectsMapping(p []settings.Project, n string) (bool, int) { @@ -31,13 +34,6 @@ func (*SonarQubeWebhookHandler) inProjectsMapping(p []settings.Project, n string } func (h *SonarQubeWebhookHandler) processData(w *webhook.Webhook, repo settings.GiteaRepository) { - if strings.ToLower(w.Branch.Type) != "pull_request" { - log.Println("Ignore Hook for non-PR") - return - } - - h.fetchDetails(w) - status := giteaSdk.StatusOK if w.QualityGate.Status != "OK" { status = giteaSdk.StatusFailure @@ -96,19 +92,21 @@ func (h *SonarQubeWebhookHandler) Handle(rw http.ResponseWriter, r *http.Request // Send response to SonarQube at this point to ensure being within 10 seconds limit of webhook response timeout rw.WriteHeader(http.StatusOK) + + if strings.ToLower(w.Branch.Type) != "pull_request" { + io.WriteString(rw, `{"message": "Ignore Hook for non-PR analysis."}`) + log.Println("Ignore Hook for non-PR analysis") + return + } + io.WriteString(rw, `{"message": "Processing data. See bot logs for details."}`) h.processData(w, settings.Projects[pIdx].Gitea) } -func fetchDetails(w *webhook.Webhook) { - log.Printf("This method will load additional data from SonarQube based on PR %d", w.PRIndex) -} - -func NewSonarQubeWebhookHandler(g giteaSdk.GiteaSdkInterface, sq sqSdk.SonarQubeSdkInterface) *SonarQubeWebhookHandler { +func NewSonarQubeWebhookHandler(g giteaSdk.GiteaSdkInterface, sq sqSdk.SonarQubeSdkInterface) SonarQubeWebhookHandlerInferface { return &SonarQubeWebhookHandler{ - fetchDetails: fetchDetails, - giteaSdk: g, - sqSdk: sq, + giteaSdk: g, + sqSdk: sq, } } diff --git a/internal/api/sonarqube_test.go b/internal/api/sonarqube_test.go index ce5011e..d0643ef 100644 --- a/internal/api/sonarqube_test.go +++ b/internal/api/sonarqube_test.go @@ -11,12 +11,8 @@ import ( "github.com/stretchr/testify/assert" ) -func withValidSonarQubeRequestData(t *testing.T, mockPreparation func(*HandlerPartialMock), jsonBody []byte) (*http.Request, *httptest.ResponseRecorder, http.HandlerFunc, *HandlerPartialMock) { - partialMock := new(HandlerPartialMock) - mockPreparation(partialMock) - +func withValidSonarQubeRequestData(t *testing.T, jsonBody []byte) (*http.Request, *httptest.ResponseRecorder, http.HandlerFunc) { webhookHandler := NewSonarQubeWebhookHandler(new(GiteaSdkMock), new(SQSdkMock)) - webhookHandler.fetchDetails = partialMock.fetchDetails req, err := http.NewRequest("POST", "/hooks/sonarqube", bytes.NewBuffer(jsonBody)) if err != nil { @@ -27,7 +23,7 @@ func withValidSonarQubeRequestData(t *testing.T, mockPreparation func(*HandlerPa rr := httptest.NewRecorder() handler := http.HandlerFunc(webhookHandler.Handle) - return req, rr, handler, partialMock + return req, rr, handler } func TestHandleSonarQubeWebhookProjectMapped(t *testing.T) { @@ -38,7 +34,7 @@ func TestHandleSonarQubeWebhookProjectMapped(t *testing.T) { }, }, } - req, rr, handler, _ := withValidSonarQubeRequestData(t, defaultMockPreparation, []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, rr, handler := withValidSonarQubeRequestData(t, []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": {} }`)) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) @@ -53,7 +49,7 @@ func TestHandleSonarQubeWebhookProjectNotMapped(t *testing.T) { }, }, } - req, rr, handler, _ := withValidSonarQubeRequestData(t, defaultMockPreparation, []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, rr, handler := withValidSonarQubeRequestData(t, []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": {} }`)) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) @@ -69,7 +65,7 @@ func TestHandleSonarQubeWebhookInvalidJSONBody(t *testing.T) { }, } - req, rr, handler, _ := withValidSonarQubeRequestData(t, defaultMockPreparation, []byte(`{ "serverUrl": ["invalid-server-url-content"] }`)) + req, rr, handler := withValidSonarQubeRequestData(t, []byte(`{ "serverUrl": ["invalid-server-url-content"] }`)) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusUnprocessableEntity, rr.Code) @@ -85,12 +81,11 @@ func TestHandleSonarQubeWebhookForPullRequest(t *testing.T) { }, } - req, rr, handler, partialMock := withValidSonarQubeRequestData(t, defaultMockPreparation, []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, rr, handler := withValidSonarQubeRequestData(t, []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": {} }`)) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, `{"message": "Processing data. See bot logs for details."}`, rr.Body.String()) - partialMock.AssertNumberOfCalls(t, "fetchDetails", 1) } func TestHandleSonarQubeWebhookForBranch(t *testing.T) { @@ -102,10 +97,9 @@ func TestHandleSonarQubeWebhookForBranch(t *testing.T) { }, } - req, rr, handler, partialMock := withValidSonarQubeRequestData(t, defaultMockPreparation, []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": "BRANCH", "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, rr, handler := withValidSonarQubeRequestData(t, []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": "BRANCH", "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": {} }`)) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Equal(t, `{"message": "Processing data. See bot logs for details."}`, rr.Body.String()) - partialMock.AssertNumberOfCalls(t, "fetchDetails", 0) + assert.Equal(t, `{"message": "Ignore Hook for non-PR analysis."}`, rr.Body.String()) } diff --git a/sonar-project.properties b/sonar-project.properties index d12c673..a1d7469 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -11,5 +11,8 @@ sonar.exclusions=**/*_test.go,contrib/**,docker/**,docs/**,helm/** sonar.tests=. sonar.test.inclusions=**/*_test.go +# Entrypoint of the application and not properly testable +sonar.coverage.exclusions=cmd/gitea-sonarqube-bot/main.go + sonar.go.tests.reportPaths=test-report.out sonar.go.coverage.reportPaths=cover.out