diff --git a/.env.example b/.env.example index b8edce7c..83264cb9 100644 --- a/.env.example +++ b/.env.example @@ -18,6 +18,11 @@ #MO_MONITORABLE_GITHUB_TOKEN= #MO_MONITORABLE_GITHUB_COUNTCACHEEXPIRATION=30000 +# Gitlab +#MO_MONITORABLE_GITLAB_URL=https://gitlab.com/ +#MO_MONITORABLE_GITLAB_TIMEOUT=5000 +#MO_MONITORABLE_GITLAB_TOKEN= + # HTTP #MO_MONITORABLE_HTTP_TIMEOUT=2000 #MO_MONITORABLE_HTTP_SSLVERIFY=true diff --git a/faker-configs/faker-build-config.json b/faker-configs/faker-build-config.json index 9b469f44..23ea1e7d 100644 --- a/faker-configs/faker-build-config.json +++ b/faker-configs/faker-build-config.json @@ -30,6 +30,10 @@ { "type": "GITHUB-CHECKS", "params": { "owner": "monitoror", "repository": "monitoror", "ref": "master", "status": "ACTION_REQUIRED" } }, { "type": "GITHUB-CHECKS", "params": { "owner": "monitoror", "repository": "monitoror", "ref": "master", "status": "RUNNING" } }, { "type": "GITHUB-PULLREQUEST", "label": "-", "params": { "owner": "monitoror", "repository": "monitoror", "id": 1337, "mergeRequestTitle": "Feature branch which adds a super new thing", "branch": "fork:feature-branch", "status": "SUCCESS" } }, + { "type": "GITLAB-PIPELINE", "params": { "projectId": 10, "ref": "master" } }, + { "type": "GITLAB-PIPELINE", "params": { "projectId": 10, "ref": "master", "status": "ACTION_REQUIRED" } }, + { "type": "GITLAB-PIPELINE", "params": { "projectId": 10, "ref": "master", "status": "RUNNING" } }, + { "type": "GITLAB-MERGEREQUEST", "label": "-", "params": { "projectId": 10, "id": 1337, "mergeRequestTitle": "Feature branch which adds a super new thing", "branch": "fork:feature-branch", "status": "SUCCESS" } }, { "type": "GROUP", "label": "Merge requests in group", diff --git a/faker-configs/faker-config.json b/faker-configs/faker-config.json index 454af1fc..cac3c309 100644 --- a/faker-configs/faker-config.json +++ b/faker-configs/faker-config.json @@ -23,6 +23,7 @@ ]}, { "type": "PINGDOM-CHECK", "params": { "id": 1000 } }, { "type": "GITHUB-COUNT", "label": "Monitoror open issues", "params": { "query": "is:open is:issue repo:monitoror/monitoror", "valueValues": ["10"] } }, + { "type": "GITLAB-ISSUES", "label": "Monitoror open issues", "params": { "valueValues": ["10"] } }, { "type": "HTTP-STATUS", "params": { "url": "http://monitoror.test" } }, { "type": "HTTP-STATUS", "params": { "url": "http://monitoror.test", "status": "SUCCESS"} }, { "type": "HTTP-FORMATTED", "params": { "format": "JSON", "url": "http://monitoror.test", "key": ".key", "status": "SUCCESS", "valueValues": ["1337", "1337"] } } diff --git a/go.mod b/go.mod index 6418627a..3ac280f8 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,6 @@ require ( github.com/go-playground/universal-translator v0.17.0 // indirect github.com/go-playground/validator v9.31.0+incompatible github.com/google/go-github v17.0.0+incompatible - github.com/google/go-querystring v1.0.0 // indirect github.com/joho/godotenv v1.3.0 github.com/jsdidierlaurent/azure-devops-go-api/azuredevops v0.0.0-20191016103718-deea5b1446b8 github.com/jsdidierlaurent/echo-middleware v1.0.3 @@ -32,9 +31,10 @@ require ( github.com/spf13/cobra v1.0.0 github.com/spf13/viper v1.4.0 github.com/stretchr/testify v1.4.0 + github.com/xanzy/go-gitlab v0.31.0 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 - golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be + golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 // indirect gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/yaml.v2 v2.2.2 diff --git a/go.sum b/go.sum index afdec227..26ebc2a6 100644 --- a/go.sum +++ b/go.sum @@ -80,6 +80,12 @@ github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoA github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-retryablehttp v0.6.4 h1:BbgctKO892xEyOXnGiaAwIoSq1QZ/SS4AhjoAh9DnfY= +github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= @@ -195,6 +201,8 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw= github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/xanzy/go-gitlab v0.31.0 h1:+nHztQuCXGSMluKe5Q9IRaPdz6tO8O0gMkQ0vqGpiBk= +github.com/xanzy/go-gitlab v0.31.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= @@ -210,7 +218,9 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49N golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -221,6 +231,8 @@ golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8ou golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 h1:JIqe8uIcRBHXDQVvZtHwp80ai3Lw3IJAeJEs55Dc1W0= +golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -242,6 +254,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -249,6 +263,8 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190608022120-eacb66d2a7c3/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.3.0 h1:FBSsiFRMz3LBeXIomRnVzrQwSDj4ibvcRexLG0LZGQk= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= diff --git a/monitorables/gitlab/api/delivery/http/handlers.go b/monitorables/gitlab/api/delivery/http/handlers.go new file mode 100644 index 00000000..13f52bf7 --- /dev/null +++ b/monitorables/gitlab/api/delivery/http/handlers.go @@ -0,0 +1,62 @@ +package http + +import ( + "net/http" + + "github.com/monitoror/monitoror/internal/pkg/monitorable/delivery" + "github.com/monitoror/monitoror/monitorables/gitlab/api" + "github.com/monitoror/monitoror/monitorables/gitlab/api/models" + + "github.com/labstack/echo/v4" +) + +type GitlabDelivery struct { + gitlabUsecase api.Usecase +} + +func NewGitlabDelivery(p api.Usecase) *GitlabDelivery { + return &GitlabDelivery{p} +} + +func (gd *GitlabDelivery) GetIssues(c echo.Context) error { + // Bind / check Params + params := &models.IssuesParams{} + _ = delivery.BindAndValidateParams(c, params) + + tile, err := gd.gitlabUsecase.Issues(params) + if err != nil { + return err + } + + return c.JSON(http.StatusOK, tile) +} + +func (gd *GitlabDelivery) GetPipeline(c echo.Context) error { + // Bind / check Params + params := &models.PipelineParams{} + if err := delivery.BindAndValidateParams(c, params); err != nil { + return err + } + + tile, err := gd.gitlabUsecase.Pipeline(params) + if err != nil { + return err + } + + return c.JSON(http.StatusOK, tile) +} + +func (gd *GitlabDelivery) GetMergeRequest(c echo.Context) error { + // Bind / check Params + params := &models.MergeRequestParams{} + if err := delivery.BindAndValidateParams(c, params); err != nil { + return err + } + + tile, err := gd.gitlabUsecase.MergeRequest(params) + if err != nil { + return err + } + + return c.JSON(http.StatusOK, tile) +} diff --git a/monitorables/gitlab/api/delivery/http/handlers_test.go b/monitorables/gitlab/api/delivery/http/handlers_test.go new file mode 100644 index 00000000..aab66b3d --- /dev/null +++ b/monitorables/gitlab/api/delivery/http/handlers_test.go @@ -0,0 +1,203 @@ +package http + +import ( + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" + + coreModels "github.com/monitoror/monitoror/models" + "github.com/monitoror/monitoror/monitorables/gitlab/api" + "github.com/monitoror/monitoror/monitorables/gitlab/api/mocks" + "github.com/monitoror/monitoror/monitorables/gitlab/api/models" + + "github.com/AlekSi/pointer" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" + . "github.com/stretchr/testify/mock" +) + +func initEcho() (ctx echo.Context, res *httptest.ResponseRecorder) { + e := echo.New() + req := httptest.NewRequest(echo.GET, "/test", nil) + res = httptest.NewRecorder() + ctx = e.NewContext(req, res) + + return +} + +func TestDelivery_GetIssues_Success(t *testing.T) { + // Init + ctx, res := initEcho() + + ctx.QueryParams().Set("projectId", "10") + ctx.QueryParams().Set("query", "test") + + tile := coreModels.NewTile(api.GitlabIssuesTileType) + tile.Status = coreModels.SuccessStatus + + mockUsecase := new(mocks.Usecase) + mockUsecase.On("Issues", &models.IssuesParams{ProjectID: pointer.ToInt(10)}).Return(tile, nil) + handler := NewGitlabDelivery(mockUsecase) + + // Expected + json, err := json.Marshal(tile) + assert.NoError(t, err, "unable to marshal tile") + + // Test + if assert.NoError(t, handler.GetIssues(ctx)) { + assert.Equal(t, http.StatusOK, res.Code) + assert.Equal(t, string(json), strings.TrimSpace(res.Body.String())) + mockUsecase.AssertNumberOfCalls(t, "Issues", 1) + mockUsecase.AssertExpectations(t) + } +} + +func TestDelivery_GetIssues_Error(t *testing.T) { + // Init + ctx, res := initEcho() + + ctx.QueryParams().Set("query", "test") + + mockUsecase := new(mocks.Usecase) + mockUsecase.On("Issues", Anything).Return(nil, errors.New("build error")) + handler := NewGitlabDelivery(mockUsecase) + + // Test + err := handler.GetIssues(ctx) + if assert.Error(t, err) { + assert.Equal(t, http.StatusOK, res.Code) + mockUsecase.AssertNumberOfCalls(t, "Issues", 1) + mockUsecase.AssertExpectations(t) + } +} + +func TestDelivery_GetPipeline_Success(t *testing.T) { + // Init + ctx, res := initEcho() + + ctx.QueryParams().Set("projectId", "10") + ctx.QueryParams().Set("ref", "master") + + tile := coreModels.NewTile(api.GitlabPipelineTileType) + tile.Status = coreModels.SuccessStatus + + mockUsecase := new(mocks.Usecase) + mockUsecase.On("Pipeline", &models.PipelineParams{ProjectID: pointer.ToInt(10), Ref: "master"}).Return(tile, nil) + handler := NewGitlabDelivery(mockUsecase) + + // Expected + json, err := json.Marshal(tile) + assert.NoError(t, err, "unable to marshal tile") + + // Test + if assert.NoError(t, handler.GetPipeline(ctx)) { + assert.Equal(t, http.StatusOK, res.Code) + assert.Equal(t, string(json), strings.TrimSpace(res.Body.String())) + mockUsecase.AssertNumberOfCalls(t, "Pipeline", 1) + mockUsecase.AssertExpectations(t) + } +} + +func TestDelivery_GetChecks_MissingParams(t *testing.T) { + // Init + ctx, res := initEcho() + + mockUsecase := new(mocks.Usecase) + handler := NewGitlabDelivery(mockUsecase) + + // Test + err := handler.GetPipeline(ctx) + if assert.Error(t, err) { + assert.Equal(t, http.StatusOK, res.Code) + assert.IsType(t, &coreModels.MonitororError{}, err) + mockUsecase.AssertNumberOfCalls(t, "Pipeline", 0) + mockUsecase.AssertExpectations(t) + } +} + +func TestDelivery_GetChecks_Error(t *testing.T) { + // Init + ctx, res := initEcho() + + ctx.QueryParams().Set("projectId", "10") + ctx.QueryParams().Set("ref", "master") + + mockUsecase := new(mocks.Usecase) + mockUsecase.On("Pipeline", Anything).Return(nil, errors.New("build error")) + handler := NewGitlabDelivery(mockUsecase) + + // Test + err := handler.GetPipeline(ctx) + if assert.Error(t, err) { + assert.Equal(t, http.StatusOK, res.Code) + mockUsecase.AssertNumberOfCalls(t, "Pipeline", 1) + mockUsecase.AssertExpectations(t) + } +} + +func TestDelivery_GetMergeRequest_Success(t *testing.T) { + // Init + ctx, res := initEcho() + + ctx.QueryParams().Set("projectId", "10") + ctx.QueryParams().Set("id", "10") + + tile := coreModels.NewTile(api.GitlabMergeRequestTileType) + tile.Status = coreModels.SuccessStatus + + mockUsecase := new(mocks.Usecase) + mockUsecase.On("MergeRequest", &models.MergeRequestParams{ProjectID: pointer.ToInt(10), ID: pointer.ToInt(10)}).Return(tile, nil) + handler := NewGitlabDelivery(mockUsecase) + + // Expected + json, err := json.Marshal(tile) + assert.NoError(t, err, "unable to marshal tile") + + // Test + if assert.NoError(t, handler.GetMergeRequest(ctx)) { + assert.Equal(t, http.StatusOK, res.Code) + assert.Equal(t, string(json), strings.TrimSpace(res.Body.String())) + mockUsecase.AssertNumberOfCalls(t, "MergeRequest", 1) + mockUsecase.AssertExpectations(t) + } +} + +func TestDelivery_GetMergeRequest_MissingParams(t *testing.T) { + // Init + ctx, res := initEcho() + + mockUsecase := new(mocks.Usecase) + handler := NewGitlabDelivery(mockUsecase) + + // Test + err := handler.GetMergeRequest(ctx) + if assert.Error(t, err) { + assert.Equal(t, http.StatusOK, res.Code) + assert.IsType(t, &coreModels.MonitororError{}, err) + mockUsecase.AssertNumberOfCalls(t, "MergeRequest", 0) + mockUsecase.AssertExpectations(t) + } +} + +func TestDelivery_GetMergeRequest_Error(t *testing.T) { + // Init + ctx, res := initEcho() + + ctx.QueryParams().Set("projectId", "10") + ctx.QueryParams().Set("id", "10") + + mockUsecase := new(mocks.Usecase) + mockUsecase.On("MergeRequest", Anything).Return(nil, errors.New("build error")) + handler := NewGitlabDelivery(mockUsecase) + + // Test + err := handler.GetMergeRequest(ctx) + if assert.Error(t, err) { + assert.Equal(t, http.StatusOK, res.Code) + mockUsecase.AssertNumberOfCalls(t, "MergeRequest", 1) + mockUsecase.AssertExpectations(t) + } +} diff --git a/monitorables/gitlab/api/mocks/Repository.go b/monitorables/gitlab/api/mocks/Repository.go new file mode 100644 index 00000000..a1e7c736 --- /dev/null +++ b/monitorables/gitlab/api/mocks/Repository.go @@ -0,0 +1,149 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import ( + models "github.com/monitoror/monitoror/monitorables/gitlab/api/models" + mock "github.com/stretchr/testify/mock" +) + +// Repository is an autogenerated mock type for the Repository type +type Repository struct { + mock.Mock +} + +// GetIssues provides a mock function with given fields: params +func (_m *Repository) GetIssues(params *models.IssuesParams) (int, error) { + ret := _m.Called(params) + + var r0 int + if rf, ok := ret.Get(0).(func(*models.IssuesParams) int); ok { + r0 = rf(params) + } else { + r0 = ret.Get(0).(int) + } + + var r1 error + if rf, ok := ret.Get(1).(func(*models.IssuesParams) error); ok { + r1 = rf(params) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetMergeRequest provides a mock function with given fields: projectID, mergeRequestID +func (_m *Repository) GetMergeRequest(projectID int, mergeRequestID int) (*models.MergeRequest, error) { + ret := _m.Called(projectID, mergeRequestID) + + var r0 *models.MergeRequest + if rf, ok := ret.Get(0).(func(int, int) *models.MergeRequest); ok { + r0 = rf(projectID, mergeRequestID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.MergeRequest) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(int, int) error); ok { + r1 = rf(projectID, mergeRequestID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetMergeRequests provides a mock function with given fields: projectID +func (_m *Repository) GetMergeRequests(projectID int) ([]models.MergeRequest, error) { + ret := _m.Called(projectID) + + var r0 []models.MergeRequest + if rf, ok := ret.Get(0).(func(int) []models.MergeRequest); ok { + r0 = rf(projectID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]models.MergeRequest) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(int) error); ok { + r1 = rf(projectID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetPipeline provides a mock function with given fields: projectID, pipelineID +func (_m *Repository) GetPipeline(projectID int, pipelineID int) (*models.Pipeline, error) { + ret := _m.Called(projectID, pipelineID) + + var r0 *models.Pipeline + if rf, ok := ret.Get(0).(func(int, int) *models.Pipeline); ok { + r0 = rf(projectID, pipelineID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Pipeline) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(int, int) error); ok { + r1 = rf(projectID, pipelineID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetPipelines provides a mock function with given fields: projectID, ref +func (_m *Repository) GetPipelines(projectID int, ref string) ([]int, error) { + ret := _m.Called(projectID, ref) + + var r0 []int + if rf, ok := ret.Get(0).(func(int, string) []int); ok { + r0 = rf(projectID, ref) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]int) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(int, string) error); ok { + r1 = rf(projectID, ref) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetProject provides a mock function with given fields: projectID +func (_m *Repository) GetProject(projectID int) (*models.Project, error) { + ret := _m.Called(projectID) + + var r0 *models.Project + if rf, ok := ret.Get(0).(func(int) *models.Project); ok { + r0 = rf(projectID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Project) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(int) error); ok { + r1 = rf(projectID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/monitorables/gitlab/api/mocks/Usecase.go b/monitorables/gitlab/api/mocks/Usecase.go new file mode 100644 index 00000000..dee359e6 --- /dev/null +++ b/monitorables/gitlab/api/mocks/Usecase.go @@ -0,0 +1,109 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import ( + configmodels "github.com/monitoror/monitoror/api/config/models" + mock "github.com/stretchr/testify/mock" + + models "github.com/monitoror/monitoror/monitorables/gitlab/api/models" + + monitorormodels "github.com/monitoror/monitoror/models" +) + +// Usecase is an autogenerated mock type for the Usecase type +type Usecase struct { + mock.Mock +} + +// Issues provides a mock function with given fields: params +func (_m *Usecase) Issues(params *models.IssuesParams) (*monitorormodels.Tile, error) { + ret := _m.Called(params) + + var r0 *monitorormodels.Tile + if rf, ok := ret.Get(0).(func(*models.IssuesParams) *monitorormodels.Tile); ok { + r0 = rf(params) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*monitorormodels.Tile) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*models.IssuesParams) error); ok { + r1 = rf(params) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MergeRequest provides a mock function with given fields: params +func (_m *Usecase) MergeRequest(params *models.MergeRequestParams) (*monitorormodels.Tile, error) { + ret := _m.Called(params) + + var r0 *monitorormodels.Tile + if rf, ok := ret.Get(0).(func(*models.MergeRequestParams) *monitorormodels.Tile); ok { + r0 = rf(params) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*monitorormodels.Tile) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*models.MergeRequestParams) error); ok { + r1 = rf(params) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MergeRequestsGenerator provides a mock function with given fields: params +func (_m *Usecase) MergeRequestsGenerator(params interface{}) ([]configmodels.GeneratedTile, error) { + ret := _m.Called(params) + + var r0 []configmodels.GeneratedTile + if rf, ok := ret.Get(0).(func(interface{}) []configmodels.GeneratedTile); ok { + r0 = rf(params) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]configmodels.GeneratedTile) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(interface{}) error); ok { + r1 = rf(params) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Pipeline provides a mock function with given fields: params +func (_m *Usecase) Pipeline(params *models.PipelineParams) (*monitorormodels.Tile, error) { + ret := _m.Called(params) + + var r0 *monitorormodels.Tile + if rf, ok := ret.Get(0).(func(*models.PipelineParams) *monitorormodels.Tile); ok { + r0 = rf(params) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*monitorormodels.Tile) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*models.PipelineParams) error); ok { + r1 = rf(params) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/monitorables/gitlab/api/models/issuesparams.go b/monitorables/gitlab/api/models/issuesparams.go new file mode 100644 index 00000000..df4eab48 --- /dev/null +++ b/monitorables/gitlab/api/models/issuesparams.go @@ -0,0 +1,21 @@ +//+build !faker + +package models + +import "github.com/monitoror/monitoror/internal/pkg/monitorable/params" + +type ( + IssuesParams struct { + params.Default + + ProjectID *int `json:"projectId" query:"projectId"` + + State *string `json:"state" query:"state"` + Labels []string `json:"labels" query:"labels"` + Milestone *string `json:"milestone" query:"milestone"` + Scope *string `json:"scope" query:"scope"` + Search *string `json:"search" query:"search"` + AuthorID *int `json:"authorId" query:"authorId"` + AssigneeID *int `json:"assigneeId" query:"assigneeId"` + } +) diff --git a/monitorables/gitlab/api/models/issuesparams_faker.go b/monitorables/gitlab/api/models/issuesparams_faker.go new file mode 100644 index 00000000..1f84b0b8 --- /dev/null +++ b/monitorables/gitlab/api/models/issuesparams_faker.go @@ -0,0 +1,23 @@ +//+build faker + +package models + +import "github.com/monitoror/monitoror/internal/pkg/monitorable/params" + +type ( + IssuesParams struct { + params.Default + + ProjectID *int `json:"projectId" query:"projectId"` + + State *string `json:"state" query:"state"` + Labels []string `json:"labels" query:"labels"` + Milestone *string `json:"milestone" query:"milestone"` + Scope *string `json:"scope" query:"scope"` + Search *string `json:"search" query:"search"` + AuthorID *int `json:"authorId" query:"authorId"` + AssigneeID *int `json:"assigneeId" query:"assigneeId"` + + ValueValues []string `json:"valueValues" query:"valueValues"` + } +) diff --git a/monitorables/gitlab/api/models/mergerequest.go b/monitorables/gitlab/api/models/mergerequest.go new file mode 100644 index 00000000..3f034d19 --- /dev/null +++ b/monitorables/gitlab/api/models/mergerequest.go @@ -0,0 +1,15 @@ +package models + +import ( + coreModels "github.com/monitoror/monitoror/models" +) + +type MergeRequest struct { + ID int + Title string + Author coreModels.Author + + SourceProjectID int + SourceBranch string + CommitSHA string +} diff --git a/monitorables/gitlab/api/models/mergerequestgeneratorparams.go b/monitorables/gitlab/api/models/mergerequestgeneratorparams.go new file mode 100644 index 00000000..cc5c0088 --- /dev/null +++ b/monitorables/gitlab/api/models/mergerequestgeneratorparams.go @@ -0,0 +1,11 @@ +package models + +import ( + "github.com/monitoror/monitoror/internal/pkg/monitorable/params" +) + +type MergeRequestGeneratorParams struct { + params.Default + + ProjectID *int `json:"projectId" query:"projectId" validate:"required"` +} diff --git a/monitorables/gitlab/api/models/mergerequestgeneratorparams_test.go b/monitorables/gitlab/api/models/mergerequestgeneratorparams_test.go new file mode 100644 index 00000000..632e7d98 --- /dev/null +++ b/monitorables/gitlab/api/models/mergerequestgeneratorparams_test.go @@ -0,0 +1,17 @@ +package models + +import ( + "testing" + + "github.com/monitoror/monitoror/internal/pkg/monitorable/test" + + "github.com/AlekSi/pointer" +) + +func TestMergeRequestGeneratorParams_Validate(t *testing.T) { + param := &MergeRequestGeneratorParams{ProjectID: pointer.ToInt(10)} + test.AssertParams(t, param, 0) + + param = &MergeRequestGeneratorParams{} + test.AssertParams(t, param, 1) +} diff --git a/monitorables/gitlab/api/models/mergerequestparams.go b/monitorables/gitlab/api/models/mergerequestparams.go new file mode 100644 index 00000000..c1be6d6b --- /dev/null +++ b/monitorables/gitlab/api/models/mergerequestparams.go @@ -0,0 +1,23 @@ +//+build !faker + +package models + +import ( + "fmt" + + "github.com/monitoror/monitoror/internal/pkg/monitorable/params" +) + +type ( + MergeRequestParams struct { + params.Default + + ProjectID *int `json:"projectId" query:"projectId" validate:"required"` + ID *int `json:"id" query:"id" validate:"required"` + } +) + +// Used by cache as identifier +func (p *MergeRequestParams) String() string { + return fmt.Sprintf("MERGEREQUEST-%d-%d", *p.ProjectID, *p.ID) +} diff --git a/monitorables/gitlab/api/models/mergerequestparams_faker.go b/monitorables/gitlab/api/models/mergerequestparams_faker.go new file mode 100644 index 00000000..adaee422 --- /dev/null +++ b/monitorables/gitlab/api/models/mergerequestparams_faker.go @@ -0,0 +1,39 @@ +//+build faker + +package models + +import ( + "fmt" + "time" + + "github.com/monitoror/monitoror/internal/pkg/monitorable/params" + coreModels "github.com/monitoror/monitoror/models" +) + +type ( + MergeRequestParams struct { + params.Default + + ProjectID *int `json:"projectId" query:"projectId" validate:"required"` + ID *int `json:"id" query:"id" validate:"required"` + + Branch string `json:"branch" query:"branch"` + + AuthorName string `json:"authorName" query:"authorName"` + AuthorAvatarURL string `json:"authorAvatarURL" query:"authorAvatarURL"` + + MergeRequestTitle string `json:"mergeRequestTitle" query:"mergeRequestTitle"` + + Status coreModels.TileStatus `json:"status" query:"status"` + PreviousStatus coreModels.TileStatus `json:"previousStatus" query:"previousStatus"` + StartedAt time.Time `json:"startedAt" query:"startedAt"` + FinishedAt time.Time `json:"finishedAt" query:"finishedAt"` + Duration int64 `json:"duration" query:"duration"` + EstimatedDuration int64 `json:"estimatedDuration" query:"estimatedDuration"` + } +) + +// Used by cache as identifier +func (p *MergeRequestParams) String() string { + return fmt.Sprintf("MERGEREQUEST-%d-%d", *p.ProjectID, *p.ID) +} diff --git a/monitorables/gitlab/api/models/mergerequestparams_test.go b/monitorables/gitlab/api/models/mergerequestparams_test.go new file mode 100644 index 00000000..47cb06bd --- /dev/null +++ b/monitorables/gitlab/api/models/mergerequestparams_test.go @@ -0,0 +1,30 @@ +package models + +import ( + "fmt" + "testing" + + "github.com/monitoror/monitoror/internal/pkg/monitorable/test" + + "github.com/AlekSi/pointer" + "github.com/stretchr/testify/assert" +) + +func TestMergeRequestParams_Validate(t *testing.T) { + param := &MergeRequestParams{ProjectID: pointer.ToInt(10), ID: pointer.ToInt(10)} + test.AssertParams(t, param, 0) + + param = &MergeRequestParams{ID: pointer.ToInt(10)} + test.AssertParams(t, param, 1) + + param = &MergeRequestParams{ProjectID: pointer.ToInt(10)} + test.AssertParams(t, param, 1) + + param = &MergeRequestParams{} + test.AssertParams(t, param, 2) +} + +func TestMergeRequestParams_String(t *testing.T) { + param := &MergeRequestParams{ProjectID: pointer.ToInt(10), ID: pointer.ToInt(10)} + assert.Equal(t, "MERGEREQUEST-10-10", fmt.Sprint(param)) +} diff --git a/monitorables/gitlab/api/models/pipeline.go b/monitorables/gitlab/api/models/pipeline.go new file mode 100644 index 00000000..cb9df876 --- /dev/null +++ b/monitorables/gitlab/api/models/pipeline.go @@ -0,0 +1,16 @@ +package models + +import ( + "time" + + coreModels "github.com/monitoror/monitoror/models" +) + +type Pipeline struct { + ID int + Branch string + Author coreModels.Author + Status string + StartedAt *time.Time + FinishedAt *time.Time +} diff --git a/monitorables/gitlab/api/models/pipelineparams.go b/monitorables/gitlab/api/models/pipelineparams.go new file mode 100644 index 00000000..314f91e2 --- /dev/null +++ b/monitorables/gitlab/api/models/pipelineparams.go @@ -0,0 +1,23 @@ +//+build !faker + +package models + +import ( + "fmt" + + "github.com/monitoror/monitoror/internal/pkg/monitorable/params" +) + +type ( + PipelineParams struct { + params.Default + + ProjectID *int `json:"projectId" query:"projectId" validate:"required"` + Ref string `json:"ref" query:"ref" validate:"required"` + } +) + +// Used by cache as identifier +func (p *PipelineParams) String() string { + return fmt.Sprintf("PIPELINE-%d-%s", *p.ProjectID, p.Ref) +} diff --git a/monitorables/gitlab/api/models/pipelineparams_faker.go b/monitorables/gitlab/api/models/pipelineparams_faker.go new file mode 100644 index 00000000..f85cf20d --- /dev/null +++ b/monitorables/gitlab/api/models/pipelineparams_faker.go @@ -0,0 +1,35 @@ +//+build faker + +package models + +import ( + "fmt" + "time" + + "github.com/monitoror/monitoror/internal/pkg/monitorable/params" + coreModels "github.com/monitoror/monitoror/models" +) + +type ( + PipelineParams struct { + params.Default + + ProjectID *int `json:"projectId" query:"projectId" validate:"required"` + Ref string `json:"ref" query:"ref" validate:"required"` + + AuthorName string `json:"authorName" query:"authorName"` + AuthorAvatarURL string `json:"authorAvatarURL" query:"authorAvatarURL"` + + Status coreModels.TileStatus `json:"status" query:"status"` + PreviousStatus coreModels.TileStatus `json:"previousStatus" query:"previousStatus"` + StartedAt time.Time `json:"startedAt" query:"startedAt"` + FinishedAt time.Time `json:"finishedAt" query:"finishedAt"` + Duration int64 `json:"duration" query:"duration"` + EstimatedDuration int64 `json:"estimatedDuration" query:"estimatedDuration"` + } +) + +// Used by cache as identifier +func (p *PipelineParams) String() string { + return fmt.Sprintf("PIPELINE-%d-%s", *p.ProjectID, p.Ref) +} diff --git a/monitorables/gitlab/api/models/pipelineparams_test.go b/monitorables/gitlab/api/models/pipelineparams_test.go new file mode 100644 index 00000000..d44abbc7 --- /dev/null +++ b/monitorables/gitlab/api/models/pipelineparams_test.go @@ -0,0 +1,30 @@ +package models + +import ( + "fmt" + "testing" + + "github.com/monitoror/monitoror/internal/pkg/monitorable/test" + + "github.com/AlekSi/pointer" + "github.com/stretchr/testify/assert" +) + +func TestPipeline_Validate(t *testing.T) { + param := &PipelineParams{ProjectID: pointer.ToInt(10), Ref: "master"} + test.AssertParams(t, param, 0) + + param = &PipelineParams{ProjectID: pointer.ToInt(10)} + test.AssertParams(t, param, 1) + + param = &PipelineParams{Ref: "master"} + test.AssertParams(t, param, 1) + + param = &PipelineParams{} + test.AssertParams(t, param, 2) +} + +func TestPipelineParams_String(t *testing.T) { + param := &PipelineParams{ProjectID: pointer.ToInt(10), Ref: "master"} + assert.Equal(t, "PIPELINE-10-master", fmt.Sprint(param)) +} diff --git a/monitorables/gitlab/api/models/project.go b/monitorables/gitlab/api/models/project.go new file mode 100644 index 00000000..cedf2519 --- /dev/null +++ b/monitorables/gitlab/api/models/project.go @@ -0,0 +1,8 @@ +package models + +type Project struct { + ID int + + Owner string + Repository string +} diff --git a/monitorables/gitlab/api/repository.go b/monitorables/gitlab/api/repository.go new file mode 100644 index 00000000..5792d265 --- /dev/null +++ b/monitorables/gitlab/api/repository.go @@ -0,0 +1,16 @@ +//go:generate mockery -name Repository + +package api + +import "github.com/monitoror/monitoror/monitorables/gitlab/api/models" + +type ( + Repository interface { + GetIssues(params *models.IssuesParams) (int, error) + GetPipeline(projectID, pipelineID int) (*models.Pipeline, error) + GetPipelines(projectID int, ref string) ([]int, error) + GetMergeRequest(projectID, mergeRequestID int) (*models.MergeRequest, error) + GetMergeRequests(projectID int) ([]models.MergeRequest, error) + GetProject(projectID int) (*models.Project, error) + } +) diff --git a/monitorables/gitlab/api/repository/api.go b/monitorables/gitlab/api/repository/api.go new file mode 100644 index 00000000..48984066 --- /dev/null +++ b/monitorables/gitlab/api/repository/api.go @@ -0,0 +1,198 @@ +package repository + +import ( + "fmt" + "net/http" + "time" + + "github.com/monitoror/monitoror/monitorables/gitlab/api" + "github.com/monitoror/monitoror/monitorables/gitlab/api/models" + "github.com/monitoror/monitoror/monitorables/gitlab/config" + "github.com/monitoror/monitoror/pkg/gogitlab" + + "github.com/AlekSi/pointer" + "github.com/xanzy/go-gitlab" +) + +type ( + gitlabRepository struct { + config *config.Gitlab + + issuesService gogitlab.IssuesService + pipelinesService gogitlab.PipelinesService + mergeRequestsService gogitlab.MergeRequestsService + projectService gogitlab.ProjectService + } +) + +func NewGitlabRepository(config *config.Gitlab) api.Repository { + httpClient := &http.Client{ + Timeout: time.Duration(config.Timeout) * time.Millisecond, + } + gitlabAPIBaseURL := fmt.Sprintf("%s/api/v4", config.URL) + + git, err := gitlab.NewClient(config.Token, gitlab.WithBaseURL(gitlabAPIBaseURL), gitlab.WithHTTPClient(httpClient)) + if err != nil { + // only when gitlabAPIBaseURL is not a valid URL + panic(fmt.Sprintf("unable to setup Gitlab client\n. %v\n", err)) + } + + return &gitlabRepository{ + config: config, + + issuesService: git.Issues, + pipelinesService: git.Pipelines, + mergeRequestsService: git.MergeRequests, + projectService: git.Projects, + } +} + +func (gr *gitlabRepository) GetIssues(params *models.IssuesParams) (int, error) { + + var resp *gitlab.Response + var err error + if params.ProjectID != nil { + listProjectIssueOption := &gitlab.ListProjectIssuesOptions{ + State: params.State, + Labels: params.Labels, + Milestone: params.Milestone, + Scope: params.Scope, + Search: params.Search, + AuthorID: params.AuthorID, + AssigneeID: params.AssigneeID, + } + + _, resp, err = gr.issuesService.ListProjectIssues(*params.ProjectID, listProjectIssueOption) + if err != nil { + return 0, err + } + } else { + listIssueOption := &gitlab.ListIssuesOptions{ + State: params.State, + Labels: params.Labels, + Milestone: params.Milestone, + Scope: params.Scope, + Search: params.Search, + AuthorID: params.AuthorID, + AssigneeID: params.AssigneeID, + } + + _, resp, err = gr.issuesService.ListIssues(listIssueOption) + if err != nil { + return 0, err + } + } + return resp.TotalItems, nil +} + +func (gr *gitlabRepository) GetPipeline(projectID, pipelineID int) (*models.Pipeline, error) { + gitlabPipeline, _, err := gr.pipelinesService.GetPipeline(projectID, pipelineID) + if err != nil { + return nil, err + } + + pipeline := &models.Pipeline{ + ID: gitlabPipeline.ID, + Branch: gitlabPipeline.Ref, + Status: gitlabPipeline.Status, + StartedAt: gitlabPipeline.CreatedAt, + FinishedAt: gitlabPipeline.FinishedAt, + } + + if gitlabPipeline.User != nil { + pipeline.Author.Name = gitlabPipeline.User.Name + pipeline.Author.AvatarURL = gitlabPipeline.User.AvatarURL + + if pipeline.Author.Name == "" { + pipeline.Author.Name = gitlabPipeline.User.Username + } + } + + return pipeline, nil +} + +func (gr *gitlabRepository) GetPipelines(projectID int, ref string) ([]int, error) { + var ids []int + + gitlabPipelines, _, err := gr.pipelinesService.ListProjectPipelines(projectID, &gitlab.ListProjectPipelinesOptions{ + Ref: &ref, + OrderBy: pointer.ToString("id"), + Sort: pointer.ToString("desc"), + }) + if err != nil { + return nil, err + } + + for _, pipeline := range gitlabPipelines { + ids = append(ids, pipeline.ID) + } + + return ids, nil +} + +func (gr *gitlabRepository) GetMergeRequest(projectID, mergeRequestID int) (*models.MergeRequest, error) { + gitlabMergeRequest, _, err := gr.mergeRequestsService.GetMergeRequest(projectID, mergeRequestID, &gitlab.GetMergeRequestsOptions{}) + if err != nil { + return nil, err + } + + return parseMergeRequest(gitlabMergeRequest), nil +} + +func (gr *gitlabRepository) GetMergeRequests(projectID int) ([]models.MergeRequest, error) { + var mergeRequests []models.MergeRequest + + gitlabMergeRequests, _, err := gr.mergeRequestsService.ListProjectMergeRequests(projectID, &gitlab.ListProjectMergeRequestsOptions{ + // If needed by users, use pagination. + ListOptions: gitlab.ListOptions{ + Page: 1, + PerPage: 100, // Maximum par_page allowed. + }, + State: pointer.ToString("opened"), + }) + if err != nil { + return nil, err + } + + for _, gitlabMergeRequest := range gitlabMergeRequests { + mergeRequests = append(mergeRequests, *parseMergeRequest(gitlabMergeRequest)) + } + + return mergeRequests, nil +} + +func (gr *gitlabRepository) GetProject(projectID int) (*models.Project, error) { + gitlabProject, _, err := gr.projectService.GetProject(projectID, &gitlab.GetProjectOptions{}) + if err != nil { + return nil, err + } + + project := &models.Project{ + ID: gitlabProject.ID, + Owner: gitlabProject.Namespace.Path, + Repository: gitlabProject.Path, + } + + return project, nil +} + +func parseMergeRequest(gitlabMergeRequest *gitlab.MergeRequest) *models.MergeRequest { + mergeRequest := &models.MergeRequest{ + ID: gitlabMergeRequest.IID, + Title: gitlabMergeRequest.Title, + SourceProjectID: gitlabMergeRequest.SourceProjectID, + SourceBranch: gitlabMergeRequest.SourceBranch, + CommitSHA: gitlabMergeRequest.SHA, + } + + if gitlabMergeRequest.Author != nil { + mergeRequest.Author.Name = gitlabMergeRequest.Author.Name + mergeRequest.Author.AvatarURL = gitlabMergeRequest.Author.AvatarURL + + if mergeRequest.Author.Name == "" { + mergeRequest.Author.Name = gitlabMergeRequest.Author.Username + } + } + + return mergeRequest +} diff --git a/monitorables/gitlab/api/repository/api_test.go b/monitorables/gitlab/api/repository/api_test.go new file mode 100644 index 00000000..5174ac7a --- /dev/null +++ b/monitorables/gitlab/api/repository/api_test.go @@ -0,0 +1,382 @@ +package repository + +import ( + "errors" + "testing" + "time" + + coreModels "github.com/monitoror/monitoror/models" + "github.com/monitoror/monitoror/monitorables/gitlab/api/models" + "github.com/monitoror/monitoror/monitorables/gitlab/config" + "github.com/monitoror/monitoror/pkg/gogitlab/mocks" + + "github.com/AlekSi/pointer" + "github.com/stretchr/testify/assert" + . "github.com/stretchr/testify/mock" + "github.com/xanzy/go-gitlab" +) + +func initRepository(t *testing.T) *gitlabRepository { + conf := &config.Gitlab{ + URL: "https://gitlab.example.com", + Token: "xxx", + Timeout: 1000, + } + + repository := NewGitlabRepository(conf) + + apiGithubRepository, ok := repository.(*gitlabRepository) + if assert.True(t, ok) { + return apiGithubRepository + } + return nil +} + +func TestNewGitlabRepository_Panic(t *testing.T) { + conf := &config.Gitlab{ + URL: "test%test", + } + + assert.Panics(t, func() { + NewGitlabRepository(conf) + }) +} + +func TestRepository_GetIssues_Error(t *testing.T) { + gitlabErr := errors.New("gitlab error") + + mockIssueService := new(mocks.IssuesService) + mockIssueService.On("ListIssues", Anything, Anything). + Return(nil, nil, gitlabErr) + mockIssueService.On("ListProjectIssues", Anything, Anything, Anything). + Return(nil, nil, gitlabErr) + + repository := initRepository(t) + if repository != nil { + repository.issuesService = mockIssueService + + _, err := repository.GetIssues(&models.IssuesParams{}) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "gitlab error") + } + _, err = repository.GetIssues(&models.IssuesParams{ProjectID: pointer.ToInt(10)}) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "gitlab error") + } + + mockIssueService.AssertNumberOfCalls(t, "ListIssues", 1) + mockIssueService.AssertNumberOfCalls(t, "ListProjectIssues", 1) + mockIssueService.AssertExpectations(t) + } +} + +func TestRepository_GetIssues_Success(t *testing.T) { + mockIssueService := new(mocks.IssuesService) + mockIssueService.On("ListIssues", Anything, Anything). + Return(nil, &gitlab.Response{TotalItems: 42}, nil) + mockIssueService.On("ListProjectIssues", Anything, Anything, Anything). + Return(nil, &gitlab.Response{TotalItems: 42}, nil) + + repository := initRepository(t) + if repository != nil { + repository.issuesService = mockIssueService + + value, err := repository.GetIssues(&models.IssuesParams{}) + if assert.NoError(t, err) { + assert.Equal(t, 42, value) + } + + value, err = repository.GetIssues(&models.IssuesParams{ProjectID: pointer.ToInt(10)}) + if assert.NoError(t, err) { + assert.Equal(t, 42, value) + } + + mockIssueService.AssertNumberOfCalls(t, "ListIssues", 1) + mockIssueService.AssertNumberOfCalls(t, "ListProjectIssues", 1) + mockIssueService.AssertExpectations(t) + } +} + +func TestRepository_GetPipeline_Error(t *testing.T) { + gitlabErr := errors.New("gitlab error") + + mockPipelineService := new(mocks.PipelinesService) + mockPipelineService.On("GetPipeline", Anything, AnythingOfType("int"), Anything). + Return(nil, nil, gitlabErr) + + repository := initRepository(t) + if repository != nil { + repository.pipelinesService = mockPipelineService + + _, err := repository.GetPipeline(10, 10) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "gitlab error") + mockPipelineService.AssertNumberOfCalls(t, "GetPipeline", 1) + mockPipelineService.AssertExpectations(t) + } + } +} + +func TestRepository_GetPipeline_Success(t *testing.T) { + now := time.Now() + + gitlabPipeline := &gitlab.Pipeline{ + ID: 10, + Status: "failed", + Ref: "master", + SHA: "12345", + User: &gitlab.BasicUser{ + Username: "test", + AvatarURL: "test.example.com", + }, + CreatedAt: pointer.ToTime(now), + FinishedAt: pointer.ToTime(now.Add(time.Second * 30)), + } + + mockPipelineService := new(mocks.PipelinesService) + mockPipelineService.On("GetPipeline", Anything, AnythingOfType("int"), Anything). + Return(gitlabPipeline, nil, nil) + + pipeline := &models.Pipeline{ + ID: 10, + Branch: "master", + Author: coreModels.Author{ + Name: "test", + AvatarURL: "test.example.com", + }, + Status: "failed", + StartedAt: pointer.ToTime(now), + FinishedAt: pointer.ToTime(now.Add(time.Second * 30)), + } + + repository := initRepository(t) + if repository != nil { + repository.pipelinesService = mockPipelineService + + result, err := repository.GetPipeline(10, 10) + if assert.NoError(t, err) { + assert.Equal(t, pipeline, result) + mockPipelineService.AssertNumberOfCalls(t, "GetPipeline", 1) + mockPipelineService.AssertExpectations(t) + } + } +} + +func TestRepository_GetPipelines_Error(t *testing.T) { + gitlabErr := errors.New("gitlab error") + + mockPipelineService := new(mocks.PipelinesService) + mockPipelineService.On("ListProjectPipelines", Anything, Anything, Anything). + Return(nil, nil, gitlabErr) + + repository := initRepository(t) + if repository != nil { + repository.pipelinesService = mockPipelineService + + _, err := repository.GetPipelines(10, "master") + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "gitlab error") + mockPipelineService.AssertNumberOfCalls(t, "ListProjectPipelines", 1) + mockPipelineService.AssertExpectations(t) + } + } +} + +func TestRepository_GetPipelines_Success(t *testing.T) { + mockPipelineService := new(mocks.PipelinesService) + mockPipelineService.On("ListProjectPipelines", Anything, Anything, Anything). + Return([]*gitlab.PipelineInfo{ + {ID: 10}, + {ID: 11}, + {ID: 12}, + }, nil, nil) + + repository := initRepository(t) + if repository != nil { + repository.pipelinesService = mockPipelineService + + pipelines, err := repository.GetPipelines(10, "master") + if assert.NoError(t, err) { + assert.Equal(t, []int{10, 11, 12}, pipelines) + mockPipelineService.AssertNumberOfCalls(t, "ListProjectPipelines", 1) + mockPipelineService.AssertExpectations(t) + } + } +} + +func TestRepository_GetMergeRequest_Error(t *testing.T) { + gitlabErr := errors.New("gitlab error") + + mockMergeRequestService := new(mocks.MergeRequestsService) + mockMergeRequestService.On("GetMergeRequest", Anything, AnythingOfType("int"), Anything, Anything). + Return(nil, nil, gitlabErr) + + repository := initRepository(t) + if repository != nil { + repository.mergeRequestsService = mockMergeRequestService + + _, err := repository.GetMergeRequest(10, 10) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "gitlab error") + mockMergeRequestService.AssertNumberOfCalls(t, "GetMergeRequest", 1) + mockMergeRequestService.AssertExpectations(t) + } + } +} + +func TestRepository_GetMergeRequest_Success(t *testing.T) { + gitlabMergeRequest := &gitlab.MergeRequest{ + ID: 10, + IID: 20, + Title: "Test", + SourceProjectID: 30, + SourceBranch: "master", + SHA: "12345", + Author: &gitlab.BasicUser{ + Username: "test", + AvatarURL: "test.example.com", + }, + } + + mockMergeRequestService := new(mocks.MergeRequestsService) + mockMergeRequestService.On("GetMergeRequest", Anything, AnythingOfType("int"), Anything, Anything). + Return(gitlabMergeRequest, nil, nil) + + mergeRequest := &models.MergeRequest{ + ID: 20, + Title: "Test", + Author: coreModels.Author{ + Name: "test", + AvatarURL: "test.example.com", + }, + SourceProjectID: 30, + SourceBranch: "master", + CommitSHA: "12345", + } + + repository := initRepository(t) + if repository != nil { + repository.mergeRequestsService = mockMergeRequestService + + result, err := repository.GetMergeRequest(10, 10) + if assert.NoError(t, err) { + assert.Equal(t, mergeRequest, result) + mockMergeRequestService.AssertNumberOfCalls(t, "GetMergeRequest", 1) + mockMergeRequestService.AssertExpectations(t) + } + } +} + +func TestRepository_GetMergeRequests_Error(t *testing.T) { + gitlabErr := errors.New("gitlab error") + + mockMergeRequestService := new(mocks.MergeRequestsService) + mockMergeRequestService.On("ListProjectMergeRequests", Anything, Anything, Anything). + Return(nil, nil, gitlabErr) + + repository := initRepository(t) + if repository != nil { + repository.mergeRequestsService = mockMergeRequestService + + _, err := repository.GetMergeRequests(10) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "gitlab error") + mockMergeRequestService.AssertNumberOfCalls(t, "ListProjectMergeRequests", 1) + mockMergeRequestService.AssertExpectations(t) + } + } +} + +func TestRepository_GetMergeRequests_Success(t *testing.T) { + gitlabMergeRequest := &gitlab.MergeRequest{ + ID: 10, + IID: 20, + Title: "Test", + SourceProjectID: 30, + SourceBranch: "master", + SHA: "12345", + Author: &gitlab.BasicUser{ + Username: "test", + AvatarURL: "test.example.com", + }, + } + + mockMergeRequestService := new(mocks.MergeRequestsService) + mockMergeRequestService.On("ListProjectMergeRequests", Anything, Anything, Anything). + Return([]*gitlab.MergeRequest{gitlabMergeRequest}, nil, nil) + + mergeRequest := models.MergeRequest{ + ID: 20, + Title: "Test", + Author: coreModels.Author{ + Name: "test", + AvatarURL: "test.example.com", + }, + SourceProjectID: 30, + SourceBranch: "master", + CommitSHA: "12345", + } + + repository := initRepository(t) + if repository != nil { + repository.mergeRequestsService = mockMergeRequestService + + result, err := repository.GetMergeRequests(10) + if assert.NoError(t, err) { + assert.Len(t, result, 1) + assert.Equal(t, mergeRequest, result[0]) + mockMergeRequestService.AssertNumberOfCalls(t, "ListProjectMergeRequests", 1) + mockMergeRequestService.AssertExpectations(t) + } + } +} + +func TestRepository_GetProject_Error(t *testing.T) { + gitlabErr := errors.New("gitlab error") + + mockProjectService := new(mocks.ProjectService) + mockProjectService.On("GetProject", Anything, Anything, Anything). + Return(nil, nil, gitlabErr) + + repository := initRepository(t) + if repository != nil { + repository.projectService = mockProjectService + + _, err := repository.GetProject(10) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "gitlab error") + mockProjectService.AssertNumberOfCalls(t, "GetProject", 1) + mockProjectService.AssertExpectations(t) + } + } +} + +func TestRepository_GetProject_Success(t *testing.T) { + gitlabProject := &gitlab.Project{ + ID: 10, + Path: "test2", + Namespace: &gitlab.ProjectNamespace{Path: "test1"}, + } + + mockProjectService := new(mocks.ProjectService) + mockProjectService.On("GetProject", Anything, Anything, Anything). + Return(gitlabProject, nil, nil) + + project := &models.Project{ + ID: 10, + Owner: "test1", + Repository: "test2", + } + + repository := initRepository(t) + if repository != nil { + repository.projectService = mockProjectService + + result, err := repository.GetProject(10) + if assert.NoError(t, err) { + assert.Equal(t, project, result) + mockProjectService.AssertNumberOfCalls(t, "GetProject", 1) + mockProjectService.AssertExpectations(t) + } + } +} diff --git a/monitorables/gitlab/api/usecase.go b/monitorables/gitlab/api/usecase.go new file mode 100644 index 00000000..acb25f18 --- /dev/null +++ b/monitorables/gitlab/api/usecase.go @@ -0,0 +1,25 @@ +//go:generate mockery -name Usecase + +package api + +import ( + uiConfigModels "github.com/monitoror/monitoror/api/config/models" + coreModels "github.com/monitoror/monitoror/models" + "github.com/monitoror/monitoror/monitorables/gitlab/api/models" +) + +const ( + GitlabIssuesTileType coreModels.TileType = "GITLAB-ISSUES" + GitlabPipelineTileType coreModels.TileType = "GITLAB-PIPELINE" + GitlabMergeRequestTileType coreModels.TileType = "GITLAB-MERGEREQUEST" +) + +type ( + Usecase interface { + Issues(params *models.IssuesParams) (*coreModels.Tile, error) + Pipeline(params *models.PipelineParams) (*coreModels.Tile, error) + MergeRequest(params *models.MergeRequestParams) (*coreModels.Tile, error) + + MergeRequestsGenerator(params interface{}) ([]uiConfigModels.GeneratedTile, error) + } +) diff --git a/monitorables/gitlab/api/usecase/gitlab.go b/monitorables/gitlab/api/usecase/gitlab.go new file mode 100644 index 00000000..e0f0ef49 --- /dev/null +++ b/monitorables/gitlab/api/usecase/gitlab.go @@ -0,0 +1,289 @@ +//+build !faker + +package usecase + +import ( + "fmt" + "time" + + "github.com/AlekSi/pointer" + uuid "github.com/satori/go.uuid" + + uiConfigModels "github.com/monitoror/monitoror/api/config/models" + monitorableCache "github.com/monitoror/monitoror/internal/pkg/monitorable/cache" + coreModels "github.com/monitoror/monitoror/models" + "github.com/monitoror/monitoror/monitorables/gitlab/api" + "github.com/monitoror/monitoror/monitorables/gitlab/api/models" + "github.com/monitoror/monitoror/pkg/git" + + "github.com/jsdidierlaurent/echo-middleware/cache" +) + +type ( + gitlabUsecase struct { + repository api.Repository + // Used to generate store key by repository + repositoryUID string + + // store is used to store persistent data (project, + store cache.Store + + // builds cache. used for save small history of build for stats + buildsCache *monitorableCache.BuildCache + } +) + +const ( + buildCacheSize = 5 + + projectCacheExpiration = cache.NEVER + mergeRequestCacheExpiration = time.Second * 30 + + GitlabProjectStoreKeyPrefix = "monitoror.gitlab.project.store" + GitlabMergeRequestStoreKeyPrefix = "monitoror.gitlab.mergeRequest.store" +) + +func NewGitlabUsecase(repository api.Repository, store cache.Store) api.Usecase { + return &gitlabUsecase{ + repository: repository, + repositoryUID: uuid.NewV4().String(), + store: store, + buildsCache: monitorableCache.NewBuildCache(buildCacheSize), + } +} + +func (gu *gitlabUsecase) Issues(params *models.IssuesParams) (*coreModels.Tile, error) { + tile := coreModels.NewTile(api.GitlabIssuesTileType).WithValue(coreModels.NumberUnit) + tile.Label = "GitLab count" + + count, err := gu.repository.GetIssues(params) + if err != nil { + return nil, &coreModels.MonitororError{Err: err, Tile: tile, Message: "unable to load issues"} + } + + tile.Status = coreModels.SuccessStatus + tile.Value.Values = append(tile.Value.Values, fmt.Sprintf("%d", count)) + + return tile, nil +} + +func (gu *gitlabUsecase) Pipeline(params *models.PipelineParams) (*coreModels.Tile, error) { + tile := coreModels.NewTile(api.GitlabPipelineTileType).WithBuild() + tile.Label = fmt.Sprintf("%d", params.ProjectID) + tile.Build.Branch = pointer.ToString(git.HumanizeBranch(params.Ref)) + + // Load Project and cache it + project, err := gu.getProject(*params.ProjectID) + if err != nil { + return nil, &coreModels.MonitororError{Err: err, Tile: tile, Message: "unable to load project"} + } + tile.Label = project.Repository + + // Load pipelines for given ref + pipelines, err := gu.repository.GetPipelines(*params.ProjectID, params.Ref) + if err != nil { + return nil, &coreModels.MonitororError{Err: err, Tile: tile, Message: "unable to load pipelines"} + } + if len(pipelines) == 0 { + // Warning because request was correct but there is no build + return nil, &coreModels.MonitororError{Tile: tile, Message: "no pipelines found", ErrorStatus: coreModels.UnknownStatus} + } + + // Load pipeline detail + pipeline, err := gu.repository.GetPipeline(*params.ProjectID, pipelines[0]) + if err != nil { + return nil, &coreModels.MonitororError{Err: err, Tile: tile, Message: "unable to load pipeline"} + } + + gu.computePipeline(params, tile, pipeline) + + // Author + if tile.Status == coreModels.FailedStatus { + tile.Build.Author = &pipeline.Author + } + + return tile, nil +} + +func (gu *gitlabUsecase) MergeRequest(params *models.MergeRequestParams) (*coreModels.Tile, error) { + tile := coreModels.NewTile(api.GitlabMergeRequestTileType).WithBuild() + tile.Label = fmt.Sprintf("%d", params.ProjectID) + + // Load Project and cache it + project, err := gu.getProject(*params.ProjectID) + if err != nil { + return nil, &coreModels.MonitororError{Err: err, Tile: tile, Message: "unable to load project"} + } + tile.Label = project.Repository + + // Load MergeRequest + mergeRequest, err := gu.getMergeRequest(*params.ProjectID, *params.ID) + if err != nil { + return nil, &coreModels.MonitororError{Err: err, Tile: tile, Message: "unable to load merge request"} + } + + // Load MergeRequest project + mergeRequestProject, err := gu.getProject(mergeRequest.SourceProjectID) + if err != nil { + return nil, &coreModels.MonitororError{Err: err, Tile: tile, Message: "unable to load project"} + } + + tile.Build.Branch = pointer.ToString(git.HumanizeBranch(mergeRequest.SourceBranch)) + if project.Owner != mergeRequestProject.Owner { + tile.Build.Branch = pointer.ToString(fmt.Sprintf("%s:%s", mergeRequestProject.Owner, *tile.Build.Branch)) + } + tile.Build.MergeRequest = &coreModels.TileMergeRequest{ + ID: mergeRequest.ID, + Title: mergeRequest.Title, + } + + // Load pipelines for given ref in case of fork + pipelines, err := gu.repository.GetPipelines(*params.ProjectID, mergeRequest.SourceBranch) + if err != nil { + return nil, &coreModels.MonitororError{Err: err, Tile: tile, Message: "unable to load pipelines"} + } + if len(pipelines) == 0 { + // Warning because request was correct but there is no build + return nil, &coreModels.MonitororError{Tile: tile, Message: "no pipelines found", ErrorStatus: coreModels.UnknownStatus} + } + + // Load pipeline detail + pipeline, err := gu.repository.GetPipeline(*params.ProjectID, pipelines[0]) + if err != nil { + return nil, &coreModels.MonitororError{Err: err, Tile: tile, Message: "unable to load pipeline"} + } + + gu.computePipeline(params, tile, pipeline) + + // Author + if tile.Status == coreModels.FailedStatus { + tile.Build.Author = &mergeRequest.Author + } + + return tile, nil +} + +func (gu *gitlabUsecase) computePipeline(params interface{}, tile *coreModels.Tile, pipeline *models.Pipeline) { + tile.Status = parseStatus(pipeline.Status) + + // Set Previous Status + strPipelineID := fmt.Sprintf("%d", pipeline.ID) + previousStatus := gu.buildsCache.GetPreviousStatus(params, strPipelineID) + if previousStatus != nil { + tile.Build.PreviousStatus = *previousStatus + } else { + tile.Build.PreviousStatus = coreModels.UnknownStatus + } + + // StartedAt / FinishedAt + tile.Build.StartedAt = pipeline.StartedAt + if tile.Status != coreModels.RunningStatus && tile.Status != coreModels.QueuedStatus { + tile.Build.FinishedAt = pipeline.FinishedAt + } + + // Duration + if tile.Status == coreModels.RunningStatus { + tile.Build.Duration = pointer.ToInt64(int64(time.Since(*tile.Build.StartedAt).Seconds())) + + estimatedDuration := gu.buildsCache.GetEstimatedDuration(params) + if estimatedDuration != nil { + tile.Build.EstimatedDuration = pointer.ToInt64(int64(estimatedDuration.Seconds())) + } else { + tile.Build.EstimatedDuration = pointer.ToInt64(int64(0)) + } + } + + // Cache Duration when success / failed + if tile.Status == coreModels.SuccessStatus || tile.Status == coreModels.FailedStatus { + gu.buildsCache.Add(params, strPipelineID, tile.Status, tile.Build.FinishedAt.Sub(*tile.Build.StartedAt)) + } +} + +func (gu *gitlabUsecase) MergeRequestsGenerator(params interface{}) ([]uiConfigModels.GeneratedTile, error) { + prParams := params.(*models.MergeRequestGeneratorParams) + + mergeRequests, err := gu.repository.GetMergeRequests(*prParams.ProjectID) + if err != nil { + return nil, &coreModels.MonitororError{Err: err, Message: "unable to load merge requests"} + } + + var results []uiConfigModels.GeneratedTile + for _, mergeRequest := range mergeRequests { + p := &models.MergeRequestParams{} + p.ProjectID = prParams.ProjectID + p.ID = pointer.ToInt(mergeRequest.ID) + + results = append(results, uiConfigModels.GeneratedTile{ + Params: p, + }) + + // Add merge request into store + _ = gu.store.Set(gu.getMergeRequestStoreKey(*prParams.ProjectID, mergeRequest.ID), mergeRequest, mergeRequestCacheExpiration) + } + + return results, nil +} + +func (gu *gitlabUsecase) getProjectStoreKey(projectID int) string { + return fmt.Sprintf("%s:%s-%d", GitlabProjectStoreKeyPrefix, gu.repositoryUID, projectID) +} + +// getProject load project information (from cache or api) and add result in cache +func (gu *gitlabUsecase) getProject(projectID int) (*models.Project, error) { + project := &models.Project{} + + storeKey := gu.getProjectStoreKey(projectID) + if err := gu.store.Get(storeKey, project); err != nil { + if project, err = gu.repository.GetProject(projectID); err != nil { + return nil, err + } + + _ = gu.store.Set(storeKey, *project, projectCacheExpiration) + } + + return project, nil +} + +func (gu *gitlabUsecase) getMergeRequestStoreKey(projectID int, mergeRequestID int) string { + return fmt.Sprintf("%s:%s-%d-%d", GitlabMergeRequestStoreKeyPrefix, gu.repositoryUID, projectID, mergeRequestID) +} + +// getMergeRequest load merge request information (from cache or api) and add result in cache +func (gu *gitlabUsecase) getMergeRequest(projectID int, mergeRequestID int) (*models.MergeRequest, error) { + mergeRequest := &models.MergeRequest{} + + storeKey := gu.getMergeRequestStoreKey(projectID, mergeRequestID) + if err := gu.store.Get(storeKey, mergeRequest); err != nil { + if mergeRequest, err = gu.repository.GetMergeRequest(projectID, mergeRequestID); err != nil { + return nil, err + } + + _ = gu.store.Set(storeKey, *mergeRequest, mergeRequestCacheExpiration) + } + + return mergeRequest, nil +} + +func parseStatus(status string) coreModels.TileStatus { + // See: https://docs.gitlab.com/ee/api/pipelines.html#list-project-pipelines + switch status { + case "running": + return coreModels.RunningStatus + case "pending": + return coreModels.QueuedStatus + case "success": + return coreModels.SuccessStatus + case "failed": + return coreModels.FailedStatus + case "canceled": + return coreModels.CanceledStatus + case "skipped": + return coreModels.CanceledStatus + case "created": + return coreModels.QueuedStatus + case "manual": + return coreModels.ActionRequiredStatus + default: + return coreModels.UnknownStatus + } +} diff --git a/monitorables/gitlab/api/usecase/gitlab_faker.go b/monitorables/gitlab/api/usecase/gitlab_faker.go new file mode 100644 index 00000000..1b1c12a6 --- /dev/null +++ b/monitorables/gitlab/api/usecase/gitlab_faker.go @@ -0,0 +1,167 @@ +//+build faker + +package usecase + +import ( + "fmt" + "time" + + "github.com/AlekSi/pointer" + cmap "github.com/orcaman/concurrent-map" + + uiConfigModels "github.com/monitoror/monitoror/api/config/models" + "github.com/monitoror/monitoror/internal/pkg/monitorable/faker" + coreModels "github.com/monitoror/monitoror/models" + "github.com/monitoror/monitoror/monitorables/gitlab/api" + "github.com/monitoror/monitoror/monitorables/gitlab/api/models" + "github.com/monitoror/monitoror/pkg/git" + "github.com/monitoror/monitoror/pkg/nonempty" +) + +type ( + gitlabUsecase struct { + timeRefByProject cmap.ConcurrentMap + } +) + +var availableBuildStatus = faker.Statuses{ + {coreModels.SuccessStatus, time.Second * 30}, + {coreModels.FailedStatus, time.Second * 30}, + {coreModels.CanceledStatus, time.Second * 20}, + {coreModels.RunningStatus, time.Second * 60}, + {coreModels.QueuedStatus, time.Second * 30}, + {coreModels.ActionRequiredStatus, time.Second * 20}, +} + +func NewGitlabUsecase() api.Usecase { + return &gitlabUsecase{cmap.New()} +} + +func (gu *gitlabUsecase) Issues(params *models.IssuesParams) (*coreModels.Tile, error) { + tile := coreModels.NewTile(api.GitlabIssuesTileType).WithValue(coreModels.NumberUnit) + tile.Label = "GitLab issues" + + tile.Status = coreModels.SuccessStatus + + if len(params.ValueValues) != 0 { + tile.Value.Values = params.ValueValues + } else { + tile.Value.Values = []string{"42"} + } + + return tile, nil +} + +func (gu *gitlabUsecase) Pipeline(params *models.PipelineParams) (tile *coreModels.Tile, err error) { + tile = coreModels.NewTile(api.GitlabPipelineTileType).WithBuild() + tile.Label = fmt.Sprintf("%d", params.ProjectID) + + projectID := fmt.Sprintf("%d-%s", params.ProjectID, params.Ref) + tile.Status = nonempty.Struct(params.Status, gu.computeStatus(projectID)).(coreModels.TileStatus) + + tile.Build.Branch = pointer.ToString(git.HumanizeBranch(params.Ref)) + tile.Build.PreviousStatus = nonempty.Struct(params.PreviousStatus, coreModels.SuccessStatus).(coreModels.TileStatus) + + // Author + if tile.Status == coreModels.FailedStatus { + tile.Build.Author = &coreModels.Author{} + tile.Build.Author.Name = nonempty.String(params.AuthorName, "John Doe") + tile.Build.Author.AvatarURL = nonempty.String(params.AuthorAvatarURL, "https://monitoror.com/assets/images/avatar.png") + } + + // Duration / EstimatedDuration + if tile.Status == coreModels.RunningStatus { + estimatedDuration := nonempty.Duration(time.Duration(params.EstimatedDuration), time.Second*300) + tile.Build.Duration = pointer.ToInt64(nonempty.Int64(params.Duration, int64(gu.computeDuration(projectID, estimatedDuration).Seconds()))) + + if tile.Build.PreviousStatus != coreModels.UnknownStatus { + tile.Build.EstimatedDuration = pointer.ToInt64(int64(estimatedDuration.Seconds())) + } else { + tile.Build.EstimatedDuration = pointer.ToInt64(0) + } + } + + // StartedAt / FinishedAt + if tile.Build.Duration == nil { + tile.Build.StartedAt = pointer.ToTime(nonempty.Time(params.StartedAt, time.Now().Add(-time.Minute*10))) + } else { + tile.Build.StartedAt = pointer.ToTime(nonempty.Time(params.StartedAt, time.Now().Add(-time.Second*time.Duration(*tile.Build.Duration)))) + } + + if tile.Status != coreModels.QueuedStatus && tile.Status != coreModels.RunningStatus { + tile.Build.FinishedAt = pointer.ToTime(nonempty.Time(params.FinishedAt, tile.Build.StartedAt.Add(time.Minute*5))) + } + + return tile, nil +} + +func (gu *gitlabUsecase) MergeRequest(params *models.MergeRequestParams) (tile *coreModels.Tile, err error) { + tile = coreModels.NewTile(api.GitlabMergeRequestTileType).WithBuild() + tile.Label = fmt.Sprintf("%d", params.ProjectID) + + projectID := fmt.Sprintf("%d-%d", params.ProjectID, params.ID) + tile.Status = nonempty.Struct(params.Status, gu.computeStatus(projectID)).(coreModels.TileStatus) + + tile.Build.Branch = pointer.ToString(nonempty.String(git.HumanizeBranch(params.Branch), "feature-branch")) + tile.Build.PreviousStatus = nonempty.Struct(params.PreviousStatus, coreModels.SuccessStatus).(coreModels.TileStatus) + tile.Build.MergeRequest = &coreModels.TileMergeRequest{ + ID: *params.ID, + Title: nonempty.String(params.MergeRequestTitle, "Feature branch title"), + } + + // Author + if tile.Status == coreModels.FailedStatus { + tile.Build.Author = &coreModels.Author{} + tile.Build.Author.Name = nonempty.String(params.AuthorName, "John Doe") + tile.Build.Author.AvatarURL = nonempty.String(params.AuthorAvatarURL, "https://monitoror.com/assets/images/avatar.png") + } + + // Duration / EstimatedDuration + if tile.Status == coreModels.RunningStatus { + estimatedDuration := nonempty.Duration(time.Duration(params.EstimatedDuration), time.Second*300) + tile.Build.Duration = pointer.ToInt64(nonempty.Int64(params.Duration, int64(gu.computeDuration(projectID, estimatedDuration).Seconds()))) + + if tile.Build.PreviousStatus != coreModels.UnknownStatus { + tile.Build.EstimatedDuration = pointer.ToInt64(int64(estimatedDuration.Seconds())) + } else { + tile.Build.EstimatedDuration = pointer.ToInt64(0) + } + } + + // StartedAt / FinishedAt + if tile.Build.Duration == nil { + tile.Build.StartedAt = pointer.ToTime(nonempty.Time(params.StartedAt, time.Now().Add(-time.Minute*10))) + } else { + tile.Build.StartedAt = pointer.ToTime(nonempty.Time(params.StartedAt, time.Now().Add(-time.Second*time.Duration(*tile.Build.Duration)))) + } + + if tile.Status != coreModels.QueuedStatus && tile.Status != coreModels.RunningStatus { + tile.Build.FinishedAt = pointer.ToTime(nonempty.Time(params.FinishedAt, tile.Build.StartedAt.Add(time.Minute*5))) + } + + return tile, nil +} + +func (gu *gitlabUsecase) MergeRequestsGenerator(params interface{}) ([]uiConfigModels.GeneratedTile, error) { + panic("unimplemented") +} + +func (gu *gitlabUsecase) computeStatus(projectUID string) coreModels.TileStatus { + value, ok := gu.timeRefByProject.Get(projectUID) + if !ok || value == nil { + value = faker.GetRefTime() + gu.timeRefByProject.Set(projectUID, value) + } + + return faker.ComputeStatus(value.(time.Time), availableBuildStatus) +} + +func (gu *gitlabUsecase) computeDuration(projectUID string, duration time.Duration) time.Duration { + value, ok := gu.timeRefByProject.Get(projectUID) + if !ok || value == nil { + value = faker.GetRefTime() + gu.timeRefByProject.Set(projectUID, value) + } + + return faker.ComputeDuration(value.(time.Time), duration) +} diff --git a/monitorables/gitlab/api/usecase/gitlab_test.go b/monitorables/gitlab/api/usecase/gitlab_test.go new file mode 100644 index 00000000..ea3d2a50 --- /dev/null +++ b/monitorables/gitlab/api/usecase/gitlab_test.go @@ -0,0 +1,581 @@ +package usecase + +import ( + "errors" + "testing" + "time" + + "github.com/AlekSi/pointer" + "github.com/stretchr/testify/assert" + + coreModels "github.com/monitoror/monitoror/models" + "github.com/monitoror/monitoror/monitorables/gitlab/api" + "github.com/monitoror/monitoror/monitorables/gitlab/api/mocks" + "github.com/monitoror/monitoror/monitorables/gitlab/api/models" + + "github.com/jsdidierlaurent/echo-middleware/cache" + "github.com/stretchr/testify/mock" +) + +func initUsecase(mockRepository api.Repository) *gitlabUsecase { + store := cache.NewGoCacheStore(time.Minute*5, time.Second) + gu := NewGitlabUsecase(mockRepository, store) + castedGu := gu.(*gitlabUsecase) + return castedGu +} + +func TestUsecase_Issues_Error(t *testing.T) { + mockRepository := new(mocks.Repository) + mockRepository.On("GetIssues", mock.Anything). + Return(0, errors.New("boom")) + + gu := initUsecase(mockRepository) + + tile, err := gu.Issues(&models.IssuesParams{}) + if assert.Error(t, err) { + assert.Nil(t, tile) + assert.IsType(t, &coreModels.MonitororError{}, err) + assert.Equal(t, "unable to load issues", err.Error()) + mockRepository.AssertNumberOfCalls(t, "GetIssues", 1) + mockRepository.AssertExpectations(t) + } +} + +func TestUsecase_Issues_Success(t *testing.T) { + mockRepository := new(mocks.Repository) + mockRepository.On("GetIssues", mock.Anything). + Return(42, nil) + + gu := initUsecase(mockRepository) + + expected := coreModels.NewTile(api.GitlabIssuesTileType).WithValue(coreModels.NumberUnit) + expected.Label = "GitLab count" + expected.Status = coreModels.SuccessStatus + expected.Value.Values = []string{"42"} + + tile, err := gu.Issues(&models.IssuesParams{}) + if assert.NoError(t, err) { + assert.Equal(t, expected, tile) + mockRepository.AssertNumberOfCalls(t, "GetIssues", 1) + mockRepository.AssertExpectations(t) + } +} + +func TestUsecase_Pipeline_ErrorProject(t *testing.T) { + mockRepository := new(mocks.Repository) + mockRepository.On("GetProject", mock.Anything). + Return(nil, errors.New("boom")) + + gu := initUsecase(mockRepository) + + tile, err := gu.Pipeline(&models.PipelineParams{ProjectID: pointer.ToInt(10), Ref: "master"}) + if assert.Error(t, err) { + assert.Nil(t, tile) + assert.IsType(t, &coreModels.MonitororError{}, err) + assert.Equal(t, "unable to load project", err.Error()) + mockRepository.AssertNumberOfCalls(t, "GetProject", 1) + mockRepository.AssertExpectations(t) + } +} + +func TestUsecase_Pipeline_ErrorPipelines(t *testing.T) { + mockRepository := new(mocks.Repository) + mockRepository.On("GetProject", mock.Anything). + Return(&models.Project{Repository: "Test"}, nil) + mockRepository.On("GetPipelines", mock.Anything, mock.Anything). + Return(nil, errors.New("boom")) + + gu := initUsecase(mockRepository) + + tile, err := gu.Pipeline(&models.PipelineParams{ProjectID: pointer.ToInt(10), Ref: "master"}) + if assert.Error(t, err) { + assert.Nil(t, tile) + assert.IsType(t, &coreModels.MonitororError{}, err) + assert.Equal(t, "unable to load pipelines", err.Error()) + mockRepository.AssertNumberOfCalls(t, "GetProject", 1) + mockRepository.AssertNumberOfCalls(t, "GetPipelines", 1) + mockRepository.AssertExpectations(t) + } +} + +func TestUsecase_Pipeline_NoPipelines(t *testing.T) { + mockRepository := new(mocks.Repository) + mockRepository.On("GetProject", mock.Anything). + Return(&models.Project{Repository: "Test"}, nil) + mockRepository.On("GetPipelines", mock.Anything, mock.Anything). + Return([]int{}, nil) + + gu := initUsecase(mockRepository) + + tile, err := gu.Pipeline(&models.PipelineParams{ProjectID: pointer.ToInt(10), Ref: "master"}) + if assert.Error(t, err) { + assert.Nil(t, tile) + assert.IsType(t, &coreModels.MonitororError{}, err) + assert.Equal(t, "no pipelines found", err.Error()) + mockRepository.AssertNumberOfCalls(t, "GetProject", 1) + mockRepository.AssertNumberOfCalls(t, "GetPipelines", 1) + mockRepository.AssertExpectations(t) + } +} + +func TestUsecase_Pipeline_ErrorPipeline(t *testing.T) { + mockRepository := new(mocks.Repository) + mockRepository.On("GetProject", mock.Anything). + Return(&models.Project{Repository: "Test"}, nil) + mockRepository.On("GetPipelines", mock.Anything, mock.Anything). + Return([]int{10}, nil) + mockRepository.On("GetPipeline", mock.Anything, mock.Anything). + Return(nil, errors.New("boom")) + + gu := initUsecase(mockRepository) + + tile, err := gu.Pipeline(&models.PipelineParams{ProjectID: pointer.ToInt(10), Ref: "master"}) + if assert.Error(t, err) { + assert.Nil(t, tile) + assert.IsType(t, &coreModels.MonitororError{}, err) + assert.Equal(t, "unable to load pipeline", err.Error()) + mockRepository.AssertNumberOfCalls(t, "GetProject", 1) + mockRepository.AssertNumberOfCalls(t, "GetPipelines", 1) + mockRepository.AssertNumberOfCalls(t, "GetPipeline", 1) + mockRepository.AssertExpectations(t) + } +} + +func TestUsecase_Pipeline_Success(t *testing.T) { + refTime := time.Now() + startedAt := refTime.Add(-time.Second * 30) + finishedAt := refTime.Add(-time.Second * 15) + + pipeline := &models.Pipeline{ + ID: 10, + Branch: "master", + Status: "success", + StartedAt: &startedAt, + FinishedAt: &finishedAt, + } + + expected := coreModels.NewTile(api.GitlabPipelineTileType).WithBuild() + expected.Label = "project" + expected.Build.Branch = pointer.ToString("master") + expected.Status = coreModels.SuccessStatus + expected.Build.PreviousStatus = coreModels.UnknownStatus + expected.Build.StartedAt = pointer.ToTime(startedAt) + expected.Build.FinishedAt = pointer.ToTime(finishedAt) + + testPipeline(t, pipeline, expected) +} + +func TestUsecase_Pipeline_Failed(t *testing.T) { + refTime := time.Now() + startedAt := refTime.Add(-time.Second * 30) + finishedAt := refTime.Add(-time.Second * 15) + + pipeline := &models.Pipeline{ + ID: 10, + Branch: "master", + Author: coreModels.Author{ + Name: "author", + AvatarURL: "author.exemple.com", + }, + Status: "failed", + StartedAt: &startedAt, + FinishedAt: &finishedAt, + } + + expected := coreModels.NewTile(api.GitlabPipelineTileType).WithBuild() + expected.Label = "project" + expected.Build.Branch = pointer.ToString("master") + expected.Status = coreModels.FailedStatus + expected.Build.PreviousStatus = coreModels.UnknownStatus + expected.Build.StartedAt = pointer.ToTime(startedAt) + expected.Build.FinishedAt = pointer.ToTime(finishedAt) + expected.Build.Author = &coreModels.Author{ + Name: "author", + AvatarURL: "author.exemple.com", + } + + testPipeline(t, pipeline, expected) +} + +func TestUsecase_Pipeline_Running(t *testing.T) { + refTime := time.Now() + startedAt := refTime.Add(-time.Second * 30) + + pipeline := &models.Pipeline{ + ID: 10, + Branch: "master", + Status: "running", + StartedAt: &startedAt, + } + + expected := coreModels.NewTile(api.GitlabPipelineTileType).WithBuild() + expected.Label = "project" + expected.Build.Branch = pointer.ToString("master") + expected.Status = coreModels.RunningStatus + expected.Build.PreviousStatus = coreModels.UnknownStatus + expected.Build.StartedAt = pointer.ToTime(startedAt) + expected.Build.Duration = pointer.ToInt64(30) + expected.Build.EstimatedDuration = pointer.ToInt64(0) + + testPipeline(t, pipeline, expected) +} + +func TestUsecase_Pipeline_Queued(t *testing.T) { + refTime := time.Now() + startedAt := refTime.Add(-time.Second * 30) + + pipeline := &models.Pipeline{ + ID: 10, + Branch: "master", + Status: "pending", + StartedAt: &startedAt, + } + + expected := coreModels.NewTile(api.GitlabPipelineTileType).WithBuild() + expected.Label = "project" + expected.Build.Branch = pointer.ToString("master") + expected.Status = coreModels.QueuedStatus + expected.Build.PreviousStatus = coreModels.UnknownStatus + expected.Build.StartedAt = pointer.ToTime(startedAt) + + testPipeline(t, pipeline, expected) +} + +func testPipeline(t *testing.T, pipeline *models.Pipeline, expected *coreModels.Tile) { + mockRepository := new(mocks.Repository) + mockRepository.On("GetProject", mock.Anything). + Return(&models.Project{Repository: "project"}, nil) + mockRepository.On("GetPipelines", mock.Anything, mock.Anything). + Return([]int{10}, nil) + mockRepository.On("GetPipeline", mock.Anything, mock.Anything). + Return(pipeline, nil) + + gu := initUsecase(mockRepository) + + tile, err := gu.Pipeline(&models.PipelineParams{ProjectID: pointer.ToInt(10), Ref: "master"}) + if assert.NoError(t, err) { + assert.Equal(t, expected, tile) + mockRepository.AssertNumberOfCalls(t, "GetProject", 1) + mockRepository.AssertNumberOfCalls(t, "GetPipelines", 1) + mockRepository.AssertNumberOfCalls(t, "GetPipeline", 1) + mockRepository.AssertExpectations(t) + } +} + +func TestUsecase_Pipeline_WithPrevious(t *testing.T) { + refTime := time.Now() + startedAt := refTime.Add(-time.Second * 30) + finishedAt := refTime.Add(-time.Second * 15) + + pipeline := &models.Pipeline{ + ID: 10, + Branch: "master", + Status: "success", + StartedAt: &startedAt, + FinishedAt: &finishedAt, + } + + mockRepository := new(mocks.Repository) + mockRepository.On("GetProject", mock.Anything). + Return(&models.Project{Repository: "project"}, nil) + mockRepository.On("GetPipelines", mock.Anything, mock.Anything). + Return([]int{10}, nil) + mockRepository.On("GetPipeline", mock.Anything, mock.Anything). + Return(pipeline, nil) + + gu := initUsecase(mockRepository) + + expected := coreModels.NewTile(api.GitlabPipelineTileType).WithBuild() + expected.Label = "project" + expected.Build.Branch = pointer.ToString("master") + expected.Status = coreModels.SuccessStatus + expected.Build.PreviousStatus = coreModels.UnknownStatus + expected.Build.StartedAt = pointer.ToTime(startedAt) + expected.Build.FinishedAt = pointer.ToTime(finishedAt) + + tile, err := gu.Pipeline(&models.PipelineParams{ProjectID: pointer.ToInt(10), Ref: "master"}) + if assert.NoError(t, err) { + assert.Equal(t, expected, tile) + } + + pipeline.ID = 20 + pipeline.Status = "running" + pipeline.FinishedAt = nil + + expected.Status = coreModels.RunningStatus + expected.Build.PreviousStatus = coreModels.SuccessStatus + expected.Build.Duration = pointer.ToInt64(30) + expected.Build.EstimatedDuration = pointer.ToInt64(15) + expected.Build.FinishedAt = nil + + tile, err = gu.Pipeline(&models.PipelineParams{ProjectID: pointer.ToInt(10), Ref: "master"}) + if assert.NoError(t, err) { + assert.Equal(t, expected, tile) + } + + mockRepository.AssertNumberOfCalls(t, "GetProject", 1) + mockRepository.AssertNumberOfCalls(t, "GetPipelines", 2) + mockRepository.AssertNumberOfCalls(t, "GetPipeline", 2) + mockRepository.AssertExpectations(t) +} + +func TestUsecase_MergeRequest_ErrorProject(t *testing.T) { + mockRepository := new(mocks.Repository) + mockRepository.On("GetProject", mock.Anything). + Return(nil, errors.New("boom")) + + gu := initUsecase(mockRepository) + + tile, err := gu.MergeRequest(&models.MergeRequestParams{ProjectID: pointer.ToInt(10), ID: pointer.ToInt(10)}) + if assert.Error(t, err) { + assert.Nil(t, tile) + assert.IsType(t, &coreModels.MonitororError{}, err) + assert.Equal(t, "unable to load project", err.Error()) + mockRepository.AssertNumberOfCalls(t, "GetProject", 1) + mockRepository.AssertExpectations(t) + } +} + +func TestUsecase_MergeRequest_ErrorMergeRequest(t *testing.T) { + mockRepository := new(mocks.Repository) + mockRepository.On("GetProject", mock.Anything). + Return(&models.Project{Repository: "Test"}, nil) + mockRepository.On("GetMergeRequest", mock.Anything, mock.Anything). + Return(nil, errors.New("boom")) + + gu := initUsecase(mockRepository) + + tile, err := gu.MergeRequest(&models.MergeRequestParams{ProjectID: pointer.ToInt(10), ID: pointer.ToInt(10)}) + if assert.Error(t, err) { + assert.Nil(t, tile) + assert.IsType(t, &coreModels.MonitororError{}, err) + assert.Equal(t, "unable to load merge request", err.Error()) + mockRepository.AssertNumberOfCalls(t, "GetProject", 1) + mockRepository.AssertNumberOfCalls(t, "GetMergeRequest", 1) + mockRepository.AssertExpectations(t) + } +} + +func TestUsecase_MergeRequest_ErrorSourceProject(t *testing.T) { + mockRepository := new(mocks.Repository) + mockRepository.On("GetProject", mock.Anything). + Return(&models.Project{Repository: "Test"}, nil).Once() + mockRepository.On("GetProject", mock.Anything). + Return(nil, errors.New("boom")) + mockRepository.On("GetMergeRequest", mock.Anything, mock.Anything). + Return(&models.MergeRequest{SourceProjectID: 20}, nil) + + gu := initUsecase(mockRepository) + + tile, err := gu.MergeRequest(&models.MergeRequestParams{ProjectID: pointer.ToInt(10), ID: pointer.ToInt(10)}) + if assert.Error(t, err) { + assert.Nil(t, tile) + assert.IsType(t, &coreModels.MonitororError{}, err) + assert.Equal(t, "unable to load project", err.Error()) + mockRepository.AssertNumberOfCalls(t, "GetProject", 2) + mockRepository.AssertNumberOfCalls(t, "GetMergeRequest", 1) + mockRepository.AssertExpectations(t) + } +} + +func TestUsecase_MergeRequest_ErrorPipelines(t *testing.T) { + mockRepository := new(mocks.Repository) + mockRepository.On("GetProject", mock.Anything). + Return(&models.Project{Repository: "Test"}, nil) + mockRepository.On("GetMergeRequest", mock.Anything, mock.Anything). + Return(&models.MergeRequest{SourceProjectID: 20}, nil) + mockRepository.On("GetPipelines", mock.Anything, mock.Anything). + Return(nil, errors.New("boom")) + + gu := initUsecase(mockRepository) + + tile, err := gu.MergeRequest(&models.MergeRequestParams{ProjectID: pointer.ToInt(10), ID: pointer.ToInt(10)}) + if assert.Error(t, err) { + assert.Nil(t, tile) + assert.IsType(t, &coreModels.MonitororError{}, err) + assert.Equal(t, "unable to load pipelines", err.Error()) + mockRepository.AssertNumberOfCalls(t, "GetProject", 2) + mockRepository.AssertNumberOfCalls(t, "GetMergeRequest", 1) + mockRepository.AssertNumberOfCalls(t, "GetPipelines", 1) + mockRepository.AssertExpectations(t) + } +} + +func TestUsecase_MergeRequest_ErrorPipeline(t *testing.T) { + mockRepository := new(mocks.Repository) + mockRepository.On("GetProject", mock.Anything). + Return(&models.Project{Repository: "Test"}, nil) + mockRepository.On("GetMergeRequest", mock.Anything, mock.Anything). + Return(&models.MergeRequest{SourceProjectID: 20}, nil) + mockRepository.On("GetPipelines", mock.Anything, mock.Anything). + Return([]int{30}, nil) + mockRepository.On("GetPipeline", mock.Anything, mock.Anything). + Return(nil, errors.New("boom")) + + gu := initUsecase(mockRepository) + + tile, err := gu.MergeRequest(&models.MergeRequestParams{ProjectID: pointer.ToInt(10), ID: pointer.ToInt(10)}) + if assert.Error(t, err) { + assert.Nil(t, tile) + assert.IsType(t, &coreModels.MonitororError{}, err) + assert.Equal(t, "unable to load pipeline", err.Error()) + mockRepository.AssertNumberOfCalls(t, "GetProject", 2) + mockRepository.AssertNumberOfCalls(t, "GetMergeRequest", 1) + mockRepository.AssertNumberOfCalls(t, "GetPipelines", 1) + mockRepository.AssertNumberOfCalls(t, "GetPipeline", 1) + mockRepository.AssertExpectations(t) + } +} + +func TestUsecase_MergeRequest_NoPipelines(t *testing.T) { + mockRepository := new(mocks.Repository) + mockRepository.On("GetProject", mock.Anything). + Return(&models.Project{Repository: "Test"}, nil) + mockRepository.On("GetMergeRequest", mock.Anything, mock.Anything). + Return(&models.MergeRequest{SourceProjectID: 20}, nil) + mockRepository.On("GetPipelines", mock.Anything, mock.Anything). + Return([]int{}, nil) + + gu := initUsecase(mockRepository) + + tile, err := gu.MergeRequest(&models.MergeRequestParams{ProjectID: pointer.ToInt(10), ID: pointer.ToInt(10)}) + if assert.Error(t, err) { + assert.Nil(t, tile) + assert.IsType(t, &coreModels.MonitororError{}, err) + assert.Equal(t, "no pipelines found", err.Error()) + mockRepository.AssertNumberOfCalls(t, "GetProject", 2) + mockRepository.AssertNumberOfCalls(t, "GetMergeRequest", 1) + mockRepository.AssertNumberOfCalls(t, "GetPipelines", 1) + mockRepository.AssertExpectations(t) + } +} + +func TestUsecase_MergeRequest_Success(t *testing.T) { + refTime := time.Now() + startedAt := refTime.Add(-time.Second * 30) + finishedAt := refTime.Add(-time.Second * 15) + + mergeRequest := &models.MergeRequest{ + ID: 10, + Title: "Test MR", + Author: coreModels.Author{ + Name: "author", + AvatarURL: "author.example.com", + }, + SourceProjectID: 20, + SourceBranch: "master", + CommitSHA: "12345", + } + + pipeline := &models.Pipeline{ + ID: 10, + Branch: "master", + Status: "failed", + StartedAt: &startedAt, + FinishedAt: &finishedAt, + } + + mockRepository := new(mocks.Repository) + mockRepository.On("GetProject", mock.Anything). + Return(&models.Project{Repository: "project"}, nil).Once() + mockRepository.On("GetProject", mock.Anything). + Return(&models.Project{Owner: "faker", Repository: "project"}, nil) + mockRepository.On("GetMergeRequest", mock.Anything, mock.Anything). + Return(mergeRequest, nil) + mockRepository.On("GetPipelines", mock.Anything, mock.Anything). + Return([]int{30}, nil) + mockRepository.On("GetPipeline", mock.Anything, mock.Anything). + Return(pipeline, nil) + + gu := initUsecase(mockRepository) + + expected := coreModels.NewTile(api.GitlabMergeRequestTileType).WithBuild() + expected.Label = "project" + expected.Build.Branch = pointer.ToString("faker:master") + expected.Build.MergeRequest = &coreModels.TileMergeRequest{ + ID: 10, + Title: "Test MR", + } + expected.Build.Author = &coreModels.Author{ + Name: "author", + AvatarURL: "author.example.com", + } + expected.Status = coreModels.FailedStatus + expected.Build.PreviousStatus = coreModels.UnknownStatus + expected.Build.StartedAt = pointer.ToTime(startedAt) + expected.Build.FinishedAt = pointer.ToTime(finishedAt) + + tile, err := gu.MergeRequest(&models.MergeRequestParams{ProjectID: pointer.ToInt(10), ID: pointer.ToInt(10)}) + if assert.NoError(t, err) { + assert.Equal(t, expected, tile) + mockRepository.AssertNumberOfCalls(t, "GetProject", 2) + mockRepository.AssertNumberOfCalls(t, "GetMergeRequest", 1) + mockRepository.AssertNumberOfCalls(t, "GetPipelines", 1) + mockRepository.AssertNumberOfCalls(t, "GetPipeline", 1) + mockRepository.AssertExpectations(t) + } +} + +func TestUsecase_MergeRequests_Error(t *testing.T) { + mockRepository := new(mocks.Repository) + mockRepository.On("GetMergeRequests", mock.Anything). + Return(nil, errors.New("boom")) + + gu := initUsecase(mockRepository) + + generated, err := gu.MergeRequestsGenerator(&models.MergeRequestGeneratorParams{ProjectID: pointer.ToInt(10)}) + if assert.Error(t, err) { + assert.Nil(t, generated) + assert.IsType(t, &coreModels.MonitororError{}, err) + assert.Equal(t, "unable to load merge requests", err.Error()) + mockRepository.AssertNumberOfCalls(t, "GetMergeRequests", 1) + mockRepository.AssertExpectations(t) + } +} + +func TestUsecase_MergeRequests_Success(t *testing.T) { + mockRepository := new(mocks.Repository) + mockRepository.On("GetMergeRequests", mock.Anything). + Return([]models.MergeRequest{{ID: 10}}, nil) + + gu := initUsecase(mockRepository) + + generated, err := gu.MergeRequestsGenerator(&models.MergeRequestGeneratorParams{ProjectID: pointer.ToInt(10)}) + if assert.NoError(t, err) { + assert.Len(t, generated, 1) + assert.Equal(t, 10, *generated[0].Params.(*models.MergeRequestParams).ID) + mockRepository.AssertNumberOfCalls(t, "GetMergeRequests", 1) + mockRepository.AssertExpectations(t) + } +} + +func TestUsecase_parseStatus(t *testing.T) { + assert.Equal(t, coreModels.RunningStatus, parseStatus("running")) + assert.Equal(t, coreModels.QueuedStatus, parseStatus("pending")) + assert.Equal(t, coreModels.SuccessStatus, parseStatus("success")) + assert.Equal(t, coreModels.FailedStatus, parseStatus("failed")) + assert.Equal(t, coreModels.CanceledStatus, parseStatus("canceled")) + assert.Equal(t, coreModels.CanceledStatus, parseStatus("skipped")) + assert.Equal(t, coreModels.QueuedStatus, parseStatus("created")) + assert.Equal(t, coreModels.ActionRequiredStatus, parseStatus("manual")) + assert.Equal(t, coreModels.UnknownStatus, parseStatus("")) +} + +func TestUsecase_getProject(t *testing.T) { + mockRepository := new(mocks.Repository) + mockRepository.On("GetProject", mock.AnythingOfType("int")). + Return(&models.Project{Repository: "TEST"}, nil) + + gu := initUsecase(mockRepository) + + project, err := gu.getProject(10) + assert.NoError(t, err) + assert.Equal(t, "TEST", project.Repository) + + project, err = gu.getProject(10) + assert.NoError(t, err) + assert.Equal(t, "TEST", project.Repository) + + mockRepository.AssertNumberOfCalls(t, "GetProject", 1) + mockRepository.AssertExpectations(t) +} diff --git a/monitorables/gitlab/config/config.go b/monitorables/gitlab/config/config.go new file mode 100644 index 00000000..2b3a9b0e --- /dev/null +++ b/monitorables/gitlab/config/config.go @@ -0,0 +1,15 @@ +package config + +type ( + Gitlab struct { + URL string `validate:"required,url,http"` + Token string `validate:"required"` + Timeout int `validate:"gte=0"` // In Millisecond + } +) + +var Default = &Gitlab{ + URL: "https://gitlab.com/", + Token: "", + Timeout: 5000, +} diff --git a/monitorables/gitlab/gitlab.go b/monitorables/gitlab/gitlab.go new file mode 100644 index 00000000..b2666363 --- /dev/null +++ b/monitorables/gitlab/gitlab.go @@ -0,0 +1,90 @@ +//+build !faker + +package gitlab + +import ( + "github.com/monitoror/monitoror/api/config/versions" + pkgMonitorable "github.com/monitoror/monitoror/internal/pkg/monitorable" + coreModels "github.com/monitoror/monitoror/models" + "github.com/monitoror/monitoror/monitorables/gitlab/api" + gitlabDelivery "github.com/monitoror/monitoror/monitorables/gitlab/api/delivery/http" + gitlabModels "github.com/monitoror/monitoror/monitorables/gitlab/api/models" + gitlabRepository "github.com/monitoror/monitoror/monitorables/gitlab/api/repository" + gitlabUsecase "github.com/monitoror/monitoror/monitorables/gitlab/api/usecase" + gitlabConfig "github.com/monitoror/monitoror/monitorables/gitlab/config" + "github.com/monitoror/monitoror/registry" + "github.com/monitoror/monitoror/store" +) + +type Monitorable struct { + store *store.Store + + config map[coreModels.VariantName]*gitlabConfig.Gitlab + + // Config tile settings + issuesTileEnabler registry.TileEnabler + pipelineTileEnabler registry.TileEnabler + mergeRequestTileEnabler registry.TileEnabler + mergeRequestGeneratorEnabler registry.GeneratorEnabler +} + +func NewMonitorable(store *store.Store) *Monitorable { + m := &Monitorable{} + m.store = store + m.config = make(map[coreModels.VariantName]*gitlabConfig.Gitlab) + + // Load core config from env + pkgMonitorable.LoadConfig(&m.config, gitlabConfig.Default) + + // Register Monitorable Tile in config manager + m.issuesTileEnabler = store.Registry.RegisterTile(api.GitlabIssuesTileType, versions.MinimalVersion, m.GetVariantsNames()) + m.pipelineTileEnabler = store.Registry.RegisterTile(api.GitlabPipelineTileType, versions.MinimalVersion, m.GetVariantsNames()) + m.mergeRequestTileEnabler = store.Registry.RegisterTile(api.GitlabMergeRequestTileType, versions.MinimalVersion, m.GetVariantsNames()) + m.mergeRequestGeneratorEnabler = store.Registry.RegisterGenerator(api.GitlabMergeRequestTileType, versions.MinimalVersion, m.GetVariantsNames()) + + return m +} + +func (m *Monitorable) GetDisplayName() string { + return "GitLab" +} + +func (m *Monitorable) GetVariantsNames() []coreModels.VariantName { + return pkgMonitorable.GetVariantsNames(m.config) +} + +func (m *Monitorable) Validate(variantName coreModels.VariantName) (bool, []error) { + conf := m.config[variantName] + + // No configuration set + if conf.URL == gitlabConfig.Default.URL && conf.Token == "" { + return false, nil + } + + // Validate Config + if errors := pkgMonitorable.ValidateConfig(conf, variantName); errors != nil { + return false, errors + } + + return true, nil +} + +func (m *Monitorable) Enable(variantName coreModels.VariantName) { + conf := m.config[variantName] + + repository := gitlabRepository.NewGitlabRepository(conf) + usecase := gitlabUsecase.NewGitlabUsecase(repository, m.store.CacheStore) + delivery := gitlabDelivery.NewGitlabDelivery(usecase) + + // EnableTile route to echo + routeGroup := m.store.MonitorableRouter.Group("/gitlab", variantName) + routeIssues := routeGroup.GET("/issues", delivery.GetIssues) + routePipeline := routeGroup.GET("/pipeline", delivery.GetPipeline) + routeMergeRequest := routeGroup.GET("/mergerequest", delivery.GetMergeRequest) + + // EnableTile data for config hydration + m.issuesTileEnabler.Enable(variantName, &gitlabModels.IssuesParams{}, routeIssues.Path) + m.pipelineTileEnabler.Enable(variantName, &gitlabModels.PipelineParams{}, routePipeline.Path) + m.mergeRequestTileEnabler.Enable(variantName, &gitlabModels.MergeRequestParams{}, routeMergeRequest.Path) + m.mergeRequestGeneratorEnabler.Enable(variantName, &gitlabModels.MergeRequestGeneratorParams{}, usecase.MergeRequestsGenerator) +} diff --git a/monitorables/gitlab/gitlab_faker.go b/monitorables/gitlab/gitlab_faker.go new file mode 100644 index 00000000..df07353e --- /dev/null +++ b/monitorables/gitlab/gitlab_faker.go @@ -0,0 +1,58 @@ +//+build faker + +package gitlab + +import ( + "github.com/monitoror/monitoror/api/config/versions" + "github.com/monitoror/monitoror/internal/pkg/monitorable" + coreModels "github.com/monitoror/monitoror/models" + "github.com/monitoror/monitoror/monitorables/gitlab/api" + gitlabDelivery "github.com/monitoror/monitoror/monitorables/gitlab/api/delivery/http" + gitlabModels "github.com/monitoror/monitoror/monitorables/gitlab/api/models" + gitlabUsecase "github.com/monitoror/monitoror/monitorables/gitlab/api/usecase" + "github.com/monitoror/monitoror/registry" + "github.com/monitoror/monitoror/store" +) + +type Monitorable struct { + monitorable.DefaultMonitorableFaker + + store *store.Store + + // Config tile settings + issuesTileEnabler registry.TileEnabler + pipelineTileEnabler registry.TileEnabler + mergeRequestTileEnabler registry.TileEnabler +} + +func NewMonitorable(store *store.Store) *Monitorable { + m := &Monitorable{} + m.store = store + + // Register Monitorable Tile in config manager + m.issuesTileEnabler = store.Registry.RegisterTile(api.GitlabIssuesTileType, versions.MinimalVersion, m.GetVariantsNames()) + m.pipelineTileEnabler = store.Registry.RegisterTile(api.GitlabPipelineTileType, versions.MinimalVersion, m.GetVariantsNames()) + m.mergeRequestTileEnabler = store.Registry.RegisterTile(api.GitlabMergeRequestTileType, versions.MinimalVersion, m.GetVariantsNames()) + + return m +} + +func (m *Monitorable) GetDisplayName() string { + return "GitLab" +} + +func (m *Monitorable) Enable(variantName coreModels.VariantName) { + usecase := gitlabUsecase.NewGitlabUsecase() + delivery := gitlabDelivery.NewGitlabDelivery(usecase) + + // EnableTile route to echo + routeGroup := m.store.MonitorableRouter.Group("/gitlab", variantName) + routeIssues := routeGroup.GET("/issues", delivery.GetIssues) + routePipeline := routeGroup.GET("/pipeline", delivery.GetPipeline) + routeMergeRequest := routeGroup.GET("/mergerequest", delivery.GetMergeRequest) + + // EnableTile data for config hydration + m.issuesTileEnabler.Enable(variantName, &gitlabModels.IssuesParams{}, routeIssues.Path) + m.pipelineTileEnabler.Enable(variantName, &gitlabModels.PipelineParams{}, routePipeline.Path) + m.mergeRequestTileEnabler.Enable(variantName, &gitlabModels.MergeRequestParams{}, routeMergeRequest.Path) +} diff --git a/monitorables/gitlab/gitlab_test.go b/monitorables/gitlab/gitlab_test.go new file mode 100644 index 00000000..5643d11f --- /dev/null +++ b/monitorables/gitlab/gitlab_test.go @@ -0,0 +1,49 @@ +package gitlab + +import ( + "os" + "testing" + + "github.com/monitoror/monitoror/internal/pkg/monitorable/test" + + "github.com/stretchr/testify/assert" +) + +func TestNewMonitorable(t *testing.T) { + // init Store + store, mockMonitorableHelper := test.InitMockAndStore() + + // init Env + // OK + _ = os.Setenv("MO_MONITORABLE_GITLAB_VARIANT0_TOKEN", "xxx") + // Missing Token + _ = os.Setenv("MO_MONITORABLE_GITLAB_VARIANT1_URL", "https://gitlab.example.com/") + // Url broken + _ = os.Setenv("MO_MONITORABLE_GITLAB_VARIANT2_URL", "url%sgitlab.example.com/") + + // NewMonitorable + monitorable := NewMonitorable(store) + assert.NotNil(t, monitorable) + + // GetDisplayName + assert.NotNil(t, monitorable.GetDisplayName()) + + // GetVariantsNames and check + if assert.Len(t, monitorable.GetVariantsNames(), 4) { + _, errors := monitorable.Validate("variant1") + assert.NotEmpty(t, errors) + _, errors = monitorable.Validate("variant2") + assert.NotEmpty(t, errors) + } + + // Enable + for _, variantName := range monitorable.GetVariantsNames() { + if valid, _ := monitorable.Validate(variantName); valid { + monitorable.Enable(variantName) + } + } + + // Test calls + mockMonitorableHelper.RouterAssertNumberOfCalls(t, 1, 3) + mockMonitorableHelper.TileSettingsManagerAssertNumberOfCalls(t, 3, 1, 3, 1) +} diff --git a/monitorables/monitorables.go b/monitorables/monitorables.go index 34358f8e..4338f449 100644 --- a/monitorables/monitorables.go +++ b/monitorables/monitorables.go @@ -3,6 +3,7 @@ package monitorables import ( "github.com/monitoror/monitoror/monitorables/azuredevops" "github.com/monitoror/monitoror/monitorables/github" + "github.com/monitoror/monitoror/monitorables/gitlab" "github.com/monitoror/monitoror/monitorables/http" "github.com/monitoror/monitoror/monitorables/jenkins" "github.com/monitoror/monitoror/monitorables/ping" @@ -17,6 +18,8 @@ func RegisterMonitorables(s *store.Store) { s.Registry.RegisterMonitorable(azuredevops.NewMonitorable(s)) // ------------ GITHUB ------------ s.Registry.RegisterMonitorable(github.NewMonitorable(s)) + // ------------ GITLAB ------------ + s.Registry.RegisterMonitorable(gitlab.NewMonitorable(s)) // ------------ HTTP ------------ s.Registry.RegisterMonitorable(http.NewMonitorable(s)) // ------------ JENKINS ------------ diff --git a/monitorables/monitorables_test.go b/monitorables/monitorables_test.go index 1070bdad..d3a79ef4 100644 --- a/monitorables/monitorables_test.go +++ b/monitorables/monitorables_test.go @@ -7,6 +7,7 @@ import ( coreModels "github.com/monitoror/monitoror/models" azureDevOpsApi "github.com/monitoror/monitoror/monitorables/azuredevops/api" githubApi "github.com/monitoror/monitoror/monitorables/github/api" + gitlabApi "github.com/monitoror/monitoror/monitorables/gitlab/api" httpApi "github.com/monitoror/monitoror/monitorables/http/api" jenkinsApi "github.com/monitoror/monitoror/monitorables/jenkins/api" pingApi "github.com/monitoror/monitoror/monitorables/ping/api" @@ -35,6 +36,10 @@ func TestManager_RegisterMonitorables(t *testing.T) { assert.NotNil(t, mr.TileMetadata[githubApi.GithubChecksTileType]) assert.NotNil(t, mr.TileMetadata[githubApi.GithubPullRequestTileType]) assert.NotNil(t, mr.GeneratorMetadata[coreModels.NewGeneratorTileType(githubApi.GithubPullRequestTileType)]) + // ------------ GITLAB ------------ + assert.NotNil(t, mr.TileMetadata[gitlabApi.GitlabPipelineTileType]) + assert.NotNil(t, mr.TileMetadata[gitlabApi.GitlabMergeRequestTileType]) + assert.NotNil(t, mr.GeneratorMetadata[coreModels.NewGeneratorTileType(gitlabApi.GitlabMergeRequestTileType)]) // ------------ HTTP ------------ assert.NotNil(t, mr.TileMetadata[httpApi.HTTPStatusTileType]) assert.NotNil(t, mr.TileMetadata[httpApi.HTTPRawTileType]) diff --git a/pkg/gogitlab/issues.go b/pkg/gogitlab/issues.go new file mode 100644 index 00000000..132d9bc3 --- /dev/null +++ b/pkg/gogitlab/issues.go @@ -0,0 +1,12 @@ +//go:generate mockery -name IssuesService + +package gogitlab + +import ( + "github.com/xanzy/go-gitlab" +) + +type IssuesService interface { + ListIssues(opt *gitlab.ListIssuesOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Issue, *gitlab.Response, error) + ListProjectIssues(pid interface{}, opt *gitlab.ListProjectIssuesOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Issue, *gitlab.Response, error) +} diff --git a/pkg/gogitlab/mergerequests.go b/pkg/gogitlab/mergerequests.go new file mode 100644 index 00000000..c0febf71 --- /dev/null +++ b/pkg/gogitlab/mergerequests.go @@ -0,0 +1,12 @@ +//go:generate mockery -name MergeRequestsService + +package gogitlab + +import ( + "github.com/xanzy/go-gitlab" +) + +type MergeRequestsService interface { + GetMergeRequest(pid interface{}, mergeRequest int, opt *gitlab.GetMergeRequestsOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) + ListProjectMergeRequests(pid interface{}, opt *gitlab.ListProjectMergeRequestsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.MergeRequest, *gitlab.Response, error) +} diff --git a/pkg/gogitlab/mocks/IssuesService.go b/pkg/gogitlab/mocks/IssuesService.go new file mode 100644 index 00000000..1d4832ce --- /dev/null +++ b/pkg/gogitlab/mocks/IssuesService.go @@ -0,0 +1,92 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import ( + gitlab "github.com/xanzy/go-gitlab" + + mock "github.com/stretchr/testify/mock" +) + +// IssuesService is an autogenerated mock type for the IssuesService type +type IssuesService struct { + mock.Mock +} + +// ListIssues provides a mock function with given fields: opt, options +func (_m *IssuesService) ListIssues(opt *gitlab.ListIssuesOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Issue, *gitlab.Response, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, opt) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 []*gitlab.Issue + if rf, ok := ret.Get(0).(func(*gitlab.ListIssuesOptions, ...gitlab.RequestOptionFunc) []*gitlab.Issue); ok { + r0 = rf(opt, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*gitlab.Issue) + } + } + + var r1 *gitlab.Response + if rf, ok := ret.Get(1).(func(*gitlab.ListIssuesOptions, ...gitlab.RequestOptionFunc) *gitlab.Response); ok { + r1 = rf(opt, options...) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*gitlab.Response) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(*gitlab.ListIssuesOptions, ...gitlab.RequestOptionFunc) error); ok { + r2 = rf(opt, options...) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// ListProjectIssues provides a mock function with given fields: pid, opt, options +func (_m *IssuesService) ListProjectIssues(pid interface{}, opt *gitlab.ListProjectIssuesOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Issue, *gitlab.Response, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, pid, opt) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 []*gitlab.Issue + if rf, ok := ret.Get(0).(func(interface{}, *gitlab.ListProjectIssuesOptions, ...gitlab.RequestOptionFunc) []*gitlab.Issue); ok { + r0 = rf(pid, opt, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*gitlab.Issue) + } + } + + var r1 *gitlab.Response + if rf, ok := ret.Get(1).(func(interface{}, *gitlab.ListProjectIssuesOptions, ...gitlab.RequestOptionFunc) *gitlab.Response); ok { + r1 = rf(pid, opt, options...) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*gitlab.Response) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(interface{}, *gitlab.ListProjectIssuesOptions, ...gitlab.RequestOptionFunc) error); ok { + r2 = rf(pid, opt, options...) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} diff --git a/pkg/gogitlab/mocks/MergeRequestsService.go b/pkg/gogitlab/mocks/MergeRequestsService.go new file mode 100644 index 00000000..a922b43c --- /dev/null +++ b/pkg/gogitlab/mocks/MergeRequestsService.go @@ -0,0 +1,92 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import ( + gitlab "github.com/xanzy/go-gitlab" + + mock "github.com/stretchr/testify/mock" +) + +// MergeRequestsService is an autogenerated mock type for the MergeRequestsService type +type MergeRequestsService struct { + mock.Mock +} + +// GetMergeRequest provides a mock function with given fields: pid, mergeRequest, opt, options +func (_m *MergeRequestsService) GetMergeRequest(pid interface{}, mergeRequest int, opt *gitlab.GetMergeRequestsOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, pid, mergeRequest, opt) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *gitlab.MergeRequest + if rf, ok := ret.Get(0).(func(interface{}, int, *gitlab.GetMergeRequestsOptions, ...gitlab.RequestOptionFunc) *gitlab.MergeRequest); ok { + r0 = rf(pid, mergeRequest, opt, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gitlab.MergeRequest) + } + } + + var r1 *gitlab.Response + if rf, ok := ret.Get(1).(func(interface{}, int, *gitlab.GetMergeRequestsOptions, ...gitlab.RequestOptionFunc) *gitlab.Response); ok { + r1 = rf(pid, mergeRequest, opt, options...) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*gitlab.Response) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(interface{}, int, *gitlab.GetMergeRequestsOptions, ...gitlab.RequestOptionFunc) error); ok { + r2 = rf(pid, mergeRequest, opt, options...) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// ListProjectMergeRequests provides a mock function with given fields: pid, opt, options +func (_m *MergeRequestsService) ListProjectMergeRequests(pid interface{}, opt *gitlab.ListProjectMergeRequestsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.MergeRequest, *gitlab.Response, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, pid, opt) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 []*gitlab.MergeRequest + if rf, ok := ret.Get(0).(func(interface{}, *gitlab.ListProjectMergeRequestsOptions, ...gitlab.RequestOptionFunc) []*gitlab.MergeRequest); ok { + r0 = rf(pid, opt, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*gitlab.MergeRequest) + } + } + + var r1 *gitlab.Response + if rf, ok := ret.Get(1).(func(interface{}, *gitlab.ListProjectMergeRequestsOptions, ...gitlab.RequestOptionFunc) *gitlab.Response); ok { + r1 = rf(pid, opt, options...) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*gitlab.Response) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(interface{}, *gitlab.ListProjectMergeRequestsOptions, ...gitlab.RequestOptionFunc) error); ok { + r2 = rf(pid, opt, options...) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} diff --git a/pkg/gogitlab/mocks/PipelinesService.go b/pkg/gogitlab/mocks/PipelinesService.go new file mode 100644 index 00000000..8c3ffe39 --- /dev/null +++ b/pkg/gogitlab/mocks/PipelinesService.go @@ -0,0 +1,92 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import ( + gitlab "github.com/xanzy/go-gitlab" + + mock "github.com/stretchr/testify/mock" +) + +// PipelinesService is an autogenerated mock type for the PipelinesService type +type PipelinesService struct { + mock.Mock +} + +// GetPipeline provides a mock function with given fields: pid, pipeline, options +func (_m *PipelinesService) GetPipeline(pid interface{}, pipeline int, options ...gitlab.RequestOptionFunc) (*gitlab.Pipeline, *gitlab.Response, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, pid, pipeline) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *gitlab.Pipeline + if rf, ok := ret.Get(0).(func(interface{}, int, ...gitlab.RequestOptionFunc) *gitlab.Pipeline); ok { + r0 = rf(pid, pipeline, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gitlab.Pipeline) + } + } + + var r1 *gitlab.Response + if rf, ok := ret.Get(1).(func(interface{}, int, ...gitlab.RequestOptionFunc) *gitlab.Response); ok { + r1 = rf(pid, pipeline, options...) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*gitlab.Response) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(interface{}, int, ...gitlab.RequestOptionFunc) error); ok { + r2 = rf(pid, pipeline, options...) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// ListProjectPipelines provides a mock function with given fields: pid, opt, options +func (_m *PipelinesService) ListProjectPipelines(pid interface{}, opt *gitlab.ListProjectPipelinesOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.PipelineInfo, *gitlab.Response, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, pid, opt) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 []*gitlab.PipelineInfo + if rf, ok := ret.Get(0).(func(interface{}, *gitlab.ListProjectPipelinesOptions, ...gitlab.RequestOptionFunc) []*gitlab.PipelineInfo); ok { + r0 = rf(pid, opt, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*gitlab.PipelineInfo) + } + } + + var r1 *gitlab.Response + if rf, ok := ret.Get(1).(func(interface{}, *gitlab.ListProjectPipelinesOptions, ...gitlab.RequestOptionFunc) *gitlab.Response); ok { + r1 = rf(pid, opt, options...) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*gitlab.Response) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(interface{}, *gitlab.ListProjectPipelinesOptions, ...gitlab.RequestOptionFunc) error); ok { + r2 = rf(pid, opt, options...) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} diff --git a/pkg/gogitlab/mocks/ProjectService.go b/pkg/gogitlab/mocks/ProjectService.go new file mode 100644 index 00000000..8031d479 --- /dev/null +++ b/pkg/gogitlab/mocks/ProjectService.go @@ -0,0 +1,53 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import ( + gitlab "github.com/xanzy/go-gitlab" + + mock "github.com/stretchr/testify/mock" +) + +// ProjectService is an autogenerated mock type for the ProjectService type +type ProjectService struct { + mock.Mock +} + +// GetProject provides a mock function with given fields: pid, opt, options +func (_m *ProjectService) GetProject(pid interface{}, opt *gitlab.GetProjectOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Project, *gitlab.Response, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, pid, opt) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *gitlab.Project + if rf, ok := ret.Get(0).(func(interface{}, *gitlab.GetProjectOptions, ...gitlab.RequestOptionFunc) *gitlab.Project); ok { + r0 = rf(pid, opt, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gitlab.Project) + } + } + + var r1 *gitlab.Response + if rf, ok := ret.Get(1).(func(interface{}, *gitlab.GetProjectOptions, ...gitlab.RequestOptionFunc) *gitlab.Response); ok { + r1 = rf(pid, opt, options...) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*gitlab.Response) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(interface{}, *gitlab.GetProjectOptions, ...gitlab.RequestOptionFunc) error); ok { + r2 = rf(pid, opt, options...) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} diff --git a/pkg/gogitlab/pipelines.go b/pkg/gogitlab/pipelines.go new file mode 100644 index 00000000..eb98ae59 --- /dev/null +++ b/pkg/gogitlab/pipelines.go @@ -0,0 +1,12 @@ +//go:generate mockery -name PipelinesService + +package gogitlab + +import ( + "github.com/xanzy/go-gitlab" +) + +type PipelinesService interface { + GetPipeline(pid interface{}, pipeline int, options ...gitlab.RequestOptionFunc) (*gitlab.Pipeline, *gitlab.Response, error) + ListProjectPipelines(pid interface{}, opt *gitlab.ListProjectPipelinesOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.PipelineInfo, *gitlab.Response, error) +} diff --git a/pkg/gogitlab/project.go b/pkg/gogitlab/project.go new file mode 100644 index 00000000..c5dcb2ee --- /dev/null +++ b/pkg/gogitlab/project.go @@ -0,0 +1,11 @@ +//go:generate mockery -name ProjectService + +package gogitlab + +import ( + "github.com/xanzy/go-gitlab" +) + +type ProjectService interface { + GetProject(pid interface{}, opt *gitlab.GetProjectOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Project, *gitlab.Response, error) +} diff --git a/ui/src/components/TileIcon.vue b/ui/src/components/TileIcon.vue index e5171996..e38cfc0e 100644 --- a/ui/src/components/TileIcon.vue +++ b/ui/src/components/TileIcon.vue @@ -105,7 +105,11 @@ } get isGitLab(): boolean { - return this.tileType === TileType.GitLab + return [ + TileType.GitLabPipeline, + TileType.GitLabMergeRequest, + TileType.GitLabIssues, + ].includes(this.tileType) } get isTravis(): boolean { diff --git a/ui/src/enums/tileType.ts b/ui/src/enums/tileType.ts index 58ff493e..e85657d3 100644 --- a/ui/src/enums/tileType.ts +++ b/ui/src/enums/tileType.ts @@ -8,7 +8,9 @@ export enum TileType { GitHubChecks = 'GITHUB-CHECKS', GitHubPullRequest = 'GITHUB-PULLREQUEST', GitHubCount = 'GITHUB-COUNT', - GitLab = 'GITLAB-BUILD', + GitLabPipeline = 'GITLAB-PIPELINE', + GitLabMergeRequest = 'GITLAB-MERGEREQUEST', + GitLabIssues = 'GITLAB-ISSUES', Travis = 'TRAVISCI-BUILD', Jenkins = 'JENKINS-BUILD', AzureDevOpsBuild = 'AZUREDEVOPS-BUILD',