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 <sk.bunsenbrenner@gmail.com> Co-authored-by: justusbunsi <sk.bunsenbrenner@gmail.com> Reviewed-on: https://codeberg.org/justusbunsi/gitea-sonarqube-bot/pulls/22
This commit is contained in:
parent
7f5c3390c4
commit
e203034228
|
@ -1,13 +1,17 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
"gitea-sonarqube-pr-bot/internal/api"
|
"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"
|
"gitea-sonarqube-pr-bot/internal/settings"
|
||||||
|
|
||||||
|
"github.com/fvbock/endless"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -27,7 +31,7 @@ func main() {
|
||||||
Name: "gitea-sonarqube-pr-bot",
|
Name: "gitea-sonarqube-pr-bot",
|
||||||
Usage: "Improve your experience with SonarQube and Gitea",
|
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.`,
|
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)
|
err := app.Run(os.Args)
|
||||||
|
@ -35,3 +39,13 @@ func main() {
|
||||||
log.Fatal(err)
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,11 @@ import (
|
||||||
webhook "gitea-sonarqube-pr-bot/internal/webhooks/gitea"
|
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 {
|
type GiteaWebhookHandler struct {
|
||||||
giteaSdk giteaSdk.GiteaSdkInterface
|
giteaSdk giteaSdk.GiteaSdkInterface
|
||||||
sqSdk sqSdk.SonarQubeSdkInterface
|
sqSdk sqSdk.SonarQubeSdkInterface
|
||||||
|
@ -88,7 +93,7 @@ func (h *GiteaWebhookHandler) HandleComment(rw http.ResponseWriter, r *http.Requ
|
||||||
w.ProcessData(h.giteaSdk, h.sqSdk)
|
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{
|
return &GiteaWebhookHandler{
|
||||||
giteaSdk: g,
|
giteaSdk: g,
|
||||||
sqSdk: sq,
|
sqSdk: sq,
|
||||||
|
|
|
@ -1,32 +1,38 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"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/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 {
|
type validSonarQubeEndpointHeader struct {
|
||||||
SonarQubeProject string `header:"X-SonarQube-Project" binding:"required"`
|
SonarQubeProject string `header:"X-SonarQube-Project" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func addSonarQubeEndpoint(r *gin.Engine) {
|
type validGiteaEndpointHeader struct {
|
||||||
webhookHandler := NewSonarQubeWebhookHandler(giteaSdk.New(), sqSdk.New())
|
GiteaEvent string `header:"X-Gitea-Event" binding:"required"`
|
||||||
r.POST("/hooks/sonarqube", func(c *gin.Context) {
|
}
|
||||||
|
|
||||||
|
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{}
|
h := validSonarQubeEndpointHeader{}
|
||||||
|
|
||||||
if err := c.ShouldBindHeader(&h); err != nil {
|
if err := c.ShouldBindHeader(&h); err != nil {
|
||||||
|
@ -34,17 +40,8 @@ func addSonarQubeEndpoint(r *gin.Engine) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
webhookHandler.Handle(c.Writer, c.Request)
|
s.sonarQubeWebhookHandler.Handle(c.Writer, c.Request)
|
||||||
})
|
}).POST("/hooks/gitea", func(c *gin.Context) {
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
h := validGiteaEndpointHeader{}
|
h := validGiteaEndpointHeader{}
|
||||||
|
|
||||||
if err := c.ShouldBindHeader(&h); err != nil {
|
if err := c.ShouldBindHeader(&h); err != nil {
|
||||||
|
@ -54,9 +51,9 @@ func addGiteaEndpoint(r *gin.Engine) {
|
||||||
|
|
||||||
switch h.GiteaEvent {
|
switch h.GiteaEvent {
|
||||||
case "pull_request":
|
case "pull_request":
|
||||||
webhookHandler.HandleSynchronize(c.Writer, c.Request)
|
s.giteaWebhookHandler.HandleSynchronize(c.Writer, c.Request)
|
||||||
case "issue_comment":
|
case "issue_comment":
|
||||||
webhookHandler.HandleComment(c.Writer, c.Request)
|
s.giteaWebhookHandler.HandleComment(c.Writer, c.Request)
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"message": "ignore unknown event",
|
"message": "ignore unknown event",
|
||||||
|
@ -65,23 +62,14 @@ func addGiteaEndpoint(r *gin.Engine) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func Serve(c *cli.Context) error {
|
func New(giteaHandler GiteaWebhookHandlerInferface, sonarQubeHandler SonarQubeWebhookHandlerInferface) *ApiServer {
|
||||||
fmt.Println("Hi! I'm the Gitea-SonarQube-PR bot. At your service.")
|
s := &ApiServer{
|
||||||
|
Engine: gin.New(),
|
||||||
|
giteaWebhookHandler: giteaHandler,
|
||||||
|
sonarQubeWebhookHandler: sonarQubeHandler,
|
||||||
|
}
|
||||||
|
|
||||||
r := gin.New()
|
s.setup()
|
||||||
|
|
||||||
r.Use(gin.Recovery())
|
return s
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,41 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea"
|
giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea"
|
||||||
sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube"
|
sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube"
|
||||||
"gitea-sonarqube-pr-bot/internal/settings"
|
"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"
|
"github.com/stretchr/testify/mock"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Default SDK mocking
|
type SonarQubeHandlerMock struct {
|
||||||
type HandlerPartialMock struct {
|
|
||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HandlerPartialMock) fetchDetails(w *webhook.Webhook) {
|
func (h *SonarQubeHandlerMock) Handle(rw http.ResponseWriter, r *http.Request) {
|
||||||
h.Called(w)
|
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 {
|
type GiteaSdkMock struct {
|
||||||
|
@ -65,12 +80,109 @@ func (h *SQSdkMock) ComposeGiteaComment(data *sqSdk.CommentComposeData) (string,
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultMockPreparation(h *HandlerPartialMock) {
|
|
||||||
h.On("fetchDetails", mock.Anything).Return(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SETUP: mute logs
|
// SETUP: mute logs
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
log.SetOutput(ioutil.Discard)
|
log.SetOutput(ioutil.Discard)
|
||||||
os.Exit(m.Run())
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -14,10 +14,13 @@ import (
|
||||||
webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube"
|
webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type SonarQubeWebhookHandlerInferface interface {
|
||||||
|
Handle(rw http.ResponseWriter, r *http.Request)
|
||||||
|
}
|
||||||
|
|
||||||
type SonarQubeWebhookHandler struct {
|
type SonarQubeWebhookHandler struct {
|
||||||
fetchDetails func(w *webhook.Webhook)
|
giteaSdk giteaSdk.GiteaSdkInterface
|
||||||
giteaSdk giteaSdk.GiteaSdkInterface
|
sqSdk sqSdk.SonarQubeSdkInterface
|
||||||
sqSdk sqSdk.SonarQubeSdkInterface
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*SonarQubeWebhookHandler) inProjectsMapping(p []settings.Project, n string) (bool, int) {
|
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) {
|
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
|
status := giteaSdk.StatusOK
|
||||||
if w.QualityGate.Status != "OK" {
|
if w.QualityGate.Status != "OK" {
|
||||||
status = giteaSdk.StatusFailure
|
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
|
// Send response to SonarQube at this point to ensure being within 10 seconds limit of webhook response timeout
|
||||||
rw.WriteHeader(http.StatusOK)
|
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."}`)
|
io.WriteString(rw, `{"message": "Processing data. See bot logs for details."}`)
|
||||||
|
|
||||||
h.processData(w, settings.Projects[pIdx].Gitea)
|
h.processData(w, settings.Projects[pIdx].Gitea)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchDetails(w *webhook.Webhook) {
|
func NewSonarQubeWebhookHandler(g giteaSdk.GiteaSdkInterface, sq sqSdk.SonarQubeSdkInterface) SonarQubeWebhookHandlerInferface {
|
||||||
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 {
|
|
||||||
return &SonarQubeWebhookHandler{
|
return &SonarQubeWebhookHandler{
|
||||||
fetchDetails: fetchDetails,
|
giteaSdk: g,
|
||||||
giteaSdk: g,
|
sqSdk: sq,
|
||||||
sqSdk: sq,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,12 +11,8 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func withValidSonarQubeRequestData(t *testing.T, mockPreparation func(*HandlerPartialMock), jsonBody []byte) (*http.Request, *httptest.ResponseRecorder, http.HandlerFunc, *HandlerPartialMock) {
|
func withValidSonarQubeRequestData(t *testing.T, jsonBody []byte) (*http.Request, *httptest.ResponseRecorder, http.HandlerFunc) {
|
||||||
partialMock := new(HandlerPartialMock)
|
|
||||||
mockPreparation(partialMock)
|
|
||||||
|
|
||||||
webhookHandler := NewSonarQubeWebhookHandler(new(GiteaSdkMock), new(SQSdkMock))
|
webhookHandler := NewSonarQubeWebhookHandler(new(GiteaSdkMock), new(SQSdkMock))
|
||||||
webhookHandler.fetchDetails = partialMock.fetchDetails
|
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", "/hooks/sonarqube", bytes.NewBuffer(jsonBody))
|
req, err := http.NewRequest("POST", "/hooks/sonarqube", bytes.NewBuffer(jsonBody))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -27,7 +23,7 @@ func withValidSonarQubeRequestData(t *testing.T, mockPreparation func(*HandlerPa
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
handler := http.HandlerFunc(webhookHandler.Handle)
|
handler := http.HandlerFunc(webhookHandler.Handle)
|
||||||
|
|
||||||
return req, rr, handler, partialMock
|
return req, rr, handler
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandleSonarQubeWebhookProjectMapped(t *testing.T) {
|
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)
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, rr.Code)
|
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)
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, rr.Code)
|
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)
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
assert.Equal(t, http.StatusUnprocessableEntity, rr.Code)
|
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)
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, rr.Code)
|
assert.Equal(t, http.StatusOK, rr.Code)
|
||||||
assert.Equal(t, `{"message": "Processing data. See bot logs for details."}`, rr.Body.String())
|
assert.Equal(t, `{"message": "Processing data. See bot logs for details."}`, rr.Body.String())
|
||||||
partialMock.AssertNumberOfCalls(t, "fetchDetails", 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandleSonarQubeWebhookForBranch(t *testing.T) {
|
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)
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, rr.Code)
|
assert.Equal(t, http.StatusOK, rr.Code)
|
||||||
assert.Equal(t, `{"message": "Processing data. See bot logs for details."}`, rr.Body.String())
|
assert.Equal(t, `{"message": "Ignore Hook for non-PR analysis."}`, rr.Body.String())
|
||||||
partialMock.AssertNumberOfCalls(t, "fetchDetails", 0)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,5 +11,8 @@ sonar.exclusions=**/*_test.go,contrib/**,docker/**,docs/**,helm/**
|
||||||
sonar.tests=.
|
sonar.tests=.
|
||||||
sonar.test.inclusions=**/*_test.go
|
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.tests.reportPaths=test-report.out
|
||||||
sonar.go.coverage.reportPaths=cover.out
|
sonar.go.coverage.reportPaths=cover.out
|
||||||
|
|
Loading…
Reference in a new issue