From 10d2d3def867fd0cf64ef332e83998bcacd698da Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sat, 21 May 2022 18:10:20 +0200 Subject: [PATCH] Use object structure for ApiServer The API entrypoint was not fully testable due to production API client instantiation. This instantiation is now done within the command entrypoint and this file is excluded from coverage analysis in SonarQube. Signed-off-by: Steven Kriegler --- cmd/gitea-sonarqube-bot/main.go | 16 +++++- internal/api/main.go | 90 +++++++++++++-------------------- internal/api/main_test.go | 45 +++++++---------- sonar-project.properties | 3 ++ 4 files changed, 72 insertions(+), 82 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/main.go b/internal/api/main.go index f71d4ef..7ae869f 100644 --- a/internal/api/main.go +++ b/internal/api/main.go @@ -1,28 +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" -) - -var ( - sonarQubeWebhookHandler SonarQubeWebhookHandlerInferface - giteaWebhookHandler GiteaWebhookHandlerInferface ) type validSonarQubeEndpointHeader struct { SonarQubeProject string `header:"X-SonarQube-Project" binding:"required"` } -func addSonarQubeEndpoint(r *gin.Engine) { - 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 { @@ -30,16 +40,8 @@ func addSonarQubeEndpoint(r *gin.Engine) { return } - sonarQubeWebhookHandler.Handle(c.Writer, c.Request) - }) -} - -type validGiteaEndpointHeader struct { - GiteaEvent string `header:"X-Gitea-Event" binding:"required"` -} - -func addGiteaEndpoint(r *gin.Engine) { - 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 { @@ -49,9 +51,9 @@ func addGiteaEndpoint(r *gin.Engine) { switch h.GiteaEvent { case "pull_request": - giteaWebhookHandler.HandleSynchronize(c.Writer, c.Request) + s.giteaWebhookHandler.HandleSynchronize(c.Writer, c.Request) case "issue_comment": - giteaWebhookHandler.HandleComment(c.Writer, c.Request) + s.giteaWebhookHandler.HandleComment(c.Writer, c.Request) default: c.JSON(http.StatusOK, gin.H{ "message": "ignore unknown event", @@ -60,36 +62,14 @@ func addGiteaEndpoint(r *gin.Engine) { }) } -func setupRouter() *gin.Engine { - r := gin.New() +func New(giteaHandler GiteaWebhookHandlerInferface, sonarQubeHandler SonarQubeWebhookHandlerInferface) *ApiServer { + s := &ApiServer{ + Engine: gin.New(), + giteaWebhookHandler: giteaHandler, + sonarQubeWebhookHandler: sonarQubeHandler, + } - r.Use(gin.Recovery()) - r.Use(gin.LoggerWithConfig(gin.LoggerConfig{ - SkipPaths: []string{"/ping", "/favicon.ico"}, - })) + s.setup() - r.GET("/ping", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "message": "pong", - }) - }) - r.GET("/favicon.ico", func(c *gin.Context) { - c.Status(http.StatusNoContent) - }) - - addSonarQubeEndpoint(r) - addGiteaEndpoint(r) - - return r -} - -func Serve(c *cli.Context) error { - fmt.Println("Hi! I'm the Gitea-SonarQube-PR bot. At your service.") - - sonarQubeWebhookHandler = NewSonarQubeWebhookHandler(giteaSdk.New(), sqSdk.New()) - giteaWebhookHandler = NewGiteaWebhookHandler(giteaSdk.New(), sqSdk.New()) - - r := setupRouter() - - return endless.ListenAndServe(":3000", r) + return s } diff --git a/internal/api/main_test.go b/internal/api/main_test.go index f4fc87f..046972a 100644 --- a/internal/api/main_test.go +++ b/internal/api/main_test.go @@ -82,76 +82,71 @@ func (h *SQSdkMock) ComposeGiteaComment(data *sqSdk.CommentComposeData) (string, // SETUP: mute logs func TestMain(m *testing.M) { - sonarQubeWebhookHandler = nil - giteaWebhookHandler = nil - gin.SetMode(gin.TestMode) log.SetOutput(ioutil.Discard) os.Exit(m.Run()) } func TestNonAPIRoutes(t *testing.T) { - router := setupRouter() + router := New(new(GiteaHandlerMock), new(SonarQubeHandlerMock)) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/favicon.ico", nil) - router.ServeHTTP(w, req) + router.Engine.ServeHTTP(w, req) assert.Equal(t, http.StatusNoContent, w.Code) w = httptest.NewRecorder() req, _ = http.NewRequest("GET", "/ping", nil) - router.ServeHTTP(w, req) + router.Engine.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestSonarQubeAPIRouteMissingProjectHeader(t *testing.T) { - router := setupRouter() + router := New(new(GiteaHandlerMock), new(SonarQubeHandlerMock)) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/hooks/sonarqube", bytes.NewBuffer([]byte(`{}`))) - router.ServeHTTP(w, req) + router.Engine.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code) } func TestSonarQubeAPIRouteProcessing(t *testing.T) { - router := setupRouter() - sonarQubeHandlerMock := new(SonarQubeHandlerMock) sonarQubeHandlerMock.On("Handle", mock.Anything, mock.Anything).Return(nil) - sonarQubeWebhookHandler = sonarQubeHandlerMock + + 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.ServeHTTP(w, req) + router.Engine.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) sonarQubeHandlerMock.AssertNumberOfCalls(t, "Handle", 1) } func TestGiteaAPIRouteMissingEventHeader(t *testing.T) { - router := setupRouter() + router := New(new(GiteaHandlerMock), new(SonarQubeHandlerMock)) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/hooks/gitea", bytes.NewBuffer([]byte(`{}`))) - router.ServeHTTP(w, req) + router.Engine.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code) } func TestGiteaAPIRouteSynchronizeProcessing(t *testing.T) { - router := setupRouter() - giteaHandlerMock := new(GiteaHandlerMock) giteaHandlerMock.On("HandleSynchronize", mock.Anything, mock.Anything).Return(nil) giteaHandlerMock.On("HandleComment", mock.Anything, mock.Anything).Return(nil) - giteaWebhookHandler = giteaHandlerMock + + 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.ServeHTTP(w, req) + router.Engine.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) giteaHandlerMock.AssertNumberOfCalls(t, "HandleSynchronize", 1) @@ -159,17 +154,16 @@ func TestGiteaAPIRouteSynchronizeProcessing(t *testing.T) { } func TestGiteaAPIRouteCommentProcessing(t *testing.T) { - router := setupRouter() - giteaHandlerMock := new(GiteaHandlerMock) giteaHandlerMock.On("HandleSynchronize", mock.Anything, mock.Anything).Return(nil) giteaHandlerMock.On("HandleComment", mock.Anything, mock.Anything).Return(nil) - giteaWebhookHandler = giteaHandlerMock + + 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.ServeHTTP(w, req) + router.Engine.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) giteaHandlerMock.AssertNumberOfCalls(t, "HandleSynchronize", 0) @@ -177,17 +171,16 @@ func TestGiteaAPIRouteCommentProcessing(t *testing.T) { } func TestGiteaAPIRouteUnknownEvent(t *testing.T) { - router := setupRouter() - giteaHandlerMock := new(GiteaHandlerMock) giteaHandlerMock.On("HandleSynchronize", mock.Anything, mock.Anything).Return(nil) giteaHandlerMock.On("HandleComment", mock.Anything, mock.Anything).Return(nil) - giteaWebhookHandler = giteaHandlerMock + + 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.ServeHTTP(w, req) + router.Engine.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) giteaHandlerMock.AssertNumberOfCalls(t, "HandleSynchronize", 0) diff --git a/sonar-project.properties b/sonar-project.properties index d12c673..dd8a6e0 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.go + sonar.go.tests.reportPaths=test-report.out sonar.go.coverage.reportPaths=cover.out