fix: date fixes; go middleware refactors; regex fixes; etc (#368)
fix: format of date when MR was closed or merged (#367) refactor: Add Payload Validators + Middleware In Go Code (#366) fix: Add better checks for leaving comments (#369) fix: regex support for http credentials embedded in remote url (#372) fix: Comment on single line selects two lines (#371) This is a #PATCH release.
This commit is contained in:
committed by
GitHub
parent
f1faf603b0
commit
22bfd0c83e
@@ -17,14 +17,7 @@ type mergeRequestApproverService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* approveHandler approves a merge request. */
|
/* approveHandler approves a merge request. */
|
||||||
func (a mergeRequestApproverService) handler(w http.ResponseWriter, r *http.Request) {
|
func (a mergeRequestApproverService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", http.MethodPost)
|
|
||||||
handleError(w, InvalidRequestError{}, "Expected POST", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, res, err := a.client.ApproveMergeRequest(a.projectInfo.ProjectId, a.projectInfo.MergeId, nil, nil)
|
_, res, err := a.client.ApproveMergeRequest(a.projectInfo.ProjectId, a.projectInfo.MergeId, nil, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -33,15 +26,12 @@ func (a mergeRequestApproverService) handler(w http.ResponseWriter, r *http.Requ
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: "/mr/approve"}, "Could not approve merge request", res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, "Could not approve merge request", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response := SuccessResponse{
|
response := SuccessResponse{Message: "Approved MR"}
|
||||||
Message: "Approved MR",
|
|
||||||
Status: http.StatusOK,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err = json.NewEncoder(w).Encode(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -23,33 +23,36 @@ func TestApproveHandler(t *testing.T) {
|
|||||||
t.Run("Approves merge request", func(t *testing.T) {
|
t.Run("Approves merge request", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/approve", nil)
|
request := makeRequest(t, http.MethodPost, "/mr/approve", nil)
|
||||||
client := fakeApproverClient{}
|
client := fakeApproverClient{}
|
||||||
svc := mergeRequestApproverService{testProjectData, client}
|
svc := middleware(
|
||||||
|
mergeRequestApproverService{testProjectData, client},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
data := getSuccessData(t, svc, request)
|
data := getSuccessData(t, svc, request)
|
||||||
assert(t, data.Message, "Approved MR")
|
assert(t, data.Message, "Approved MR")
|
||||||
assert(t, data.Status, http.StatusOK)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Disallows non-POST method", func(t *testing.T) {
|
|
||||||
request := makeRequest(t, http.MethodGet, "/mr/approve", nil)
|
|
||||||
client := fakeApproverClient{}
|
|
||||||
svc := mergeRequestApproverService{testProjectData, client}
|
|
||||||
data := getFailData(t, svc, request)
|
|
||||||
checkBadMethod(t, data, http.MethodPost)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/approve", nil)
|
request := makeRequest(t, http.MethodPost, "/mr/approve", nil)
|
||||||
client := fakeApproverClient{testBase{errFromGitlab: true}}
|
client := fakeApproverClient{testBase{errFromGitlab: true}}
|
||||||
svc := mergeRequestApproverService{testProjectData, client}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
mergeRequestApproverService{testProjectData, client},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
checkErrorFromGitlab(t, data, "Could not approve merge request")
|
checkErrorFromGitlab(t, data, "Could not approve merge request")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/approve", nil)
|
request := makeRequest(t, http.MethodPost, "/mr/approve", nil)
|
||||||
client := fakeApproverClient{testBase{status: http.StatusSeeOther}}
|
client := fakeApproverClient{testBase{status: http.StatusSeeOther}}
|
||||||
svc := mergeRequestApproverService{testProjectData, client}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
mergeRequestApproverService{testProjectData, client},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
checkNon200(t, data, "Could not approve merge request", "/mr/approve")
|
checkNon200(t, data, "Could not approve merge request", "/mr/approve")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/xanzy/go-gitlab"
|
"github.com/xanzy/go-gitlab"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AssigneeUpdateRequest struct {
|
type AssigneeUpdateRequest struct {
|
||||||
Ids []int `json:"ids"`
|
Ids []int `json:"ids" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AssigneeUpdateResponse struct {
|
type AssigneeUpdateResponse struct {
|
||||||
@@ -17,37 +17,18 @@ type AssigneeUpdateResponse struct {
|
|||||||
Assignees []*gitlab.BasicUser `json:"assignees"`
|
Assignees []*gitlab.BasicUser `json:"assignees"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AssigneesRequestResponse struct {
|
|
||||||
SuccessResponse
|
|
||||||
Assignees []int `json:"assignees"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type assigneesService struct {
|
type assigneesService struct {
|
||||||
data
|
data
|
||||||
client MergeRequestUpdater
|
client MergeRequestUpdater
|
||||||
}
|
}
|
||||||
|
|
||||||
/* assigneesHandler adds or removes assignees from a merge request. */
|
/* assigneesHandler adds or removes assignees from a merge request. */
|
||||||
func (a assigneesService) handler(w http.ResponseWriter, r *http.Request) {
|
func (a assigneesService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
if r.Method != http.MethodPut {
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", http.MethodPut)
|
|
||||||
handleError(w, InvalidRequestError{}, "Expected PUT", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
assigneeUpdateRequest, ok := r.Context().Value(payload("payload")).(*AssigneeUpdateRequest)
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer r.Body.Close()
|
if !ok {
|
||||||
var assigneeUpdateRequest AssigneeUpdateRequest
|
handleError(w, errors.New("Could not get payload from context"), "Bad payload", http.StatusInternalServerError)
|
||||||
err = json.Unmarshal(body, &assigneeUpdateRequest)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,16 +42,13 @@ func (a assigneesService) handler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: "/mr/assignee"}, "Could not modify merge request assignees", res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, "Could not modify merge request assignees", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response := AssigneeUpdateResponse{
|
response := AssigneeUpdateResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{Message: "Assignees updated"},
|
||||||
Message: "Assignees updated",
|
|
||||||
Status: http.StatusOK,
|
|
||||||
},
|
|
||||||
Assignees: mr.Assignees,
|
Assignees: mr.Assignees,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,34 +24,39 @@ func TestAssigneeHandler(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("Updates assignees", func(t *testing.T) {
|
t.Run("Updates assignees", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPut, "/mr/assignee", updatePayload)
|
request := makeRequest(t, http.MethodPut, "/mr/assignee", updatePayload)
|
||||||
client := fakeAssigneeClient{}
|
svc := middleware(
|
||||||
svc := assigneesService{testProjectData, client}
|
assigneesService{testProjectData, fakeAssigneeClient{}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPut: &AssigneeUpdateRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPut),
|
||||||
|
)
|
||||||
data := getSuccessData(t, svc, request)
|
data := getSuccessData(t, svc, request)
|
||||||
assert(t, data.Message, "Assignees updated")
|
assert(t, data.Message, "Assignees updated")
|
||||||
assert(t, data.Status, http.StatusOK)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Disallows non-PUT method", func(t *testing.T) {
|
|
||||||
request := makeRequest(t, http.MethodGet, "/mr/assignee", nil)
|
|
||||||
client := fakeAssigneeClient{}
|
|
||||||
svc := assigneesService{testProjectData, client}
|
|
||||||
data := getFailData(t, svc, request)
|
|
||||||
checkBadMethod(t, data, http.MethodPut)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPut, "/mr/approve", updatePayload)
|
request := makeRequest(t, http.MethodPut, "/mr/assignee", updatePayload)
|
||||||
client := fakeAssigneeClient{testBase{errFromGitlab: true}}
|
client := fakeAssigneeClient{testBase{errFromGitlab: true}}
|
||||||
svc := assigneesService{testProjectData, client}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
assigneesService{testProjectData, client},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPut: &AssigneeUpdateRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPut),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
checkErrorFromGitlab(t, data, "Could not modify merge request assignees")
|
checkErrorFromGitlab(t, data, "Could not modify merge request assignees")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPut, "/mr/approve", updatePayload)
|
request := makeRequest(t, http.MethodPut, "/mr/assignee", updatePayload)
|
||||||
client := fakeAssigneeClient{testBase{status: http.StatusSeeOther}}
|
client := fakeAssigneeClient{testBase{status: http.StatusSeeOther}}
|
||||||
svc := assigneesService{testProjectData, client}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
assigneesService{testProjectData, client},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPut: &AssigneeUpdateRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPut),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
checkNon200(t, data, "Could not modify merge request assignees", "/mr/assignee")
|
checkNon200(t, data, "Could not modify merge request assignees", "/mr/assignee")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ type FileReader interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AttachmentRequest struct {
|
type AttachmentRequest struct {
|
||||||
FilePath string `json:"file_path"`
|
FilePath string `json:"file_path" validate:"required"`
|
||||||
FileName string `json:"file_name"`
|
FileName string `json:"file_name" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AttachmentResponse struct {
|
type AttachmentResponse struct {
|
||||||
@@ -58,52 +58,28 @@ type attachmentService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* attachmentHandler uploads an attachment (file, image, etc) to Gitlab and returns metadata about the upload. */
|
/* attachmentHandler uploads an attachment (file, image, etc) to Gitlab and returns metadata about the upload. */
|
||||||
func (a attachmentService) handler(w http.ResponseWriter, r *http.Request) {
|
func (a attachmentService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
payload := r.Context().Value(payload("payload")).(*AttachmentRequest)
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", http.MethodPost)
|
|
||||||
handleError(w, InvalidRequestError{}, "Expected POST", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var attachmentRequest AttachmentRequest
|
file, err := a.fileReader.ReadFile(payload.FilePath)
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
err = json.Unmarshal(body, &attachmentRequest)
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not unmarshal JSON", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := a.fileReader.ReadFile(attachmentRequest.FilePath)
|
|
||||||
if err != nil || file == nil {
|
if err != nil || file == nil {
|
||||||
handleError(w, err, fmt.Sprintf("Could not read %s file", attachmentRequest.FileName), http.StatusInternalServerError)
|
handleError(w, err, fmt.Sprintf("Could not read %s file", payload.FileName), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
projectFile, res, err := a.client.UploadFile(a.projectInfo.ProjectId, file, attachmentRequest.FileName)
|
projectFile, res, err := a.client.UploadFile(a.projectInfo.ProjectId, file, payload.FileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, err, fmt.Sprintf("Could not upload %s to Gitlab", attachmentRequest.FileName), http.StatusInternalServerError)
|
handleError(w, err, fmt.Sprintf("Could not upload %s to Gitlab", payload.FileName), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: "/attachment"}, fmt.Sprintf("Could not upload %s to Gitlab", attachmentRequest.FileName), res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, fmt.Sprintf("Could not upload %s to Gitlab", payload.FileName), res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response := AttachmentResponse{
|
response := AttachmentResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{Message: "File uploaded successfully"},
|
||||||
Status: http.StatusOK,
|
|
||||||
Message: "File uploaded successfully",
|
|
||||||
},
|
|
||||||
Markdown: projectFile.Markdown,
|
Markdown: projectFile.Markdown,
|
||||||
Alt: projectFile.Alt,
|
Alt: projectFile.Alt,
|
||||||
Url: projectFile.URL,
|
Url: projectFile.URL,
|
||||||
|
|||||||
@@ -36,29 +36,34 @@ func TestAttachmentHandler(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("Returns 200-status response after upload", func(t *testing.T) {
|
t.Run("Returns 200-status response after upload", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/attachment", attachmentTestRequestData)
|
request := makeRequest(t, http.MethodPost, "/attachment", attachmentTestRequestData)
|
||||||
svc := attachmentService{testProjectData, fakeFileReader{}, fakeFileUploaderClient{}}
|
svc := middleware(
|
||||||
|
attachmentService{testProjectData, fakeFileReader{}, fakeFileUploaderClient{}},
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &AttachmentRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
data := getSuccessData(t, svc, request)
|
data := getSuccessData(t, svc, request)
|
||||||
assert(t, data.Status, http.StatusOK)
|
|
||||||
assert(t, data.Message, "File uploaded successfully")
|
assert(t, data.Message, "File uploaded successfully")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Disallows non-POST method", func(t *testing.T) {
|
|
||||||
request := makeRequest(t, http.MethodGet, "/attachment", nil)
|
|
||||||
svc := attachmentService{testProjectData, fakeFileReader{}, fakeFileUploaderClient{}}
|
|
||||||
data := getFailData(t, svc, request)
|
|
||||||
checkBadMethod(t, data, http.MethodPost)
|
|
||||||
})
|
|
||||||
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/attachment", attachmentTestRequestData)
|
request := makeRequest(t, http.MethodPost, "/attachment", attachmentTestRequestData)
|
||||||
svc := attachmentService{testProjectData, fakeFileReader{}, fakeFileUploaderClient{testBase{errFromGitlab: true}}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
attachmentService{testProjectData, fakeFileReader{}, fakeFileUploaderClient{testBase{errFromGitlab: true}}},
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &AttachmentRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
checkErrorFromGitlab(t, data, "Could not upload some_file_name to Gitlab")
|
checkErrorFromGitlab(t, data, "Could not upload some_file_name to Gitlab")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/attachment", attachmentTestRequestData)
|
request := makeRequest(t, http.MethodPost, "/attachment", attachmentTestRequestData)
|
||||||
svc := attachmentService{testProjectData, fakeFileReader{}, fakeFileUploaderClient{testBase{status: http.StatusSeeOther}}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
attachmentService{testProjectData, fakeFileReader{}, fakeFileUploaderClient{testBase{status: http.StatusSeeOther}}},
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &AttachmentRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
checkNon200(t, data, "Could not upload some_file_name to Gitlab", "/attachment")
|
checkNon200(t, data, "Could not upload some_file_name to Gitlab", "/attachment")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/harrisoncramer/gitlab.nvim/cmd/app/git"
|
"github.com/harrisoncramer/gitlab.nvim/cmd/app/git"
|
||||||
"github.com/hashicorp/go-retryablehttp"
|
"github.com/hashicorp/go-retryablehttp"
|
||||||
@@ -48,12 +45,19 @@ func NewClient() (error, *Client) {
|
|||||||
gitlab.WithBaseURL(apiCustUrl),
|
gitlab.WithBaseURL(apiCustUrl),
|
||||||
}
|
}
|
||||||
|
|
||||||
if pluginOptions.Debug.Request {
|
if pluginOptions.Debug.GitlabRequest {
|
||||||
gitlabOptions = append(gitlabOptions, gitlab.WithRequestLogHook(requestLogger))
|
gitlabOptions = append(gitlabOptions, gitlab.WithRequestLogHook(
|
||||||
|
func(l retryablehttp.Logger, r *http.Request, i int) {
|
||||||
|
logRequest("REQUEST TO GITLAB", r)
|
||||||
|
},
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
if pluginOptions.Debug.Response {
|
if pluginOptions.Debug.GitlabResponse {
|
||||||
gitlabOptions = append(gitlabOptions, gitlab.WithResponseLogHook(responseLogger))
|
gitlabOptions = append(gitlabOptions, gitlab.WithResponseLogHook(func(l retryablehttp.Logger, response *http.Response) {
|
||||||
|
logResponse("RESPONSE FROM GITLAB", response)
|
||||||
|
},
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
tr := &http.Transport{
|
tr := &http.Transport{
|
||||||
@@ -106,7 +110,6 @@ func InitProjectSettings(c *Client, gitInfo git.GitData) (error, *ProjectInfo) {
|
|||||||
return nil, &ProjectInfo{
|
return nil, &ProjectInfo{
|
||||||
ProjectId: projectId,
|
ProjectId: projectId,
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* handleError is a utililty handler that returns errors to the client along with their statuses and messages */
|
/* handleError is a utililty handler that returns errors to the client along with their statuses and messages */
|
||||||
@@ -115,7 +118,6 @@ func handleError(w http.ResponseWriter, err error, message string, status int) {
|
|||||||
response := ErrorResponse{
|
response := ErrorResponse{
|
||||||
Message: message,
|
Message: message,
|
||||||
Details: err.Error(),
|
Details: err.Error(),
|
||||||
Status: status,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err = json.NewEncoder(w).Encode(response)
|
||||||
@@ -123,53 +125,3 @@ func handleError(w http.ResponseWriter, err error, message string, status int) {
|
|||||||
handleError(w, err, "Could not encode error response", http.StatusInternalServerError)
|
handleError(w, err, "Could not encode error response", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var requestLogger retryablehttp.RequestLogHook = func(l retryablehttp.Logger, r *http.Request, i int) {
|
|
||||||
file := openLogFile()
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
token := r.Header.Get("Private-Token")
|
|
||||||
r.Header.Set("Private-Token", "REDACTED")
|
|
||||||
res, err := httputil.DumpRequest(r, true)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error dumping request: %v", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
r.Header.Set("Private-Token", token)
|
|
||||||
|
|
||||||
_, err = file.Write([]byte("\n-- REQUEST --\n")) //nolint:all
|
|
||||||
_, err = file.Write(res) //nolint:all
|
|
||||||
_, err = file.Write([]byte("\n")) //nolint:all
|
|
||||||
}
|
|
||||||
|
|
||||||
var responseLogger retryablehttp.ResponseLogHook = func(l retryablehttp.Logger, response *http.Response) {
|
|
||||||
file := openLogFile()
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
res, err := httputil.DumpResponse(response, true)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error dumping response: %v", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = file.Write([]byte("\n-- RESPONSE --\n")) //nolint:all
|
|
||||||
_, err = file.Write(res) //nolint:all
|
|
||||||
_, err = file.Write([]byte("\n")) //nolint:all
|
|
||||||
}
|
|
||||||
|
|
||||||
func openLogFile() *os.File {
|
|
||||||
file, err := os.OpenFile(pluginOptions.LogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
log.Printf("Log file %s does not exist", pluginOptions.LogPath)
|
|
||||||
} else if os.IsPermission(err) {
|
|
||||||
log.Printf("Permission denied for log file %s", pluginOptions.LogPath)
|
|
||||||
} else {
|
|
||||||
log.Printf("Error opening log file %s: %v", pluginOptions.LogPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return file
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,45 +2,17 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/xanzy/go-gitlab"
|
"github.com/xanzy/go-gitlab"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PostCommentRequest struct {
|
|
||||||
Comment string `json:"comment"`
|
|
||||||
PositionData
|
|
||||||
}
|
|
||||||
|
|
||||||
type DeleteCommentRequest struct {
|
|
||||||
NoteId int `json:"note_id"`
|
|
||||||
DiscussionId string `json:"discussion_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type EditCommentRequest struct {
|
|
||||||
Comment string `json:"comment"`
|
|
||||||
NoteId int `json:"note_id"`
|
|
||||||
DiscussionId string `json:"discussion_id"`
|
|
||||||
Resolved bool `json:"resolved"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CommentResponse struct {
|
type CommentResponse struct {
|
||||||
SuccessResponse
|
SuccessResponse
|
||||||
Comment *gitlab.Note `json:"note"`
|
Comment *gitlab.Note `json:"note"`
|
||||||
Discussion *gitlab.Discussion `json:"discussion"`
|
Discussion *gitlab.Discussion `json:"discussion"`
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CommentWithPosition is a comment with an (optional) position data value embedded in it. The position data will be non-nil for range-based comments. */
|
|
||||||
type CommentWithPosition struct {
|
|
||||||
PositionData PositionData
|
|
||||||
}
|
|
||||||
|
|
||||||
func (comment CommentWithPosition) GetPositionData() PositionData {
|
|
||||||
return comment.PositionData
|
|
||||||
}
|
|
||||||
|
|
||||||
type CommentManager interface {
|
type CommentManager interface {
|
||||||
CreateMergeRequestDiscussion(pid interface{}, mergeRequest int, opt *gitlab.CreateMergeRequestDiscussionOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Discussion, *gitlab.Response, error)
|
CreateMergeRequestDiscussion(pid interface{}, mergeRequest int, opt *gitlab.CreateMergeRequestDiscussionOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Discussion, *gitlab.Response, error)
|
||||||
UpdateMergeRequestDiscussionNote(pid interface{}, mergeRequest int, discussion string, note int, opt *gitlab.UpdateMergeRequestDiscussionNoteOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Note, *gitlab.Response, error)
|
UpdateMergeRequestDiscussionNote(pid interface{}, mergeRequest int, discussion string, note int, opt *gitlab.UpdateMergeRequestDiscussionNoteOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Note, *gitlab.Response, error)
|
||||||
@@ -53,7 +25,7 @@ type commentService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* commentHandler creates, edits, and deletes discussions (comments, multi-line comments) */
|
/* commentHandler creates, edits, and deletes discussions (comments, multi-line comments) */
|
||||||
func (a commentService) handler(w http.ResponseWriter, r *http.Request) {
|
func (a commentService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
@@ -62,30 +34,19 @@ func (a commentService) handler(w http.ResponseWriter, r *http.Request) {
|
|||||||
a.editComment(w, r)
|
a.editComment(w, r)
|
||||||
case http.MethodDelete:
|
case http.MethodDelete:
|
||||||
a.deleteComment(w, r)
|
a.deleteComment(w, r)
|
||||||
default:
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", fmt.Sprintf("%s, %s, %s", http.MethodDelete, http.MethodPost, http.MethodPatch))
|
|
||||||
handleError(w, InvalidRequestError{}, "Expected DELETE, POST or PATCH", http.StatusMethodNotAllowed)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DeleteCommentRequest struct {
|
||||||
|
NoteId int `json:"note_id" validate:"required"`
|
||||||
|
DiscussionId string `json:"discussion_id" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
/* deleteComment deletes a note, multiline comment, or comment, which are all considered discussion notes. */
|
/* deleteComment deletes a note, multiline comment, or comment, which are all considered discussion notes. */
|
||||||
func (a commentService) deleteComment(w http.ResponseWriter, r *http.Request) {
|
func (a commentService) deleteComment(w http.ResponseWriter, r *http.Request) {
|
||||||
body, err := io.ReadAll(r.Body)
|
payload := r.Context().Value(payload("payload")).(*DeleteCommentRequest)
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer r.Body.Close()
|
res, err := a.client.DeleteMergeRequestDiscussionNote(a.projectInfo.ProjectId, a.projectInfo.MergeId, payload.DiscussionId, payload.NoteId)
|
||||||
|
|
||||||
var deleteCommentRequest DeleteCommentRequest
|
|
||||||
err = json.Unmarshal(body, &deleteCommentRequest)
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := a.client.DeleteMergeRequestDiscussionNote(a.projectInfo.ProjectId, a.projectInfo.MergeId, deleteCommentRequest.DiscussionId, deleteCommentRequest.NoteId)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, err, "Could not delete comment", http.StatusInternalServerError)
|
handleError(w, err, "Could not delete comment", http.StatusInternalServerError)
|
||||||
@@ -93,15 +54,12 @@ func (a commentService) deleteComment(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: "/mr/comment"}, "Could not delete comment", res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, "Could not delete comment", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response := SuccessResponse{
|
response := SuccessResponse{Message: "Comment deleted successfully"}
|
||||||
Message: "Comment deleted successfully",
|
|
||||||
Status: http.StatusOK,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err = json.NewEncoder(w).Encode(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -109,32 +67,33 @@ func (a commentService) deleteComment(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PostCommentRequest struct {
|
||||||
|
Comment string `json:"comment" validate:"required"`
|
||||||
|
PositionData
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CommentWithPosition is a comment with an (optional) position data value embedded in it. The position data will be non-nil for range-based comments. */
|
||||||
|
type CommentWithPosition struct {
|
||||||
|
PositionData PositionData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (comment CommentWithPosition) GetPositionData() PositionData {
|
||||||
|
return comment.PositionData
|
||||||
|
}
|
||||||
|
|
||||||
/* postComment creates a note, multiline comment, or comment. */
|
/* postComment creates a note, multiline comment, or comment. */
|
||||||
func (a commentService) postComment(w http.ResponseWriter, r *http.Request) {
|
func (a commentService) postComment(w http.ResponseWriter, r *http.Request) {
|
||||||
body, err := io.ReadAll(r.Body)
|
payload := r.Context().Value(payload("payload")).(*PostCommentRequest)
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
var postCommentRequest PostCommentRequest
|
|
||||||
err = json.Unmarshal(body, &postCommentRequest)
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not unmarshal data from request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
opt := gitlab.CreateMergeRequestDiscussionOptions{
|
opt := gitlab.CreateMergeRequestDiscussionOptions{
|
||||||
Body: &postCommentRequest.Comment,
|
Body: &payload.Comment,
|
||||||
}
|
}
|
||||||
|
|
||||||
/* If we are leaving a comment on a line, leave position. Otherwise,
|
/* If we are leaving a comment on a line, leave position. Otherwise,
|
||||||
we are leaving a note (unlinked comment) */
|
we are leaving a note (unlinked comment) */
|
||||||
|
|
||||||
if postCommentRequest.FileName != "" {
|
if payload.FileName != "" {
|
||||||
commentWithPositionData := CommentWithPosition{postCommentRequest.PositionData}
|
commentWithPositionData := CommentWithPosition{payload.PositionData}
|
||||||
opt.Position = buildCommentPosition(commentWithPositionData)
|
opt.Position = buildCommentPosition(commentWithPositionData)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,16 +105,13 @@ func (a commentService) postComment(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: "/mr/comment"}, "Could not create discussion", res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, "Could not create discussion", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response := CommentResponse{
|
response := CommentResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{Message: "Comment created successfully"},
|
||||||
Message: "Comment created successfully",
|
|
||||||
Status: http.StatusOK,
|
|
||||||
},
|
|
||||||
Comment: discussion.Notes[0],
|
Comment: discussion.Notes[0],
|
||||||
Discussion: discussion,
|
Discussion: discussion,
|
||||||
}
|
}
|
||||||
@@ -166,28 +122,23 @@ func (a commentService) postComment(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EditCommentRequest struct {
|
||||||
|
Comment string `json:"comment" validate:"required"`
|
||||||
|
NoteId int `json:"note_id" validate:"required"`
|
||||||
|
DiscussionId string `json:"discussion_id" validate:"required"`
|
||||||
|
Resolved bool `json:"resolved"`
|
||||||
|
}
|
||||||
|
|
||||||
/* editComment changes the text of a comment or changes it's resolved status. */
|
/* editComment changes the text of a comment or changes it's resolved status. */
|
||||||
func (a commentService) editComment(w http.ResponseWriter, r *http.Request) {
|
func (a commentService) editComment(w http.ResponseWriter, r *http.Request) {
|
||||||
body, err := io.ReadAll(r.Body)
|
|
||||||
|
|
||||||
if err != nil {
|
payload := r.Context().Value(payload("payload")).(*EditCommentRequest)
|
||||||
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
|
||||||
return
|
options := gitlab.UpdateMergeRequestDiscussionNoteOptions{
|
||||||
|
Body: gitlab.Ptr(payload.Comment),
|
||||||
}
|
}
|
||||||
|
|
||||||
defer r.Body.Close()
|
note, res, err := a.client.UpdateMergeRequestDiscussionNote(a.projectInfo.ProjectId, a.projectInfo.MergeId, payload.DiscussionId, payload.NoteId, &options)
|
||||||
|
|
||||||
var editCommentRequest EditCommentRequest
|
|
||||||
err = json.Unmarshal(body, &editCommentRequest)
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not unmarshal data from request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
options := gitlab.UpdateMergeRequestDiscussionNoteOptions{}
|
|
||||||
options.Body = gitlab.Ptr(editCommentRequest.Comment)
|
|
||||||
|
|
||||||
note, res, err := a.client.UpdateMergeRequestDiscussionNote(a.projectInfo.ProjectId, a.projectInfo.MergeId, editCommentRequest.DiscussionId, editCommentRequest.NoteId, &options)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, err, "Could not update comment", http.StatusInternalServerError)
|
handleError(w, err, "Could not update comment", http.StatusInternalServerError)
|
||||||
@@ -195,16 +146,13 @@ func (a commentService) editComment(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: "/mr/comment"}, "Could not update comment", res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, "Could not update comment", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response := CommentResponse{
|
response := CommentResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{Message: "Comment updated successfully"},
|
||||||
Message: "Comment updated successfully",
|
|
||||||
Status: http.StatusOK,
|
|
||||||
},
|
|
||||||
Comment: note,
|
Comment: note,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,10 +40,18 @@ func TestPostComment(t *testing.T) {
|
|||||||
var testCommentCreationData = PostCommentRequest{Comment: "Some comment"}
|
var testCommentCreationData = PostCommentRequest{Comment: "Some comment"}
|
||||||
t.Run("Creates a new note (unlinked comment)", func(t *testing.T) {
|
t.Run("Creates a new note (unlinked comment)", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/comment", testCommentCreationData)
|
request := makeRequest(t, http.MethodPost, "/mr/comment", testCommentCreationData)
|
||||||
svc := commentService{testProjectData, fakeCommentClient{}}
|
svc := middleware(
|
||||||
|
commentService{testProjectData, fakeCommentClient{}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{
|
||||||
|
http.MethodPost: &PostCommentRequest{},
|
||||||
|
http.MethodDelete: &DeleteCommentRequest{},
|
||||||
|
http.MethodPatch: &EditCommentRequest{},
|
||||||
|
}),
|
||||||
|
withMethodCheck(http.MethodPost, http.MethodDelete, http.MethodPatch),
|
||||||
|
)
|
||||||
data := getSuccessData(t, svc, request)
|
data := getSuccessData(t, svc, request)
|
||||||
assert(t, data.Message, "Comment created successfully")
|
assert(t, data.Message, "Comment created successfully")
|
||||||
assert(t, data.Status, http.StatusOK)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Creates a new comment", func(t *testing.T) {
|
t.Run("Creates a new comment", func(t *testing.T) {
|
||||||
@@ -54,23 +62,49 @@ func TestPostComment(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/comment", testCommentCreationData)
|
request := makeRequest(t, http.MethodPost, "/mr/comment", testCommentCreationData)
|
||||||
svc := commentService{testProjectData, fakeCommentClient{}}
|
svc := middleware(
|
||||||
|
commentService{testProjectData, fakeCommentClient{}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{
|
||||||
|
http.MethodPost: &PostCommentRequest{},
|
||||||
|
http.MethodDelete: &DeleteCommentRequest{},
|
||||||
|
http.MethodPatch: &EditCommentRequest{},
|
||||||
|
}),
|
||||||
|
withMethodCheck(http.MethodPost, http.MethodDelete, http.MethodPatch),
|
||||||
|
)
|
||||||
data := getSuccessData(t, svc, request)
|
data := getSuccessData(t, svc, request)
|
||||||
assert(t, data.Message, "Comment created successfully")
|
assert(t, data.Message, "Comment created successfully")
|
||||||
assert(t, data.Status, http.StatusOK)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/comment", testCommentCreationData)
|
request := makeRequest(t, http.MethodPost, "/mr/comment", testCommentCreationData)
|
||||||
svc := commentService{testProjectData, fakeCommentClient{testBase{errFromGitlab: true}}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
commentService{testProjectData, fakeCommentClient{testBase{errFromGitlab: true}}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{
|
||||||
|
http.MethodPost: &PostCommentRequest{},
|
||||||
|
http.MethodDelete: &DeleteCommentRequest{},
|
||||||
|
http.MethodPatch: &EditCommentRequest{},
|
||||||
|
}),
|
||||||
|
withMethodCheck(http.MethodPost, http.MethodDelete, http.MethodPatch),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
checkErrorFromGitlab(t, data, "Could not create discussion")
|
checkErrorFromGitlab(t, data, "Could not create discussion")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/comment", testCommentCreationData)
|
request := makeRequest(t, http.MethodPost, "/mr/comment", testCommentCreationData)
|
||||||
svc := commentService{testProjectData, fakeCommentClient{testBase{status: http.StatusSeeOther}}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
commentService{testProjectData, fakeCommentClient{testBase{status: http.StatusSeeOther}}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{
|
||||||
|
http.MethodPost: &PostCommentRequest{},
|
||||||
|
http.MethodDelete: &DeleteCommentRequest{},
|
||||||
|
http.MethodPatch: &EditCommentRequest{},
|
||||||
|
}),
|
||||||
|
withMethodCheck(http.MethodPost, http.MethodDelete, http.MethodPatch),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
checkNon200(t, data, "Could not create discussion", "/mr/comment")
|
checkNon200(t, data, "Could not create discussion", "/mr/comment")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -79,23 +113,18 @@ func TestDeleteComment(t *testing.T) {
|
|||||||
var testCommentDeletionData = DeleteCommentRequest{NoteId: 3, DiscussionId: "abc123"}
|
var testCommentDeletionData = DeleteCommentRequest{NoteId: 3, DiscussionId: "abc123"}
|
||||||
t.Run("Deletes a comment", func(t *testing.T) {
|
t.Run("Deletes a comment", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodDelete, "/mr/comment", testCommentDeletionData)
|
request := makeRequest(t, http.MethodDelete, "/mr/comment", testCommentDeletionData)
|
||||||
svc := commentService{testProjectData, fakeCommentClient{}}
|
svc := middleware(
|
||||||
|
commentService{testProjectData, fakeCommentClient{}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{
|
||||||
|
http.MethodPost: &PostCommentRequest{},
|
||||||
|
http.MethodDelete: &DeleteCommentRequest{},
|
||||||
|
http.MethodPatch: &EditCommentRequest{},
|
||||||
|
}),
|
||||||
|
withMethodCheck(http.MethodPost, http.MethodDelete, http.MethodPatch),
|
||||||
|
)
|
||||||
data := getSuccessData(t, svc, request)
|
data := getSuccessData(t, svc, request)
|
||||||
assert(t, data.Message, "Comment deleted successfully")
|
assert(t, data.Message, "Comment deleted successfully")
|
||||||
assert(t, data.Status, http.StatusOK)
|
|
||||||
})
|
|
||||||
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
|
||||||
request := makeRequest(t, http.MethodDelete, "/mr/comment", testCommentDeletionData)
|
|
||||||
svc := commentService{testProjectData, fakeCommentClient{testBase{errFromGitlab: true}}}
|
|
||||||
data := getFailData(t, svc, request)
|
|
||||||
checkErrorFromGitlab(t, data, "Could not delete comment")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
|
||||||
request := makeRequest(t, http.MethodDelete, "/mr/comment", testCommentDeletionData)
|
|
||||||
svc := commentService{testProjectData, fakeCommentClient{testBase{status: http.StatusSeeOther}}}
|
|
||||||
data := getFailData(t, svc, request)
|
|
||||||
checkNon200(t, data, "Could not delete comment", "/mr/comment")
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,22 +132,17 @@ func TestEditComment(t *testing.T) {
|
|||||||
var testEditCommentData = EditCommentRequest{Comment: "Some comment", NoteId: 3, DiscussionId: "abc123"}
|
var testEditCommentData = EditCommentRequest{Comment: "Some comment", NoteId: 3, DiscussionId: "abc123"}
|
||||||
t.Run("Edits a comment", func(t *testing.T) {
|
t.Run("Edits a comment", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPatch, "/mr/comment", testEditCommentData)
|
request := makeRequest(t, http.MethodPatch, "/mr/comment", testEditCommentData)
|
||||||
svc := commentService{testProjectData, fakeCommentClient{}}
|
svc := middleware(
|
||||||
|
commentService{testProjectData, fakeCommentClient{}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{
|
||||||
|
http.MethodPost: &PostCommentRequest{},
|
||||||
|
http.MethodDelete: &DeleteCommentRequest{},
|
||||||
|
http.MethodPatch: &EditCommentRequest{},
|
||||||
|
}),
|
||||||
|
withMethodCheck(http.MethodPost, http.MethodDelete, http.MethodPatch),
|
||||||
|
)
|
||||||
data := getSuccessData(t, svc, request)
|
data := getSuccessData(t, svc, request)
|
||||||
assert(t, data.Message, "Comment updated successfully")
|
assert(t, data.Message, "Comment updated successfully")
|
||||||
assert(t, data.Status, http.StatusOK)
|
|
||||||
})
|
|
||||||
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
|
||||||
request := makeRequest(t, http.MethodPatch, "/mr/comment", testEditCommentData)
|
|
||||||
svc := commentService{testProjectData, fakeCommentClient{testBase{errFromGitlab: true}}}
|
|
||||||
data := getFailData(t, svc, request)
|
|
||||||
checkErrorFromGitlab(t, data, "Could not update comment")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
|
||||||
request := makeRequest(t, http.MethodPatch, "/mr/comment", testEditCommentData)
|
|
||||||
svc := commentService{testProjectData, fakeCommentClient{testBase{status: http.StatusSeeOther}}}
|
|
||||||
data := getFailData(t, svc, request)
|
|
||||||
checkNon200(t, data, "Could not update comment", "/mr/comment")
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ type PluginOptions struct {
|
|||||||
AuthToken string `json:"auth_token"`
|
AuthToken string `json:"auth_token"`
|
||||||
LogPath string `json:"log_path"`
|
LogPath string `json:"log_path"`
|
||||||
Debug struct {
|
Debug struct {
|
||||||
Request bool `json:"go_request"`
|
Request bool `json:"request"`
|
||||||
Response bool `json:"go_response"`
|
Response bool `json:"response"`
|
||||||
|
GitlabRequest bool `json:"gitlab_request"`
|
||||||
|
GitlabResponse bool `json:"gitlab_response"`
|
||||||
} `json:"debug"`
|
} `json:"debug"`
|
||||||
ChosenTargetBranch *string `json:"chosen_target_branch,omitempty"`
|
ChosenTargetBranch *string `json:"chosen_target_branch,omitempty"`
|
||||||
ConnectionSettings struct {
|
ConnectionSettings struct {
|
||||||
|
|||||||
@@ -2,21 +2,19 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/xanzy/go-gitlab"
|
"github.com/xanzy/go-gitlab"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CreateMrRequest struct {
|
type CreateMrRequest struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title" validate:"required"`
|
||||||
|
TargetBranch string `json:"target_branch" validate:"required"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
TargetBranch string `json:"target_branch"`
|
TargetProjectID int `json:"forked_project_id,omitempty"`
|
||||||
DeleteBranch bool `json:"delete_branch"`
|
DeleteBranch bool `json:"delete_branch"`
|
||||||
Squash bool `json:"squash"`
|
Squash bool `json:"squash"`
|
||||||
TargetProjectID int `json:"forked_project_id,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type MergeRequestCreator interface {
|
type MergeRequestCreator interface {
|
||||||
@@ -29,36 +27,9 @@ type mergeRequestCreatorService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* createMr creates a merge request */
|
/* createMr creates a merge request */
|
||||||
func (a mergeRequestCreatorService) handler(w http.ResponseWriter, r *http.Request) {
|
func (a mergeRequestCreatorService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", http.MethodGet)
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
handleError(w, InvalidRequestError{}, "Expected POST", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
createMrRequest := r.Context().Value(payload("payload")).(*CreateMrRequest)
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var createMrRequest CreateMrRequest
|
|
||||||
err = json.Unmarshal(body, &createMrRequest)
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not unmarshal request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if createMrRequest.Title == "" {
|
|
||||||
handleError(w, errors.New("Title cannot be empty"), "Could not create MR", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if createMrRequest.TargetBranch == "" {
|
|
||||||
handleError(w, errors.New("Target branch cannot be empty"), "Could not create MR", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := gitlab.CreateMergeRequestOptions{
|
opts := gitlab.CreateMergeRequestOptions{
|
||||||
Title: &createMrRequest.Title,
|
Title: &createMrRequest.Title,
|
||||||
@@ -81,14 +52,11 @@ func (a mergeRequestCreatorService) handler(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: "/create_mr"}, "Could not create MR", res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, "Could not create MR", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response := SuccessResponse{
|
response := SuccessResponse{Message: fmt.Sprintf("MR '%s' created", createMrRequest.Title)}
|
||||||
Status: http.StatusOK,
|
|
||||||
Message: fmt.Sprintf("MR '%s' created", createMrRequest.Title),
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
|
|||||||
@@ -29,30 +29,34 @@ func TestCreateMr(t *testing.T) {
|
|||||||
}
|
}
|
||||||
t.Run("Creates an MR", func(t *testing.T) {
|
t.Run("Creates an MR", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/create_mr", testCreateMrRequestData)
|
request := makeRequest(t, http.MethodPost, "/create_mr", testCreateMrRequestData)
|
||||||
svc := mergeRequestCreatorService{testProjectData, fakeMergeCreatorClient{}}
|
svc := middleware(
|
||||||
|
mergeRequestCreatorService{testProjectData, fakeMergeCreatorClient{}},
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &CreateMrRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
data := getSuccessData(t, svc, request)
|
data := getSuccessData(t, svc, request)
|
||||||
assert(t, data.Message, "MR 'Some title' created")
|
assert(t, data.Message, "MR 'Some title' created")
|
||||||
assert(t, data.Status, http.StatusOK)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Disallows non-POST methods", func(t *testing.T) {
|
|
||||||
request := makeRequest(t, http.MethodGet, "/create_mr", testCreateMrRequestData)
|
|
||||||
svc := mergeRequestCreatorService{testProjectData, fakeMergeCreatorClient{}}
|
|
||||||
data := getFailData(t, svc, request)
|
|
||||||
checkBadMethod(t, data, http.MethodPost)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/create_mr", testCreateMrRequestData)
|
request := makeRequest(t, http.MethodPost, "/create_mr", testCreateMrRequestData)
|
||||||
svc := mergeRequestCreatorService{testProjectData, fakeMergeCreatorClient{testBase{errFromGitlab: true}}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
mergeRequestCreatorService{testProjectData, fakeMergeCreatorClient{testBase{errFromGitlab: true}}},
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &CreateMrRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
checkErrorFromGitlab(t, data, "Could not create MR")
|
checkErrorFromGitlab(t, data, "Could not create MR")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/create_mr", testCreateMrRequestData)
|
request := makeRequest(t, http.MethodPost, "/create_mr", testCreateMrRequestData)
|
||||||
svc := mergeRequestCreatorService{testProjectData, fakeMergeCreatorClient{testBase{status: http.StatusSeeOther}}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
mergeRequestCreatorService{testProjectData, fakeMergeCreatorClient{testBase{status: http.StatusSeeOther}}},
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &CreateMrRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
checkNon200(t, data, "Could not create MR", "/create_mr")
|
checkNon200(t, data, "Could not create MR", "/create_mr")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -60,21 +64,27 @@ func TestCreateMr(t *testing.T) {
|
|||||||
reqData := testCreateMrRequestData
|
reqData := testCreateMrRequestData
|
||||||
reqData.Title = ""
|
reqData.Title = ""
|
||||||
request := makeRequest(t, http.MethodPost, "/create_mr", reqData)
|
request := makeRequest(t, http.MethodPost, "/create_mr", reqData)
|
||||||
svc := mergeRequestCreatorService{testProjectData, fakeMergeCreatorClient{}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
mergeRequestCreatorService{testProjectData, fakeMergeCreatorClient{}},
|
||||||
assert(t, data.Status, http.StatusBadRequest)
|
withPayloadValidation(methodToPayload{http.MethodPost: &CreateMrRequest{}}),
|
||||||
assert(t, data.Message, "Could not create MR")
|
withMethodCheck(http.MethodPost),
|
||||||
assert(t, data.Details, "Title cannot be empty")
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
|
assert(t, data.Message, "Invalid payload")
|
||||||
|
assert(t, data.Details, "Title is required")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Handles missing target branch", func(t *testing.T) {
|
t.Run("Handles missing target branch", func(t *testing.T) {
|
||||||
reqData := testCreateMrRequestData
|
reqData := testCreateMrRequestData
|
||||||
reqData.TargetBranch = ""
|
reqData.TargetBranch = ""
|
||||||
request := makeRequest(t, http.MethodPost, "/create_mr", reqData)
|
request := makeRequest(t, http.MethodPost, "/create_mr", reqData)
|
||||||
svc := mergeRequestCreatorService{testProjectData, fakeMergeCreatorClient{}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
mergeRequestCreatorService{testProjectData, fakeMergeCreatorClient{}},
|
||||||
assert(t, data.Status, http.StatusBadRequest)
|
withPayloadValidation(methodToPayload{http.MethodPost: &CreateMrRequest{}}),
|
||||||
assert(t, data.Message, "Could not create MR")
|
withMethodCheck(http.MethodPost),
|
||||||
assert(t, data.Details, "Target branch cannot be empty")
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
|
assert(t, data.Message, "Invalid payload")
|
||||||
|
assert(t, data.Details, "TargetBranch is required")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/xanzy/go-gitlab"
|
"github.com/xanzy/go-gitlab"
|
||||||
@@ -19,38 +17,19 @@ type draftNotePublisherService struct {
|
|||||||
client DraftNotePublisher
|
client DraftNotePublisher
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a draftNotePublisherService) handler(w http.ResponseWriter, r *http.Request) {
|
type DraftNotePublishRequest struct {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
Note int `json:"note,omitempty"`
|
||||||
if r.Method != http.MethodPost {
|
}
|
||||||
w.Header().Set("Access-Control-Allow-Methods", http.MethodPost)
|
|
||||||
handleError(w, InvalidRequestError{}, "Expected POST", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
func (a draftNotePublisherService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
if err != nil {
|
payload := r.Context().Value(payload("payload")).(*DraftNotePublishRequest)
|
||||||
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer r.Body.Close()
|
|
||||||
var draftNotePublishRequest DraftNotePublishRequest
|
|
||||||
err = json.Unmarshal(body, &draftNotePublishRequest)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var res *gitlab.Response
|
var res *gitlab.Response
|
||||||
if draftNotePublishRequest.PublishAll {
|
var err error
|
||||||
res, err = a.client.PublishAllDraftNotes(a.projectInfo.ProjectId, a.projectInfo.MergeId)
|
if payload.Note != 0 {
|
||||||
|
res, err = a.client.PublishDraftNote(a.projectInfo.ProjectId, a.projectInfo.MergeId, payload.Note)
|
||||||
} else {
|
} else {
|
||||||
if draftNotePublishRequest.Note == 0 {
|
res, err = a.client.PublishAllDraftNotes(a.projectInfo.ProjectId, a.projectInfo.MergeId)
|
||||||
handleError(w, errors.New("No ID provided"), "Must provide Note ID", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
res, err = a.client.PublishDraftNote(a.projectInfo.ProjectId, a.projectInfo.MergeId, draftNotePublishRequest.Note)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -59,15 +38,12 @@ func (a draftNotePublisherService) handler(w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: "/mr/draft_notes/publish"}, "Could not publish dfaft note", res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, "Could not publish dfaft note", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response := SuccessResponse{
|
response := SuccessResponse{Message: "Draft note(s) published"}
|
||||||
Message: "Draft note(s) published",
|
|
||||||
Status: http.StatusOK,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err = json.NewEncoder(w).Encode(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -19,56 +19,53 @@ func (f fakeDraftNotePublisher) PublishDraftNote(pid interface{}, mergeRequest i
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPublishDraftNote(t *testing.T) {
|
func TestPublishDraftNote(t *testing.T) {
|
||||||
var testDraftNotePublishRequest = DraftNotePublishRequest{Note: 3, PublishAll: false}
|
var testDraftNotePublishRequest = DraftNotePublishRequest{Note: 3}
|
||||||
t.Run("Publishes draft note", func(t *testing.T) {
|
t.Run("Publishes draft note", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/draft_notes/publish", testDraftNotePublishRequest)
|
request := makeRequest(t, http.MethodPost, "/mr/draft_notes/publish", testDraftNotePublishRequest)
|
||||||
svc := draftNotePublisherService{testProjectData, fakeDraftNotePublisher{}}
|
svc := middleware(
|
||||||
|
draftNotePublisherService{testProjectData, fakeDraftNotePublisher{}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &DraftNotePublishRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
data := getSuccessData(t, svc, request)
|
data := getSuccessData(t, svc, request)
|
||||||
assert(t, data.Status, http.StatusOK)
|
|
||||||
assert(t, data.Message, "Draft note(s) published")
|
assert(t, data.Message, "Draft note(s) published")
|
||||||
})
|
})
|
||||||
t.Run("Disallows non-POST method", func(t *testing.T) {
|
|
||||||
request := makeRequest(t, http.MethodGet, "/mr/draft_notes/publish", testDraftNotePublishRequest)
|
|
||||||
svc := draftNotePublisherService{testProjectData, fakeDraftNotePublisher{}}
|
|
||||||
data := getFailData(t, svc, request)
|
|
||||||
checkBadMethod(t, data, http.MethodPost)
|
|
||||||
})
|
|
||||||
t.Run("Handles bad ID", func(t *testing.T) {
|
|
||||||
badData := testDraftNotePublishRequest
|
|
||||||
badData.Note = 0
|
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/draft_notes/publish", badData)
|
|
||||||
svc := draftNotePublisherService{testProjectData, fakeDraftNotePublisher{}}
|
|
||||||
data := getFailData(t, svc, request)
|
|
||||||
assert(t, data.Status, http.StatusBadRequest)
|
|
||||||
assert(t, data.Message, "Must provide Note ID")
|
|
||||||
})
|
|
||||||
t.Run("Handles error from Gitlab", func(t *testing.T) {
|
t.Run("Handles error from Gitlab", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/draft_notes/publish", testDraftNotePublishRequest)
|
request := makeRequest(t, http.MethodPost, "/mr/draft_notes/publish", testDraftNotePublishRequest)
|
||||||
svc := draftNotePublisherService{testProjectData, fakeDraftNotePublisher{testBase{errFromGitlab: true}}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
draftNotePublisherService{testProjectData, fakeDraftNotePublisher{testBase: testBase{errFromGitlab: true}}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &DraftNotePublishRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
checkErrorFromGitlab(t, data, "Could not publish draft note(s)")
|
checkErrorFromGitlab(t, data, "Could not publish draft note(s)")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPublishAllDraftNotes(t *testing.T) {
|
func TestPublishAllDraftNotes(t *testing.T) {
|
||||||
var testDraftNotePublishRequest = DraftNotePublishRequest{PublishAll: true}
|
var testDraftNotePublishRequest = DraftNotePublishRequest{}
|
||||||
t.Run("Should publish all draft notes", func(t *testing.T) {
|
t.Run("Should publish all draft notes", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/draft_notes/publish", testDraftNotePublishRequest)
|
request := makeRequest(t, http.MethodPost, "/mr/draft_notes/publish", testDraftNotePublishRequest)
|
||||||
svc := draftNotePublisherService{testProjectData, fakeDraftNotePublisher{}}
|
svc := middleware(
|
||||||
|
draftNotePublisherService{testProjectData, fakeDraftNotePublisher{}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &DraftNotePublishRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
data := getSuccessData(t, svc, request)
|
data := getSuccessData(t, svc, request)
|
||||||
assert(t, data.Status, http.StatusOK)
|
|
||||||
assert(t, data.Message, "Draft note(s) published")
|
assert(t, data.Message, "Draft note(s) published")
|
||||||
})
|
})
|
||||||
t.Run("Disallows non-POST method", func(t *testing.T) {
|
|
||||||
request := makeRequest(t, http.MethodGet, "/mr/draft_notes/publish", testDraftNotePublishRequest)
|
|
||||||
svc := draftNotePublisherService{testProjectData, fakeDraftNotePublisher{}}
|
|
||||||
data := getFailData(t, svc, request)
|
|
||||||
checkBadMethod(t, data, http.MethodPost)
|
|
||||||
})
|
|
||||||
t.Run("Handles error from Gitlab", func(t *testing.T) {
|
t.Run("Handles error from Gitlab", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/draft_notes/publish", testDraftNotePublishRequest)
|
request := makeRequest(t, http.MethodPost, "/mr/draft_notes/publish", testDraftNotePublishRequest)
|
||||||
svc := draftNotePublisherService{testProjectData, fakeDraftNotePublisher{testBase{errFromGitlab: true}}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
draftNotePublisherService{testProjectData, fakeDraftNotePublisher{testBase: testBase{errFromGitlab: true}}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &DraftNotePublishRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
checkErrorFromGitlab(t, data, "Could not publish draft note(s)")
|
checkErrorFromGitlab(t, data, "Could not publish draft note(s)")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ package app
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -12,36 +10,15 @@ import (
|
|||||||
"github.com/xanzy/go-gitlab"
|
"github.com/xanzy/go-gitlab"
|
||||||
)
|
)
|
||||||
|
|
||||||
/* The data coming from the client when creating a draft note is the same,
|
/* The data coming from the client when creating a draft note is the same
|
||||||
as when they are creating a normal comment, but the Gitlab
|
as when they are creating a normal comment, but the Gitlab
|
||||||
endpoints + resources we handle are different */
|
endpoints + resources we handle are different */
|
||||||
|
|
||||||
type PostDraftNoteRequest struct {
|
|
||||||
Comment string `json:"comment"`
|
|
||||||
DiscussionId string `json:"discussion_id,omitempty"`
|
|
||||||
PositionData
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateDraftNoteRequest struct {
|
|
||||||
Note string `json:"note"`
|
|
||||||
Position gitlab.PositionOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
type DraftNotePublishRequest struct {
|
|
||||||
Note int `json:"note,omitempty"`
|
|
||||||
PublishAll bool `json:"publish_all"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DraftNoteResponse struct {
|
type DraftNoteResponse struct {
|
||||||
SuccessResponse
|
SuccessResponse
|
||||||
DraftNote *gitlab.DraftNote `json:"draft_note"`
|
DraftNote *gitlab.DraftNote `json:"draft_note"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListDraftNotesResponse struct {
|
|
||||||
SuccessResponse
|
|
||||||
DraftNotes []*gitlab.DraftNote `json:"draft_notes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
/* DraftNoteWithPosition is a draft comment with an (optional) position data value embedded in it. The position data will be non-nil for range-based draft comments. */
|
/* DraftNoteWithPosition is a draft comment with an (optional) position data value embedded in it. The position data will be non-nil for range-based draft comments. */
|
||||||
type DraftNoteWithPosition struct {
|
type DraftNoteWithPosition struct {
|
||||||
PositionData PositionData
|
PositionData PositionData
|
||||||
@@ -64,7 +41,7 @@ type draftNoteService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* draftNoteHandler creates, edits, and deletes draft notes */
|
/* draftNoteHandler creates, edits, and deletes draft notes */
|
||||||
func (a draftNoteService) handler(w http.ResponseWriter, r *http.Request) {
|
func (a draftNoteService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
@@ -75,14 +52,16 @@ func (a draftNoteService) handler(w http.ResponseWriter, r *http.Request) {
|
|||||||
a.updateDraftNote(w, r)
|
a.updateDraftNote(w, r)
|
||||||
case http.MethodDelete:
|
case http.MethodDelete:
|
||||||
a.deleteDraftNote(w, r)
|
a.deleteDraftNote(w, r)
|
||||||
default:
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", fmt.Sprintf("%s, %s, %s, %s", http.MethodDelete, http.MethodPost, http.MethodPatch, http.MethodGet))
|
|
||||||
handleError(w, InvalidRequestError{}, "Expected DELETE, GET, POST or PATCH", http.StatusMethodNotAllowed)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ListDraftNotesResponse struct {
|
||||||
|
SuccessResponse
|
||||||
|
DraftNotes []*gitlab.DraftNote `json:"draft_notes"`
|
||||||
|
}
|
||||||
|
|
||||||
/* listDraftNotes lists all draft notes for the currently authenticated user */
|
/* listDraftNotes lists all draft notes for the currently authenticated user */
|
||||||
func (a draftNoteService) listDraftNotes(w http.ResponseWriter, _ *http.Request) {
|
func (a draftNoteService) listDraftNotes(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
opt := gitlab.ListDraftNotesOptions{}
|
opt := gitlab.ListDraftNotesOptions{}
|
||||||
draftNotes, res, err := a.client.ListDraftNotes(a.projectInfo.ProjectId, a.projectInfo.MergeId, &opt)
|
draftNotes, res, err := a.client.ListDraftNotes(a.projectInfo.ProjectId, a.projectInfo.MergeId, &opt)
|
||||||
@@ -93,16 +72,13 @@ func (a draftNoteService) listDraftNotes(w http.ResponseWriter, _ *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: "/mr/draft_notes/"}, "Could not get draft notes", res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, "Could not get draft notes", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response := ListDraftNotesResponse{
|
response := ListDraftNotesResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{Message: "Draft notes fetched successfully"},
|
||||||
Message: "Draft notes fetched successfully",
|
|
||||||
Status: http.StatusOK,
|
|
||||||
},
|
|
||||||
DraftNotes: draftNotes,
|
DraftNotes: draftNotes,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,34 +88,27 @@ func (a draftNoteService) listDraftNotes(w http.ResponseWriter, _ *http.Request)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PostDraftNoteRequest struct {
|
||||||
|
Comment string `json:"comment" validate:"required"`
|
||||||
|
DiscussionId string `json:"discussion_id,omitempty" validate:"required"`
|
||||||
|
PositionData // TODO: How to add validations to data from external package???
|
||||||
|
}
|
||||||
|
|
||||||
/* postDraftNote creates a draft note */
|
/* postDraftNote creates a draft note */
|
||||||
func (a draftNoteService) postDraftNote(w http.ResponseWriter, r *http.Request) {
|
func (a draftNoteService) postDraftNote(w http.ResponseWriter, r *http.Request) {
|
||||||
body, err := io.ReadAll(r.Body)
|
payload := r.Context().Value(payload("payload")).(*PostDraftNoteRequest)
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
var postDraftNoteRequest PostDraftNoteRequest
|
|
||||||
err = json.Unmarshal(body, &postDraftNoteRequest)
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not unmarshal data from request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
opt := gitlab.CreateDraftNoteOptions{
|
opt := gitlab.CreateDraftNoteOptions{
|
||||||
Note: &postDraftNoteRequest.Comment,
|
Note: &payload.Comment,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draft notes can be posted in "response" to existing discussions
|
// Draft notes can be posted in "response" to existing discussions
|
||||||
if postDraftNoteRequest.DiscussionId != "" {
|
if payload.DiscussionId != "" {
|
||||||
opt.InReplyToDiscussionID = gitlab.Ptr(postDraftNoteRequest.DiscussionId)
|
opt.InReplyToDiscussionID = gitlab.Ptr(payload.DiscussionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
if postDraftNoteRequest.FileName != "" {
|
if payload.FileName != "" {
|
||||||
draftNoteWithPosition := DraftNoteWithPosition{postDraftNoteRequest.PositionData}
|
draftNoteWithPosition := DraftNoteWithPosition{payload.PositionData}
|
||||||
opt.Position = buildCommentPosition(draftNoteWithPosition)
|
opt.Position = buildCommentPosition(draftNoteWithPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,16 +120,13 @@ func (a draftNoteService) postDraftNote(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: "/mr/draft_notes/"}, "Could not create draft note", res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, "Could not create draft note", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response := DraftNoteResponse{
|
response := DraftNoteResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{Message: "Draft note created successfully"},
|
||||||
Message: "Draft note created successfully",
|
|
||||||
Status: http.StatusOK,
|
|
||||||
},
|
|
||||||
DraftNote: draftNote,
|
DraftNote: draftNote,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,15 +153,12 @@ func (a draftNoteService) deleteDraftNote(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: fmt.Sprintf("/mr/draft_notes/%d", id)}, "Could not delete draft note", res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, "Could not delete draft note", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response := SuccessResponse{
|
response := SuccessResponse{Message: "Draft note deleted"}
|
||||||
Message: "Draft note deleted",
|
|
||||||
Status: http.StatusOK,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err = json.NewEncoder(w).Encode(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -203,6 +166,11 @@ func (a draftNoteService) deleteDraftNote(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UpdateDraftNoteRequest struct {
|
||||||
|
Note string `json:"note" validate:"required"`
|
||||||
|
Position gitlab.PositionOptions
|
||||||
|
}
|
||||||
|
|
||||||
/* updateDraftNote edits the text of a draft comment */
|
/* updateDraftNote edits the text of a draft comment */
|
||||||
func (a draftNoteService) updateDraftNote(w http.ResponseWriter, r *http.Request) {
|
func (a draftNoteService) updateDraftNote(w http.ResponseWriter, r *http.Request) {
|
||||||
suffix := strings.TrimPrefix(r.URL.Path, "/mr/draft_notes/")
|
suffix := strings.TrimPrefix(r.URL.Path, "/mr/draft_notes/")
|
||||||
@@ -212,29 +180,16 @@ func (a draftNoteService) updateDraftNote(w http.ResponseWriter, r *http.Request
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
payload := r.Context().Value(payload("payload")).(*UpdateDraftNoteRequest)
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer r.Body.Close()
|
if payload.Note == "" {
|
||||||
|
|
||||||
var updateDraftNoteRequest UpdateDraftNoteRequest
|
|
||||||
err = json.Unmarshal(body, &updateDraftNoteRequest)
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not unmarshal data from request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if updateDraftNoteRequest.Note == "" {
|
|
||||||
handleError(w, errors.New("Draft note text missing"), "Must provide draft note text", http.StatusBadRequest)
|
handleError(w, errors.New("Draft note text missing"), "Must provide draft note text", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
opt := gitlab.UpdateDraftNoteOptions{
|
opt := gitlab.UpdateDraftNoteOptions{
|
||||||
Note: &updateDraftNoteRequest.Note,
|
Note: &payload.Note,
|
||||||
Position: &updateDraftNoteRequest.Position,
|
Position: &payload.Position,
|
||||||
}
|
}
|
||||||
|
|
||||||
draftNote, res, err := a.client.UpdateDraftNote(a.projectInfo.ProjectId, a.projectInfo.MergeId, id, &opt)
|
draftNote, res, err := a.client.UpdateDraftNote(a.projectInfo.ProjectId, a.projectInfo.MergeId, id, &opt)
|
||||||
@@ -245,16 +200,13 @@ func (a draftNoteService) updateDraftNote(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: fmt.Sprintf("/mr/draft_notes/%d", id)}, "Could not update draft note", res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, "Could not update draft note", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response := DraftNoteResponse{
|
response := DraftNoteResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{Message: "Draft note updated"},
|
||||||
Message: "Draft note updated",
|
|
||||||
Status: http.StatusOK,
|
|
||||||
},
|
|
||||||
DraftNote: draftNote,
|
DraftNote: draftNote,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,74 +42,99 @@ func (f fakeDraftNoteManager) UpdateDraftNote(pid interface{}, mergeRequest int,
|
|||||||
func TestListDraftNotes(t *testing.T) {
|
func TestListDraftNotes(t *testing.T) {
|
||||||
t.Run("Lists all draft notes", func(t *testing.T) {
|
t.Run("Lists all draft notes", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodGet, "/mr/draft_notes/", nil)
|
request := makeRequest(t, http.MethodGet, "/mr/draft_notes/", nil)
|
||||||
svc := draftNoteService{testProjectData, fakeDraftNoteManager{}}
|
svc := middleware(
|
||||||
|
draftNoteService{testProjectData, fakeDraftNoteManager{}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{
|
||||||
|
http.MethodPost: &PostDraftNoteRequest{},
|
||||||
|
http.MethodPatch: &UpdateDraftNoteRequest{},
|
||||||
|
}),
|
||||||
|
withMethodCheck(http.MethodGet, http.MethodPost, http.MethodPatch, http.MethodDelete),
|
||||||
|
)
|
||||||
|
|
||||||
data := getSuccessData(t, svc, request)
|
data := getSuccessData(t, svc, request)
|
||||||
assert(t, data.Status, http.StatusOK)
|
|
||||||
assert(t, data.Message, "Draft notes fetched successfully")
|
assert(t, data.Message, "Draft notes fetched successfully")
|
||||||
})
|
})
|
||||||
t.Run("Handles error from Gitlab client", func(t *testing.T) {
|
t.Run("Handles error from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodGet, "/mr/draft_notes/", nil)
|
request := makeRequest(t, http.MethodGet, "/mr/draft_notes/", nil)
|
||||||
svc := draftNoteService{testProjectData, fakeDraftNoteManager{testBase{errFromGitlab: true}}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
draftNoteService{testProjectData, fakeDraftNoteManager{testBase: testBase{errFromGitlab: true}}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{
|
||||||
|
http.MethodPost: &PostDraftNoteRequest{},
|
||||||
|
http.MethodPatch: &UpdateDraftNoteRequest{},
|
||||||
|
}),
|
||||||
|
withMethodCheck(http.MethodGet, http.MethodPost, http.MethodPatch, http.MethodDelete),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
checkErrorFromGitlab(t, data, "Could not get draft notes")
|
checkErrorFromGitlab(t, data, "Could not get draft notes")
|
||||||
})
|
})
|
||||||
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodGet, "/mr/draft_notes/", nil)
|
request := makeRequest(t, http.MethodGet, "/mr/draft_notes/", nil)
|
||||||
svc := draftNoteService{testProjectData, fakeDraftNoteManager{testBase{status: http.StatusSeeOther}}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
draftNoteService{testProjectData, fakeDraftNoteManager{testBase: testBase{status: http.StatusSeeOther}}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{
|
||||||
|
http.MethodPost: &PostDraftNoteRequest{},
|
||||||
|
http.MethodPatch: &UpdateDraftNoteRequest{},
|
||||||
|
}),
|
||||||
|
withMethodCheck(http.MethodGet, http.MethodPost, http.MethodPatch, http.MethodDelete),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
checkNon200(t, data, "Could not get draft notes", "/mr/draft_notes/")
|
checkNon200(t, data, "Could not get draft notes", "/mr/draft_notes/")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPostDraftNote(t *testing.T) {
|
func TestPostDraftNote(t *testing.T) {
|
||||||
var testPostDraftNoteRequestData = PostDraftNoteRequest{Comment: "Some comment"}
|
var testPostDraftNoteRequestData = PostDraftNoteRequest{
|
||||||
|
Comment: "Some comment",
|
||||||
|
DiscussionId: "abc123",
|
||||||
|
}
|
||||||
t.Run("Posts new draft note", func(t *testing.T) {
|
t.Run("Posts new draft note", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/draft_notes/", testPostDraftNoteRequestData)
|
request := makeRequest(t, http.MethodPost, "/mr/draft_notes/", testPostDraftNoteRequestData)
|
||||||
svc := draftNoteService{testProjectData, fakeDraftNoteManager{}}
|
svc := middleware(
|
||||||
|
draftNoteService{testProjectData, fakeDraftNoteManager{}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{
|
||||||
|
http.MethodPost: &PostDraftNoteRequest{},
|
||||||
|
http.MethodPatch: &UpdateDraftNoteRequest{},
|
||||||
|
}),
|
||||||
|
withMethodCheck(http.MethodGet, http.MethodPost, http.MethodPatch, http.MethodDelete),
|
||||||
|
)
|
||||||
data := getSuccessData(t, svc, request)
|
data := getSuccessData(t, svc, request)
|
||||||
assert(t, data.Status, http.StatusOK)
|
|
||||||
assert(t, data.Message, "Draft note created successfully")
|
assert(t, data.Message, "Draft note created successfully")
|
||||||
})
|
})
|
||||||
t.Run("Handles error from Gitlab client", func(t *testing.T) {
|
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/draft_notes/", testPostDraftNoteRequestData)
|
|
||||||
svc := draftNoteService{testProjectData, fakeDraftNoteManager{testBase{errFromGitlab: true}}}
|
|
||||||
data := getFailData(t, svc, request)
|
|
||||||
checkErrorFromGitlab(t, data, "Could not create draft note")
|
|
||||||
})
|
|
||||||
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/draft_notes/", testPostDraftNoteRequestData)
|
|
||||||
svc := draftNoteService{testProjectData, fakeDraftNoteManager{testBase{status: http.StatusSeeOther}}}
|
|
||||||
data := getFailData(t, svc, request)
|
|
||||||
checkNon200(t, data, "Could not create draft note", "/mr/draft_notes/")
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDeleteDraftNote(t *testing.T) {
|
func TestDeleteDraftNote(t *testing.T) {
|
||||||
t.Run("Deletes new draft note", func(t *testing.T) {
|
t.Run("Deletes new draft note", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodDelete, "/mr/draft_notes/3", nil)
|
request := makeRequest(t, http.MethodDelete, "/mr/draft_notes/3", nil)
|
||||||
svc := draftNoteService{testProjectData, fakeDraftNoteManager{}}
|
svc := middleware(
|
||||||
|
draftNoteService{testProjectData, fakeDraftNoteManager{}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{
|
||||||
|
http.MethodPost: &PostDraftNoteRequest{},
|
||||||
|
http.MethodPatch: &UpdateDraftNoteRequest{},
|
||||||
|
}),
|
||||||
|
withMethodCheck(http.MethodGet, http.MethodPost, http.MethodPatch, http.MethodDelete),
|
||||||
|
)
|
||||||
data := getSuccessData(t, svc, request)
|
data := getSuccessData(t, svc, request)
|
||||||
assert(t, data.Status, http.StatusOK)
|
|
||||||
assert(t, data.Message, "Draft note deleted")
|
assert(t, data.Message, "Draft note deleted")
|
||||||
})
|
})
|
||||||
t.Run("Handles error from Gitlab client", func(t *testing.T) {
|
|
||||||
request := makeRequest(t, http.MethodDelete, "/mr/draft_notes/3", nil)
|
|
||||||
svc := draftNoteService{testProjectData, fakeDraftNoteManager{testBase{errFromGitlab: true}}}
|
|
||||||
data := getFailData(t, svc, request)
|
|
||||||
checkErrorFromGitlab(t, data, "Could not delete draft note")
|
|
||||||
})
|
|
||||||
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
|
||||||
request := makeRequest(t, http.MethodDelete, "/mr/draft_notes/3", nil)
|
|
||||||
svc := draftNoteService{testProjectData, fakeDraftNoteManager{testBase{status: http.StatusSeeOther}}}
|
|
||||||
data := getFailData(t, svc, request)
|
|
||||||
checkNon200(t, data, "Could not delete draft note", "/mr/draft_notes/3")
|
|
||||||
})
|
|
||||||
t.Run("Handles bad ID", func(t *testing.T) {
|
t.Run("Handles bad ID", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodDelete, "/mr/draft_notes/blah", nil)
|
request := makeRequest(t, http.MethodDelete, "/mr/draft_notes/blah", nil)
|
||||||
svc := draftNoteService{testProjectData, fakeDraftNoteManager{testBase{status: http.StatusSeeOther}}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
draftNoteService{testProjectData, fakeDraftNoteManager{}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{
|
||||||
|
http.MethodPost: &PostDraftNoteRequest{},
|
||||||
|
http.MethodPatch: &UpdateDraftNoteRequest{},
|
||||||
|
}),
|
||||||
|
withMethodCheck(http.MethodGet, http.MethodPost, http.MethodPatch, http.MethodDelete),
|
||||||
|
)
|
||||||
|
data, status := getFailData(t, svc, request)
|
||||||
assert(t, data.Message, "Could not parse draft note ID")
|
assert(t, data.Message, "Could not parse draft note ID")
|
||||||
assert(t, data.Status, http.StatusBadRequest)
|
assert(t, status, http.StatusBadRequest)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,37 +142,49 @@ func TestEditDraftNote(t *testing.T) {
|
|||||||
var testUpdateDraftNoteRequest = UpdateDraftNoteRequest{Note: "Some new note"}
|
var testUpdateDraftNoteRequest = UpdateDraftNoteRequest{Note: "Some new note"}
|
||||||
t.Run("Edits new draft note", func(t *testing.T) {
|
t.Run("Edits new draft note", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPatch, "/mr/draft_notes/3", testUpdateDraftNoteRequest)
|
request := makeRequest(t, http.MethodPatch, "/mr/draft_notes/3", testUpdateDraftNoteRequest)
|
||||||
svc := draftNoteService{testProjectData, fakeDraftNoteManager{}}
|
svc := middleware(
|
||||||
|
draftNoteService{testProjectData, fakeDraftNoteManager{}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{
|
||||||
|
http.MethodPost: &PostDraftNoteRequest{},
|
||||||
|
http.MethodPatch: &UpdateDraftNoteRequest{},
|
||||||
|
}),
|
||||||
|
withMethodCheck(http.MethodGet, http.MethodPost, http.MethodPatch, http.MethodDelete),
|
||||||
|
)
|
||||||
data := getSuccessData(t, svc, request)
|
data := getSuccessData(t, svc, request)
|
||||||
assert(t, data.Status, http.StatusOK)
|
|
||||||
assert(t, data.Message, "Draft note updated")
|
assert(t, data.Message, "Draft note updated")
|
||||||
})
|
})
|
||||||
t.Run("Handles error from Gitlab client", func(t *testing.T) {
|
|
||||||
request := makeRequest(t, http.MethodPatch, "/mr/draft_notes/3", testUpdateDraftNoteRequest)
|
|
||||||
svc := draftNoteService{testProjectData, fakeDraftNoteManager{testBase{errFromGitlab: true}}}
|
|
||||||
data := getFailData(t, svc, request)
|
|
||||||
checkErrorFromGitlab(t, data, "Could not update draft note")
|
|
||||||
})
|
|
||||||
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
|
||||||
request := makeRequest(t, http.MethodPatch, "/mr/draft_notes/3", testUpdateDraftNoteRequest)
|
|
||||||
svc := draftNoteService{testProjectData, fakeDraftNoteManager{testBase{status: http.StatusSeeOther}}}
|
|
||||||
data := getFailData(t, svc, request)
|
|
||||||
checkNon200(t, data, "Could not update draft note", "/mr/draft_notes/3")
|
|
||||||
})
|
|
||||||
t.Run("Handles bad ID", func(t *testing.T) {
|
t.Run("Handles bad ID", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPatch, "/mr/draft_notes/blah", testUpdateDraftNoteRequest)
|
request := makeRequest(t, http.MethodPatch, "/mr/draft_notes/blah", testUpdateDraftNoteRequest)
|
||||||
svc := draftNoteService{testProjectData, fakeDraftNoteManager{testBase{status: http.StatusSeeOther}}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
draftNoteService{testProjectData, fakeDraftNoteManager{}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{
|
||||||
|
http.MethodPost: &PostDraftNoteRequest{},
|
||||||
|
http.MethodPatch: &UpdateDraftNoteRequest{},
|
||||||
|
}),
|
||||||
|
withMethodCheck(http.MethodGet, http.MethodPost, http.MethodPatch, http.MethodDelete),
|
||||||
|
)
|
||||||
|
data, status := getFailData(t, svc, request)
|
||||||
assert(t, data.Message, "Could not parse draft note ID")
|
assert(t, data.Message, "Could not parse draft note ID")
|
||||||
assert(t, data.Status, http.StatusBadRequest)
|
assert(t, status, http.StatusBadRequest)
|
||||||
})
|
})
|
||||||
t.Run("Handles empty note", func(t *testing.T) {
|
t.Run("Handles empty note", func(t *testing.T) {
|
||||||
requestData := testUpdateDraftNoteRequest
|
requestData := testUpdateDraftNoteRequest
|
||||||
requestData.Note = ""
|
requestData.Note = ""
|
||||||
request := makeRequest(t, http.MethodPatch, "/mr/draft_notes/3", requestData)
|
request := makeRequest(t, http.MethodPatch, "/mr/draft_notes/3", requestData)
|
||||||
svc := draftNoteService{testProjectData, fakeDraftNoteManager{testBase{status: http.StatusSeeOther}}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
draftNoteService{testProjectData, fakeDraftNoteManager{}},
|
||||||
assert(t, data.Message, "Must provide draft note text")
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
assert(t, data.Status, http.StatusBadRequest)
|
withPayloadValidation(methodToPayload{
|
||||||
|
http.MethodPost: &PostDraftNoteRequest{},
|
||||||
|
http.MethodPatch: &UpdateDraftNoteRequest{},
|
||||||
|
}),
|
||||||
|
withMethodCheck(http.MethodGet, http.MethodPost, http.MethodPatch, http.MethodDelete),
|
||||||
|
)
|
||||||
|
data, status := getFailData(t, svc, request)
|
||||||
|
assert(t, data.Message, "Invalid payload")
|
||||||
|
assert(t, data.Details, "Note is required")
|
||||||
|
assert(t, status, http.StatusBadRequest)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,16 +48,13 @@ type emojiService struct {
|
|||||||
client EmojiManager
|
client EmojiManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a emojiService) handler(w http.ResponseWriter, r *http.Request) {
|
func (a emojiService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
a.postEmojiOnNote(w, r)
|
a.postEmojiOnNote(w, r)
|
||||||
case http.MethodDelete:
|
case http.MethodDelete:
|
||||||
a.deleteEmojiFromNote(w, r)
|
a.deleteEmojiFromNote(w, r)
|
||||||
default:
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", fmt.Sprintf("%s, %s", http.MethodDelete, http.MethodPost))
|
|
||||||
handleError(w, InvalidRequestError{}, "Expected DELETE or POST", http.StatusMethodNotAllowed)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,15 +84,12 @@ func (a emojiService) deleteEmojiFromNote(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: "/pipeline"}, "Could not delete awardable", res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, "Could not delete awardable", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response := SuccessResponse{
|
response := SuccessResponse{Message: "Emoji deleted"}
|
||||||
Message: "Emoji deleted",
|
|
||||||
Status: http.StatusOK,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err = json.NewEncoder(w).Encode(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -131,16 +125,13 @@ func (a emojiService) postEmojiOnNote(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: "/mr/awardable/note"}, "Could not post emoji", res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, "Could not post emoji", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response := CreateEmojiResponse{
|
response := CreateEmojiResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{Message: "Merge requests retrieved"},
|
||||||
Message: "Merge requests retrieved",
|
|
||||||
Status: http.StatusOK,
|
|
||||||
},
|
|
||||||
Emoji: awardEmoji,
|
Emoji: awardEmoji,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ func NewGitData(remote string, g GitManager) (GitData, error) {
|
|||||||
https://git@gitlab.com/namespace/subnamespace/dummy-test-repo.git
|
https://git@gitlab.com/namespace/subnamespace/dummy-test-repo.git
|
||||||
git@git@gitlab.com:namespace/subnamespace/dummy-test-repo.git
|
git@git@gitlab.com:namespace/subnamespace/dummy-test-repo.git
|
||||||
*/
|
*/
|
||||||
re := regexp.MustCompile(`(?:^https?:\/\/|^ssh:\/\/|^git@)(?:[^\/:]+)(?::\d+)?[\/:](.*)\/([^\/]+?)(?:\.git)?$`)
|
re := regexp.MustCompile(`^(?:git@[^\/:]*|https?:\/\/[^\/]+|ssh:\/\/[^\/:]+)(?::\d+)?[\/:](.*)\/([^\/]+?)(?:\.git)?\/?$`)
|
||||||
matches := re.FindStringSubmatch(url)
|
matches := re.FindStringSubmatch(url)
|
||||||
if len(matches) != 3 {
|
if len(matches) != 3 {
|
||||||
return GitData{}, fmt.Errorf("Invalid Git URL format: %s", url)
|
return GitData{}, fmt.Errorf("Invalid Git URL format: %s", url)
|
||||||
|
|||||||
@@ -101,6 +101,13 @@ func TestExtractGitInfo_Success(t *testing.T) {
|
|||||||
projectName: "project-name",
|
projectName: "project-name",
|
||||||
namespace: "namespace-1",
|
namespace: "namespace-1",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "Project configured in HTTP and under a single folder without .git extension (with embedded credentials)",
|
||||||
|
remote: "http://username:password@custom-gitlab.com/namespace-1/project-name",
|
||||||
|
branch: "feature/abc",
|
||||||
|
projectName: "project-name",
|
||||||
|
namespace: "namespace-1",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
desc: "Project configured in HTTPS and under a single folder",
|
desc: "Project configured in HTTPS and under a single folder",
|
||||||
remote: "https://custom-gitlab.com/namespace-1/project-name.git",
|
remote: "https://custom-gitlab.com/namespace-1/project-name.git",
|
||||||
@@ -108,6 +115,13 @@ func TestExtractGitInfo_Success(t *testing.T) {
|
|||||||
projectName: "project-name",
|
projectName: "project-name",
|
||||||
namespace: "namespace-1",
|
namespace: "namespace-1",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "Project configured in HTTPS and under a single folder (with embedded credentials)",
|
||||||
|
remote: "https://username:password@custom-gitlab.com/namespace-1/project-name.git",
|
||||||
|
branch: "feature/abc",
|
||||||
|
projectName: "project-name",
|
||||||
|
namespace: "namespace-1",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
desc: "Project configured in HTTPS and under a nested folder",
|
desc: "Project configured in HTTPS and under a nested folder",
|
||||||
remote: "https://custom-gitlab.com/namespace-1/namespace-2/project-name.git",
|
remote: "https://custom-gitlab.com/namespace-1/namespace-2/project-name.git",
|
||||||
@@ -115,6 +129,13 @@ func TestExtractGitInfo_Success(t *testing.T) {
|
|||||||
projectName: "project-name",
|
projectName: "project-name",
|
||||||
namespace: "namespace-1/namespace-2",
|
namespace: "namespace-1/namespace-2",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "Project configured in HTTPS and under a nested folder (with embedded credentials)",
|
||||||
|
remote: "https://username:password@custom-gitlab.com/namespace-1/namespace-2/project-name.git",
|
||||||
|
branch: "feature/abc",
|
||||||
|
projectName: "project-name",
|
||||||
|
namespace: "namespace-1/namespace-2",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
desc: "Project configured in HTTPS and under two nested folders",
|
desc: "Project configured in HTTPS and under two nested folders",
|
||||||
remote: "https://custom-gitlab.com/namespace-1/namespace-2/namespace-3/project-name.git",
|
remote: "https://custom-gitlab.com/namespace-1/namespace-2/namespace-3/project-name.git",
|
||||||
@@ -122,6 +143,13 @@ func TestExtractGitInfo_Success(t *testing.T) {
|
|||||||
projectName: "project-name",
|
projectName: "project-name",
|
||||||
namespace: "namespace-1/namespace-2/namespace-3",
|
namespace: "namespace-1/namespace-2/namespace-3",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "Project configured in HTTPS and under two nested folders (with embedded credentials)",
|
||||||
|
remote: "https://username:password@custom-gitlab.com/namespace-1/namespace-2/namespace-3/project-name.git",
|
||||||
|
branch: "feature/abc",
|
||||||
|
projectName: "project-name",
|
||||||
|
namespace: "namespace-1/namespace-2/namespace-3",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tC := range testCases {
|
for _, tC := range testCases {
|
||||||
t.Run(tC.desc, func(t *testing.T) {
|
t.Run(tC.desc, func(t *testing.T) {
|
||||||
|
|||||||
@@ -22,14 +22,7 @@ type infoService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* infoHandler fetches infomation about the current git project. The data returned here is used in many other API calls */
|
/* infoHandler fetches infomation about the current git project. The data returned here is used in many other API calls */
|
||||||
func (a infoService) handler(w http.ResponseWriter, r *http.Request) {
|
func (a infoService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
if r.Method != http.MethodGet {
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", http.MethodGet)
|
|
||||||
handleError(w, InvalidRequestError{}, "Expected GET", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mr, res, err := a.client.GetMergeRequest(a.projectInfo.ProjectId, a.projectInfo.MergeId, &gitlab.GetMergeRequestsOptions{})
|
mr, res, err := a.client.GetMergeRequest(a.projectInfo.ProjectId, a.projectInfo.MergeId, &gitlab.GetMergeRequestsOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, err, "Could not get project info", http.StatusInternalServerError)
|
handleError(w, err, "Could not get project info", http.StatusInternalServerError)
|
||||||
@@ -37,16 +30,13 @@ func (a infoService) handler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: "/mr/info"}, "Could not get project info", res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, "Could not get project info", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response := InfoResponse{
|
response := InfoResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{Message: "Merge requests retrieved"},
|
||||||
Message: "Merge requests retrieved",
|
|
||||||
Status: http.StatusOK,
|
|
||||||
},
|
|
||||||
Info: mr,
|
Info: mr,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,27 +23,29 @@ func (f fakeMergeRequestGetter) GetMergeRequest(pid interface{}, mergeRequest in
|
|||||||
func TestInfoHandler(t *testing.T) {
|
func TestInfoHandler(t *testing.T) {
|
||||||
t.Run("Returns normal information", func(t *testing.T) {
|
t.Run("Returns normal information", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodGet, "/mr/info", nil)
|
request := makeRequest(t, http.MethodGet, "/mr/info", nil)
|
||||||
svc := infoService{testProjectData, fakeMergeRequestGetter{}}
|
svc := middleware(
|
||||||
|
infoService{testProjectData, fakeMergeRequestGetter{}},
|
||||||
|
withMethodCheck(http.MethodGet),
|
||||||
|
)
|
||||||
data := getSuccessData(t, svc, request)
|
data := getSuccessData(t, svc, request)
|
||||||
assert(t, data.Message, "Merge requests retrieved")
|
assert(t, data.Message, "Merge requests retrieved")
|
||||||
assert(t, data.Status, http.StatusOK)
|
|
||||||
})
|
|
||||||
t.Run("Disallows non-GET methods", func(t *testing.T) {
|
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/info", nil)
|
|
||||||
svc := infoService{testProjectData, fakeMergeRequestGetter{}}
|
|
||||||
data := getFailData(t, svc, request)
|
|
||||||
checkBadMethod(t, data, http.MethodGet)
|
|
||||||
})
|
})
|
||||||
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodGet, "/mr/info", nil)
|
request := makeRequest(t, http.MethodGet, "/mr/info", nil)
|
||||||
svc := infoService{testProjectData, fakeMergeRequestGetter{testBase{errFromGitlab: true}}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
infoService{testProjectData, fakeMergeRequestGetter{testBase{errFromGitlab: true}}},
|
||||||
|
withMethodCheck(http.MethodGet),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
checkErrorFromGitlab(t, data, "Could not get project info")
|
checkErrorFromGitlab(t, data, "Could not get project info")
|
||||||
})
|
})
|
||||||
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodGet, "/mr/info", nil)
|
request := makeRequest(t, http.MethodGet, "/mr/info", nil)
|
||||||
svc := infoService{testProjectData, fakeMergeRequestGetter{testBase{status: http.StatusSeeOther}}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
infoService{testProjectData, fakeMergeRequestGetter{testBase{status: http.StatusSeeOther}}},
|
||||||
|
withMethodCheck(http.MethodGet),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
checkNon200(t, data, "Could not get project info", "/mr/info")
|
checkNon200(t, data, "Could not get project info", "/mr/info")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type JobTraceRequest struct {
|
type JobTraceRequest struct {
|
||||||
JobId int `json:"job_id"`
|
JobId int `json:"job_id" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type JobTraceResponse struct {
|
type JobTraceResponse struct {
|
||||||
@@ -28,30 +28,11 @@ type traceFileService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* jobHandler returns a string that shows the output of a specific job run in a Gitlab pipeline */
|
/* jobHandler returns a string that shows the output of a specific job run in a Gitlab pipeline */
|
||||||
func (a traceFileService) handler(w http.ResponseWriter, r *http.Request) {
|
func (a traceFileService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
if r.Method != http.MethodGet {
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", http.MethodGet)
|
|
||||||
handleError(w, InvalidRequestError{}, "Expected GET", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
payload := r.Context().Value(payload("payload")).(*JobTraceRequest)
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer r.Body.Close()
|
reader, res, err := a.client.GetTraceFile(a.projectInfo.ProjectId, payload.JobId)
|
||||||
|
|
||||||
var jobTraceRequest JobTraceRequest
|
|
||||||
err = json.Unmarshal(body, &jobTraceRequest)
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not unmarshal data from request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
reader, res, err := a.client.GetTraceFile(a.projectInfo.ProjectId, jobTraceRequest.JobId)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, err, "Could not get trace file for job", http.StatusInternalServerError)
|
handleError(w, err, "Could not get trace file for job", http.StatusInternalServerError)
|
||||||
@@ -59,7 +40,7 @@ func (a traceFileService) handler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: "/job"}, "Could not get trace file for job", res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, "Could not get trace file for job", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,10 +52,7 @@ func (a traceFileService) handler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
response := JobTraceResponse{
|
response := JobTraceResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{Message: "Log file read"},
|
||||||
Status: http.StatusOK,
|
|
||||||
Message: "Log file read",
|
|
||||||
},
|
|
||||||
File: string(file),
|
File: string(file),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ type fakeTraceFileGetter struct {
|
|||||||
testBase
|
testBase
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTraceFileData(t *testing.T, svc ServiceWithHandler, request *http.Request) JobTraceResponse {
|
func getTraceFileData(t *testing.T, svc http.Handler, request *http.Request) JobTraceResponse {
|
||||||
res := httptest.NewRecorder()
|
res := httptest.NewRecorder()
|
||||||
svc.handler(res, request)
|
svc.ServeHTTP(res, request)
|
||||||
|
|
||||||
var data JobTraceResponse
|
var data JobTraceResponse
|
||||||
err := json.Unmarshal(res.Body.Bytes(), &data)
|
err := json.Unmarshal(res.Body.Bytes(), &data)
|
||||||
@@ -35,37 +35,37 @@ func (f fakeTraceFileGetter) GetTraceFile(pid interface{}, jobID int, options ..
|
|||||||
return re, resp, err
|
return re, resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// var jobId = 0
|
|
||||||
func TestJobHandler(t *testing.T) {
|
func TestJobHandler(t *testing.T) {
|
||||||
t.Run("Should read a job trace file", func(t *testing.T) {
|
t.Run("Should read a job trace file", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodGet, "/job", JobTraceRequest{})
|
request := makeRequest(t, http.MethodGet, "/job", JobTraceRequest{JobId: 3})
|
||||||
client := fakeTraceFileGetter{}
|
svc := middleware(
|
||||||
svc := traceFileService{testProjectData, client}
|
traceFileService{testProjectData, fakeTraceFileGetter{}},
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodGet: &JobTraceRequest{}}),
|
||||||
|
withMethodCheck(http.MethodGet),
|
||||||
|
)
|
||||||
data := getTraceFileData(t, svc, request)
|
data := getTraceFileData(t, svc, request)
|
||||||
assert(t, data.Message, "Log file read")
|
assert(t, data.Message, "Log file read")
|
||||||
assert(t, data.Status, http.StatusOK)
|
|
||||||
assert(t, data.File, "Some data")
|
assert(t, data.File, "Some data")
|
||||||
})
|
})
|
||||||
t.Run("Disallows non-GET methods", func(t *testing.T) {
|
|
||||||
request := makeRequest(t, http.MethodPost, "/job", JobTraceRequest{})
|
|
||||||
client := fakeTraceFileGetter{}
|
|
||||||
svc := traceFileService{testProjectData, client}
|
|
||||||
data := getFailData(t, svc, request)
|
|
||||||
checkBadMethod(t, data, http.MethodGet)
|
|
||||||
})
|
|
||||||
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodGet, "/job", JobTraceRequest{})
|
request := makeRequest(t, http.MethodGet, "/job", JobTraceRequest{JobId: 2})
|
||||||
client := fakeTraceFileGetter{testBase{errFromGitlab: true}}
|
svc := middleware(
|
||||||
svc := traceFileService{testProjectData, client}
|
traceFileService{testProjectData, fakeTraceFileGetter{testBase{errFromGitlab: true}}},
|
||||||
data := getFailData(t, svc, request)
|
withPayloadValidation(methodToPayload{http.MethodGet: &JobTraceRequest{}}),
|
||||||
|
withMethodCheck(http.MethodGet),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
checkErrorFromGitlab(t, data, "Could not get trace file for job")
|
checkErrorFromGitlab(t, data, "Could not get trace file for job")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodGet, "/job", JobTraceRequest{})
|
request := makeRequest(t, http.MethodGet, "/job", JobTraceRequest{JobId: 1})
|
||||||
client := fakeTraceFileGetter{testBase{status: http.StatusSeeOther}}
|
svc := middleware(
|
||||||
svc := traceFileService{testProjectData, client}
|
traceFileService{testProjectData, fakeTraceFileGetter{testBase{status: http.StatusSeeOther}}},
|
||||||
data := getFailData(t, svc, request)
|
withPayloadValidation(methodToPayload{http.MethodGet: &JobTraceRequest{}}),
|
||||||
|
withMethodCheck(http.MethodGet),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
checkNon200(t, data, "Could not get trace file for job", "/job")
|
checkNon200(t, data, "Could not get trace file for job", "/job")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
@@ -39,20 +38,16 @@ type labelService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* labelsHandler adds or removes labels from a merge request, and returns all labels for the current project */
|
/* labelsHandler adds or removes labels from a merge request, and returns all labels for the current project */
|
||||||
func (a labelService) handler(w http.ResponseWriter, r *http.Request) {
|
func (a labelService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
a.getLabels(w, r)
|
a.getLabels(w, r)
|
||||||
case http.MethodPut:
|
case http.MethodPut:
|
||||||
a.updateLabels(w, r)
|
a.updateLabels(w, r)
|
||||||
default:
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", fmt.Sprintf("%s, %s", http.MethodPut, http.MethodGet))
|
|
||||||
handleError(w, InvalidRequestError{}, "Expected GET or PUT", http.StatusMethodNotAllowed)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a labelService) getLabels(w http.ResponseWriter, _ *http.Request) {
|
func (a labelService) getLabels(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
labels, res, err := a.client.ListLabels(a.projectInfo.ProjectId, &gitlab.ListLabelsOptions{})
|
labels, res, err := a.client.ListLabels(a.projectInfo.ProjectId, &gitlab.ListLabelsOptions{})
|
||||||
@@ -63,7 +58,7 @@ func (a labelService) getLabels(w http.ResponseWriter, _ *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: "/mr/label"}, "Could not modify merge request labels", res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, "Could not modify merge request labels", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,10 +73,7 @@ func (a labelService) getLabels(w http.ResponseWriter, _ *http.Request) {
|
|||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response := LabelsRequestResponse{
|
response := LabelsRequestResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{Message: "Labels updated"},
|
||||||
Message: "Labels updated",
|
|
||||||
Status: http.StatusOK,
|
|
||||||
},
|
|
||||||
Labels: convertedLabels,
|
Labels: convertedLabels,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,16 +112,13 @@ func (a labelService) updateLabels(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: "/mr/label"}, "Could not modify merge request labels", res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, "Could not modify merge request labels", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response := LabelUpdateResponse{
|
response := LabelUpdateResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{Message: "Labels updated"},
|
||||||
Message: "Labels updated",
|
|
||||||
Status: http.StatusOK,
|
|
||||||
},
|
|
||||||
Labels: mr.Labels,
|
Labels: mr.Labels,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -21,7 +20,7 @@ func Contains[T comparable](elems []T, v T) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DiscussionsRequest struct {
|
type DiscussionsRequest struct {
|
||||||
Blacklist []string `json:"blacklist"`
|
Blacklist []string `json:"blacklist" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DiscussionsResponse struct {
|
type DiscussionsResponse struct {
|
||||||
@@ -61,27 +60,9 @@ type discussionsListerService struct {
|
|||||||
listDiscussionsHandler lists all discusions for a given merge request, both those linked and unlinked to particular points in the code.
|
listDiscussionsHandler lists all discusions for a given merge request, both those linked and unlinked to particular points in the code.
|
||||||
The responses are sorted by date created, and blacklisted users are not included
|
The responses are sorted by date created, and blacklisted users are not included
|
||||||
*/
|
*/
|
||||||
func (a discussionsListerService) handler(w http.ResponseWriter, r *http.Request) {
|
func (a discussionsListerService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", http.MethodPost)
|
|
||||||
handleError(w, InvalidRequestError{}, "Expected POST", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
request := r.Context().Value(payload(payload("payload"))).(*DiscussionsRequest)
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
var requestBody DiscussionsRequest
|
|
||||||
err = json.Unmarshal(body, &requestBody)
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not unmarshal request body", http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
mergeRequestDiscussionOptions := gitlab.ListMergeRequestDiscussionsOptions{
|
mergeRequestDiscussionOptions := gitlab.ListMergeRequestDiscussionsOptions{
|
||||||
Page: 1,
|
Page: 1,
|
||||||
@@ -96,7 +77,7 @@ func (a discussionsListerService) handler(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: "/mr/discussions/list"}, "Could not list discussions", res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, "Could not list discussions", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +87,7 @@ func (a discussionsListerService) handler(w http.ResponseWriter, r *http.Request
|
|||||||
var linkedDiscussions []*gitlab.Discussion
|
var linkedDiscussions []*gitlab.Discussion
|
||||||
|
|
||||||
for _, discussion := range discussions {
|
for _, discussion := range discussions {
|
||||||
if discussion.Notes == nil || len(discussion.Notes) == 0 || Contains(requestBody.Blacklist, discussion.Notes[0].Author.Username) {
|
if discussion.Notes == nil || len(discussion.Notes) == 0 || Contains(request.Blacklist, discussion.Notes[0].Author.Username) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, note := range discussion.Notes {
|
for _, note := range discussion.Notes {
|
||||||
@@ -142,10 +123,7 @@ func (a discussionsListerService) handler(w http.ResponseWriter, r *http.Request
|
|||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response := DiscussionsResponse{
|
response := DiscussionsResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{Message: "Discussions retrieved"},
|
||||||
Message: "Discussions retrieved",
|
|
||||||
Status: http.StatusOK,
|
|
||||||
},
|
|
||||||
Discussions: linkedDiscussions,
|
Discussions: linkedDiscussions,
|
||||||
UnlinkedDiscussions: unlinkedDiscussions,
|
UnlinkedDiscussions: unlinkedDiscussions,
|
||||||
Emojis: emojis,
|
Emojis: emojis,
|
||||||
|
|||||||
@@ -53,9 +53,9 @@ func (f fakeDiscussionsLister) ListMergeRequestAwardEmojiOnNote(pid interface{},
|
|||||||
return []*gitlab.AwardEmoji{}, resp, err
|
return []*gitlab.AwardEmoji{}, resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDiscussionsList(t *testing.T, svc ServiceWithHandler, request *http.Request) DiscussionsResponse {
|
func getDiscussionsList(t *testing.T, svc http.Handler, request *http.Request) DiscussionsResponse {
|
||||||
res := httptest.NewRecorder()
|
res := httptest.NewRecorder()
|
||||||
svc.handler(res, request)
|
svc.ServeHTTP(res, request)
|
||||||
|
|
||||||
var data DiscussionsResponse
|
var data DiscussionsResponse
|
||||||
err := json.Unmarshal(res.Body.Bytes(), &data)
|
err := json.Unmarshal(res.Body.Bytes(), &data)
|
||||||
@@ -67,46 +67,63 @@ func getDiscussionsList(t *testing.T, svc ServiceWithHandler, request *http.Requ
|
|||||||
|
|
||||||
func TestListDiscussions(t *testing.T) {
|
func TestListDiscussions(t *testing.T) {
|
||||||
t.Run("Returns sorted discussions", func(t *testing.T) {
|
t.Run("Returns sorted discussions", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{})
|
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{Blacklist: []string{}})
|
||||||
svc := discussionsListerService{testProjectData, fakeDiscussionsLister{}}
|
svc := middleware(
|
||||||
|
discussionsListerService{testProjectData, fakeDiscussionsLister{}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &DiscussionsRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
data := getDiscussionsList(t, svc, request)
|
data := getDiscussionsList(t, svc, request)
|
||||||
assert(t, data.Message, "Discussions retrieved")
|
assert(t, data.Message, "Discussions retrieved")
|
||||||
assert(t, data.SuccessResponse.Status, http.StatusOK)
|
|
||||||
assert(t, data.Discussions[0].Notes[0].Author.Username, "hcramer2") /* Sorting applied */
|
assert(t, data.Discussions[0].Notes[0].Author.Username, "hcramer2") /* Sorting applied */
|
||||||
assert(t, data.Discussions[1].Notes[0].Author.Username, "hcramer")
|
assert(t, data.Discussions[1].Notes[0].Author.Username, "hcramer")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Uses blacklist to filter unwanted authors", func(t *testing.T) {
|
t.Run("Uses blacklist to filter unwanted authors", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{Blacklist: []string{"hcramer"}})
|
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{Blacklist: []string{"hcramer"}})
|
||||||
svc := discussionsListerService{testProjectData, fakeDiscussionsLister{}}
|
svc := middleware(
|
||||||
|
discussionsListerService{testProjectData, fakeDiscussionsLister{}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &DiscussionsRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
data := getDiscussionsList(t, svc, request)
|
data := getDiscussionsList(t, svc, request)
|
||||||
assert(t, data.SuccessResponse.Message, "Discussions retrieved")
|
assert(t, data.SuccessResponse.Message, "Discussions retrieved")
|
||||||
assert(t, data.SuccessResponse.Status, http.StatusOK)
|
|
||||||
assert(t, len(data.Discussions), 1)
|
assert(t, len(data.Discussions), 1)
|
||||||
assert(t, data.Discussions[0].Notes[0].Author.Username, "hcramer2")
|
assert(t, data.Discussions[0].Notes[0].Author.Username, "hcramer2")
|
||||||
})
|
})
|
||||||
t.Run("Disallows non-GET methods", func(t *testing.T) {
|
|
||||||
request := makeRequest(t, http.MethodGet, "/mr/discussions/list", DiscussionsRequest{})
|
|
||||||
svc := discussionsListerService{testProjectData, fakeDiscussionsLister{}}
|
|
||||||
data := getFailData(t, svc, request)
|
|
||||||
checkBadMethod(t, data, http.MethodPost)
|
|
||||||
})
|
|
||||||
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{})
|
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{Blacklist: []string{}})
|
||||||
svc := discussionsListerService{testProjectData, fakeDiscussionsLister{testBase: testBase{errFromGitlab: true}}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
discussionsListerService{testProjectData, fakeDiscussionsLister{testBase: testBase{errFromGitlab: true}}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &DiscussionsRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
checkErrorFromGitlab(t, data, "Could not list discussions")
|
checkErrorFromGitlab(t, data, "Could not list discussions")
|
||||||
})
|
})
|
||||||
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{})
|
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{Blacklist: []string{}})
|
||||||
svc := discussionsListerService{testProjectData, fakeDiscussionsLister{testBase: testBase{status: http.StatusSeeOther}}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
discussionsListerService{testProjectData, fakeDiscussionsLister{testBase: testBase{status: http.StatusSeeOther}}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &DiscussionsRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
checkNon200(t, data, "Could not list discussions", "/mr/discussions/list")
|
checkNon200(t, data, "Could not list discussions", "/mr/discussions/list")
|
||||||
})
|
})
|
||||||
t.Run("Handles error from emoji service", func(t *testing.T) {
|
t.Run("Handles error from emoji service", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{})
|
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{Blacklist: []string{}})
|
||||||
svc := discussionsListerService{testProjectData, fakeDiscussionsLister{badEmojiResponse: true}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
discussionsListerService{testProjectData, fakeDiscussionsLister{badEmojiResponse: true, testBase: testBase{}}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &DiscussionsRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
assert(t, data.Message, "Could not fetch emojis")
|
assert(t, data.Message, "Could not fetch emojis")
|
||||||
assert(t, data.Details, "Some error from emoji service")
|
assert(t, data.Details, "Some error from emoji service")
|
||||||
})
|
})
|
||||||
|
|||||||
96
cmd/app/logging.go
Normal file
96
cmd/app/logging.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoggingServer is a wrapper around an http.Handler to log incoming requests and outgoing responses.
|
||||||
|
type LoggingServer struct {
|
||||||
|
handler http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoggingResponseWriter struct {
|
||||||
|
statusCode int
|
||||||
|
body *bytes.Buffer
|
||||||
|
http.ResponseWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LoggingResponseWriter) WriteHeader(statusCode int) {
|
||||||
|
l.statusCode = statusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LoggingResponseWriter) Write(b []byte) (int, error) {
|
||||||
|
l.body.Write(b)
|
||||||
|
return l.ResponseWriter.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logs the request, calls the original handler on the ServeMux, then logs the response
|
||||||
|
func (l LoggingServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if pluginOptions.Debug.Request {
|
||||||
|
logRequest("REQUEST TO GO SERVER", r)
|
||||||
|
}
|
||||||
|
lrw := &LoggingResponseWriter{ResponseWriter: w, body: &bytes.Buffer{}}
|
||||||
|
l.handler.ServeHTTP(lrw, r)
|
||||||
|
resp := &http.Response{
|
||||||
|
Status: http.StatusText(lrw.statusCode),
|
||||||
|
StatusCode: lrw.statusCode,
|
||||||
|
Body: io.NopCloser(bytes.NewBuffer(lrw.body.Bytes())), // Use the captured body
|
||||||
|
ContentLength: int64(lrw.body.Len()),
|
||||||
|
Header: lrw.Header(),
|
||||||
|
Request: r,
|
||||||
|
}
|
||||||
|
if pluginOptions.Debug.Response {
|
||||||
|
logResponse("RESPONSE FROM GO SERVER", resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func logRequest(prefix string, r *http.Request) {
|
||||||
|
file := openLogFile()
|
||||||
|
defer file.Close()
|
||||||
|
token := r.Header.Get("Private-Token")
|
||||||
|
r.Header.Set("Private-Token", "REDACTED")
|
||||||
|
res, err := httputil.DumpRequest(r, true)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error dumping request: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
r.Header.Set("Private-Token", token)
|
||||||
|
_, err = file.Write([]byte(fmt.Sprintf("\n-- %s --\n%s\n", prefix, res))) //nolint:all
|
||||||
|
}
|
||||||
|
|
||||||
|
func logResponse(prefix string, r *http.Response) {
|
||||||
|
file := openLogFile()
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
res, err := httputil.DumpResponse(r, true)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error dumping response: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = file.Write([]byte(fmt.Sprintf("\n-- %s --\n%s\n", prefix, res))) //nolint:all
|
||||||
|
}
|
||||||
|
|
||||||
|
func openLogFile() *os.File {
|
||||||
|
file, err := os.OpenFile(pluginOptions.LogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
log.Printf("Log file %s does not exist", pluginOptions.LogPath)
|
||||||
|
} else if os.IsPermission(err) {
|
||||||
|
log.Printf("Permission denied for log file %s", pluginOptions.LogPath)
|
||||||
|
} else {
|
||||||
|
log.Printf("Error opening log file %s: %v", pluginOptions.LogPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return file
|
||||||
|
}
|
||||||
@@ -22,13 +22,7 @@ type projectMemberService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* projectMembersHandler returns all members of the current Gitlab project */
|
/* projectMembersHandler returns all members of the current Gitlab project */
|
||||||
func (a projectMemberService) handler(w http.ResponseWriter, r *http.Request) {
|
func (a projectMemberService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
if r.Method != http.MethodGet {
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", http.MethodGet)
|
|
||||||
handleError(w, InvalidRequestError{}, "Expected GET", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
projectMemberOptions := gitlab.ListProjectMembersOptions{
|
projectMemberOptions := gitlab.ListProjectMembersOptions{
|
||||||
ListOptions: gitlab.ListOptions{
|
ListOptions: gitlab.ListOptions{
|
||||||
@@ -44,17 +38,14 @@ func (a projectMemberService) handler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: "/project/members"}, "Could not retrieve project members", res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, "Could not retrieve project members", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
response := ProjectMembersResponse{
|
response := ProjectMembersResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{Message: "Project members retrieved"},
|
||||||
Status: http.StatusOK,
|
|
||||||
Message: "Project members retrieved",
|
|
||||||
},
|
|
||||||
ProjectMembers: projectMembers,
|
ProjectMembers: projectMembers,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,27 +22,29 @@ func (f fakeMemberLister) ListAllProjectMembers(pid interface{}, opt *gitlab.Lis
|
|||||||
func TestMembersHandler(t *testing.T) {
|
func TestMembersHandler(t *testing.T) {
|
||||||
t.Run("Returns project members", func(t *testing.T) {
|
t.Run("Returns project members", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodGet, "/project/members", nil)
|
request := makeRequest(t, http.MethodGet, "/project/members", nil)
|
||||||
svc := projectMemberService{testProjectData, fakeMemberLister{}}
|
svc := middleware(
|
||||||
|
projectMemberService{testProjectData, fakeMemberLister{}},
|
||||||
|
withMethodCheck(http.MethodGet),
|
||||||
|
)
|
||||||
data := getSuccessData(t, svc, request)
|
data := getSuccessData(t, svc, request)
|
||||||
assert(t, data.Status, http.StatusOK)
|
|
||||||
assert(t, data.Message, "Project members retrieved")
|
assert(t, data.Message, "Project members retrieved")
|
||||||
})
|
})
|
||||||
t.Run("Disallows non-GET methods", func(t *testing.T) {
|
|
||||||
request := makeRequest(t, http.MethodPost, "/project/members", nil)
|
|
||||||
svc := projectMemberService{testProjectData, fakeMemberLister{}}
|
|
||||||
data := getFailData(t, svc, request)
|
|
||||||
checkBadMethod(t, data, http.MethodGet)
|
|
||||||
})
|
|
||||||
t.Run("Handles error from Gitlab client", func(t *testing.T) {
|
t.Run("Handles error from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodGet, "/project/members", nil)
|
request := makeRequest(t, http.MethodGet, "/project/members", nil)
|
||||||
svc := projectMemberService{testProjectData, fakeMemberLister{testBase{errFromGitlab: true}}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
projectMemberService{testProjectData, fakeMemberLister{testBase{errFromGitlab: true}}},
|
||||||
|
withMethodCheck(http.MethodGet),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
checkErrorFromGitlab(t, data, "Could not retrieve project members")
|
checkErrorFromGitlab(t, data, "Could not retrieve project members")
|
||||||
})
|
})
|
||||||
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodGet, "/project/members", nil)
|
request := makeRequest(t, http.MethodGet, "/project/members", nil)
|
||||||
svc := projectMemberService{testProjectData, fakeMemberLister{testBase{status: http.StatusSeeOther}}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
projectMemberService{testProjectData, fakeMemberLister{testBase{status: http.StatusSeeOther}}},
|
||||||
|
withMethodCheck(http.MethodGet),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
checkNon200(t, data, "Could not retrieve project members", "/project/members")
|
checkNon200(t, data, "Could not retrieve project members", "/project/members")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,15 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/xanzy/go-gitlab"
|
"github.com/xanzy/go-gitlab"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AcceptMergeRequestRequest struct {
|
type AcceptMergeRequestRequest struct {
|
||||||
Squash bool `json:"squash"`
|
|
||||||
SquashMessage string `json:"squash_message"`
|
|
||||||
DeleteBranch bool `json:"delete_branch"`
|
DeleteBranch bool `json:"delete_branch"`
|
||||||
|
SquashMessage string `json:"squash_message"`
|
||||||
|
Squash bool `json:"squash"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MergeRequestAccepter interface {
|
type MergeRequestAccepter interface {
|
||||||
@@ -24,34 +23,16 @@ type mergeRequestAccepterService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* acceptAndMergeHandler merges a given merge request into the target branch */
|
/* acceptAndMergeHandler merges a given merge request into the target branch */
|
||||||
func (a mergeRequestAccepterService) handler(w http.ResponseWriter, r *http.Request) {
|
func (a mergeRequestAccepterService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
payload := r.Context().Value(payload("payload")).(*AcceptMergeRequestRequest)
|
||||||
w.Header().Set("Access-Control-Allow-Methods", http.MethodGet)
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
handleError(w, InvalidRequestError{}, "Expected POST", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var acceptAndMergeRequest AcceptMergeRequestRequest
|
|
||||||
err = json.Unmarshal(body, &acceptAndMergeRequest)
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not unmarshal request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := gitlab.AcceptMergeRequestOptions{
|
opts := gitlab.AcceptMergeRequestOptions{
|
||||||
Squash: &acceptAndMergeRequest.Squash,
|
Squash: &payload.Squash,
|
||||||
ShouldRemoveSourceBranch: &acceptAndMergeRequest.DeleteBranch,
|
ShouldRemoveSourceBranch: &payload.DeleteBranch,
|
||||||
}
|
}
|
||||||
|
|
||||||
if acceptAndMergeRequest.SquashMessage != "" {
|
if payload.SquashMessage != "" {
|
||||||
opts.SquashCommitMessage = &acceptAndMergeRequest.SquashMessage
|
opts.SquashCommitMessage = &payload.SquashMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
_, res, err := a.client.AcceptMergeRequest(a.projectInfo.ProjectId, a.projectInfo.MergeId, &opts)
|
_, res, err := a.client.AcceptMergeRequest(a.projectInfo.ProjectId, a.projectInfo.MergeId, &opts)
|
||||||
@@ -62,14 +43,11 @@ func (a mergeRequestAccepterService) handler(w http.ResponseWriter, r *http.Requ
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: "/mr/merge"}, "Could not merge MR", res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, "Could not merge MR", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response := SuccessResponse{
|
response := SuccessResponse{Message: "MR merged successfully"}
|
||||||
Status: http.StatusOK,
|
|
||||||
Message: "MR merged successfully",
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
|
|||||||
@@ -24,27 +24,41 @@ func TestAcceptAndMergeHandler(t *testing.T) {
|
|||||||
var testAcceptMergeRequestPayload = AcceptMergeRequestRequest{Squash: false, SquashMessage: "Squash me!", DeleteBranch: false}
|
var testAcceptMergeRequestPayload = AcceptMergeRequestRequest{Squash: false, SquashMessage: "Squash me!", DeleteBranch: false}
|
||||||
t.Run("Accepts and merges a merge request", func(t *testing.T) {
|
t.Run("Accepts and merges a merge request", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/merge", testAcceptMergeRequestPayload)
|
request := makeRequest(t, http.MethodPost, "/mr/merge", testAcceptMergeRequestPayload)
|
||||||
svc := mergeRequestAccepterService{testProjectData, fakeMergeRequestAccepter{}}
|
svc := middleware(
|
||||||
|
mergeRequestAccepterService{testProjectData, fakeMergeRequestAccepter{}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{
|
||||||
|
http.MethodPost: &AcceptMergeRequestRequest{},
|
||||||
|
}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
data := getSuccessData(t, svc, request)
|
data := getSuccessData(t, svc, request)
|
||||||
assert(t, data.Message, "MR merged successfully")
|
assert(t, data.Message, "MR merged successfully")
|
||||||
assert(t, data.Status, http.StatusOK)
|
|
||||||
})
|
|
||||||
t.Run("Disallows non-POST methods", func(t *testing.T) {
|
|
||||||
request := makeRequest(t, http.MethodPut, "/mr/merge", testAcceptMergeRequestPayload)
|
|
||||||
svc := mergeRequestAccepterService{testProjectData, fakeMergeRequestAccepter{}}
|
|
||||||
data := getFailData(t, svc, request)
|
|
||||||
checkBadMethod(t, data, http.MethodPost)
|
|
||||||
})
|
})
|
||||||
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/merge", testAcceptMergeRequestPayload)
|
request := makeRequest(t, http.MethodPost, "/mr/merge", testAcceptMergeRequestPayload)
|
||||||
svc := mergeRequestAccepterService{testProjectData, fakeMergeRequestAccepter{testBase{errFromGitlab: true}}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
mergeRequestAccepterService{testProjectData, fakeMergeRequestAccepter{testBase{errFromGitlab: true}}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{
|
||||||
|
http.MethodPost: &AcceptMergeRequestRequest{},
|
||||||
|
}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
checkErrorFromGitlab(t, data, "Could not merge MR")
|
checkErrorFromGitlab(t, data, "Could not merge MR")
|
||||||
})
|
})
|
||||||
t.Run("Handles non-200s from Gitlab", func(t *testing.T) {
|
t.Run("Handles non-200s from Gitlab", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/merge", testAcceptMergeRequestPayload)
|
request := makeRequest(t, http.MethodPost, "/mr/merge", testAcceptMergeRequestPayload)
|
||||||
svc := mergeRequestAccepterService{testProjectData, fakeMergeRequestAccepter{testBase{status: http.StatusSeeOther}}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
mergeRequestAccepterService{testProjectData, fakeMergeRequestAccepter{testBase{status: http.StatusSeeOther}}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{
|
||||||
|
http.MethodPost: &AcceptMergeRequestRequest{},
|
||||||
|
}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
checkNon200(t, data, "Could not merge MR", "/mr/merge")
|
checkNon200(t, data, "Could not merge MR", "/mr/merge")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package app
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/xanzy/go-gitlab"
|
"github.com/xanzy/go-gitlab"
|
||||||
@@ -23,37 +22,20 @@ type mergeRequestListerService struct {
|
|||||||
client MergeRequestLister
|
client MergeRequestLister
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a mergeRequestListerService) handler(w http.ResponseWriter, r *http.Request) {
|
// Lists all merge requests in Gitlab according to the provided filters
|
||||||
w.Header().Set("Content-Type", "application/json")
|
func (a mergeRequestListerService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", http.MethodPost)
|
payload := r.Context().Value(payload("payload")).(*gitlab.ListProjectMergeRequestsOptions)
|
||||||
handleError(w, InvalidRequestError{}, "Expected POST", http.StatusMethodNotAllowed)
|
|
||||||
return
|
if payload.State == nil {
|
||||||
|
payload.State = gitlab.Ptr("opened")
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
if payload.Scope == nil {
|
||||||
if err != nil {
|
payload.Scope = gitlab.Ptr("all")
|
||||||
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defer r.Body.Close()
|
mergeRequests, res, err := a.client.ListProjectMergeRequests(a.projectInfo.ProjectId, payload)
|
||||||
var listMergeRequestRequest gitlab.ListProjectMergeRequestsOptions
|
|
||||||
err = json.Unmarshal(body, &listMergeRequestRequest)
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if listMergeRequestRequest.State == nil {
|
|
||||||
listMergeRequestRequest.State = gitlab.Ptr("opened")
|
|
||||||
}
|
|
||||||
|
|
||||||
if listMergeRequestRequest.Scope == nil {
|
|
||||||
listMergeRequestRequest.Scope = gitlab.Ptr("all")
|
|
||||||
}
|
|
||||||
|
|
||||||
mergeRequests, res, err := a.client.ListProjectMergeRequests(a.projectInfo.ProjectId, &listMergeRequestRequest)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, err, "Failed to list merge requests", http.StatusInternalServerError)
|
handleError(w, err, "Failed to list merge requests", http.StatusInternalServerError)
|
||||||
@@ -61,7 +43,7 @@ func (a mergeRequestListerService) handler(w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: "/merge_requests"}, "Failed to list merge requests", res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, "Failed to list merge requests", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,10 +54,7 @@ func (a mergeRequestListerService) handler(w http.ResponseWriter, r *http.Reques
|
|||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response := ListMergeRequestResponse{
|
response := ListMergeRequestResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{Message: "Merge requests fetched successfully"},
|
||||||
Message: "Merge requests fetched successfully",
|
|
||||||
Status: http.StatusOK,
|
|
||||||
},
|
|
||||||
MergeRequests: mergeRequests,
|
MergeRequests: mergeRequests,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -21,42 +20,15 @@ type mergeRequestListerByUsernameService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MergeRequestByUsernameRequest struct {
|
type MergeRequestByUsernameRequest struct {
|
||||||
UserId int `json:"user_id"`
|
UserId int `json:"user_id" validate:"required"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username" validate:"required"`
|
||||||
State string `json:"state,omitempty"`
|
State string `json:"state,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a mergeRequestListerByUsernameService) handler(w http.ResponseWriter, r *http.Request) {
|
// Returns a list of merge requests where the given username/id is either an assignee, reviewer, or author
|
||||||
w.Header().Set("Content-Type", "application/json")
|
func (a mergeRequestListerByUsernameService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", http.MethodPost)
|
|
||||||
handleError(w, InvalidRequestError{}, "Expected POST", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
request := r.Context().Value(payload("payload")).(*MergeRequestByUsernameRequest)
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer r.Body.Close()
|
|
||||||
var request MergeRequestByUsernameRequest
|
|
||||||
err = json.Unmarshal(body, &request)
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if request.Username == "" {
|
|
||||||
handleError(w, errors.New("username is a required payload field"), "username is required", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if request.UserId == 0 {
|
|
||||||
handleError(w, errors.New("user_id is a required payload field"), "user_id is required", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if request.State == "" {
|
if request.State == "" {
|
||||||
request.State = "opened"
|
request.State = "opened"
|
||||||
@@ -133,14 +105,11 @@ func (a mergeRequestListerByUsernameService) handler(w http.ResponseWriter, r *h
|
|||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response := ListMergeRequestResponse{
|
response := ListMergeRequestResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{Message: fmt.Sprintf("Merge requests fetched for %s", request.Username)},
|
||||||
Message: fmt.Sprintf("Merge requests fetched for %s", request.Username),
|
|
||||||
Status: http.StatusOK,
|
|
||||||
},
|
|
||||||
MergeRequests: mergeRequests,
|
MergeRequests: mergeRequests,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err := json.NewEncoder(w).Encode(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,58 +30,81 @@ func TestListMergeRequestByUsername(t *testing.T) {
|
|||||||
var testListMrsByUsernamePayload = MergeRequestByUsernameRequest{Username: "hcramer", UserId: 1234, State: "opened"}
|
var testListMrsByUsernamePayload = MergeRequestByUsernameRequest{Username: "hcramer", UserId: 1234, State: "opened"}
|
||||||
t.Run("Gets merge requests by username", func(t *testing.T) {
|
t.Run("Gets merge requests by username", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/merge_requests_by_username", testListMrsByUsernamePayload)
|
request := makeRequest(t, http.MethodPost, "/merge_requests_by_username", testListMrsByUsernamePayload)
|
||||||
svc := mergeRequestListerByUsernameService{testProjectData, fakeMergeRequestListerByUsername{}}
|
svc := middleware(
|
||||||
|
mergeRequestListerByUsernameService{testProjectData, fakeMergeRequestListerByUsername{}},
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &MergeRequestByUsernameRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
data := getSuccessData(t, svc, request)
|
data := getSuccessData(t, svc, request)
|
||||||
assert(t, data.Message, "Merge requests fetched for hcramer")
|
assert(t, data.Message, "Merge requests fetched for hcramer")
|
||||||
assert(t, data.Status, http.StatusOK)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Should handle no merge requests", func(t *testing.T) {
|
t.Run("Should handle no merge requests", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/merge_requests_by_username", testListMrsByUsernamePayload)
|
request := makeRequest(t, http.MethodPost, "/merge_requests_by_username", testListMrsByUsernamePayload)
|
||||||
svc := mergeRequestListerByUsernameService{testProjectData, fakeMergeRequestListerByUsername{emptyResponse: true}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
mergeRequestListerByUsernameService{testProjectData, fakeMergeRequestListerByUsername{emptyResponse: true}},
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &MergeRequestByUsernameRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
|
data, status := getFailData(t, svc, request)
|
||||||
assert(t, data.Message, "No MRs found")
|
assert(t, data.Message, "No MRs found")
|
||||||
assert(t, data.Details, "hcramer did not have any MRs")
|
assert(t, data.Details, "hcramer did not have any MRs")
|
||||||
assert(t, data.Status, http.StatusNotFound)
|
assert(t, status, http.StatusNotFound)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Should require username", func(t *testing.T) {
|
t.Run("Should require username", func(t *testing.T) {
|
||||||
missingUsernamePayload := testListMrsByUsernamePayload
|
missingUsernamePayload := testListMrsByUsernamePayload
|
||||||
missingUsernamePayload.Username = ""
|
missingUsernamePayload.Username = ""
|
||||||
request := makeRequest(t, http.MethodPost, "/merge_requests_by_username", missingUsernamePayload)
|
request := makeRequest(t, http.MethodPost, "/merge_requests_by_username", missingUsernamePayload)
|
||||||
svc := mergeRequestListerByUsernameService{testProjectData, fakeMergeRequestListerByUsername{}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
mergeRequestListerByUsernameService{testProjectData, fakeMergeRequestListerByUsername{}},
|
||||||
assert(t, data.Message, "username is required")
|
withPayloadValidation(methodToPayload{http.MethodPost: &MergeRequestByUsernameRequest{}}),
|
||||||
assert(t, data.Details, "username is a required payload field")
|
withMethodCheck(http.MethodPost),
|
||||||
assert(t, data.Status, http.StatusBadRequest)
|
)
|
||||||
|
data, status := getFailData(t, svc, request)
|
||||||
|
assert(t, data.Message, "Invalid payload")
|
||||||
|
assert(t, data.Details, "Username is required")
|
||||||
|
assert(t, status, http.StatusBadRequest)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Should require User ID for assignee call", func(t *testing.T) {
|
t.Run("Should require User ID for assignee call", func(t *testing.T) {
|
||||||
missingUsernamePayload := testListMrsByUsernamePayload
|
missingUsernamePayload := testListMrsByUsernamePayload
|
||||||
missingUsernamePayload.UserId = 0
|
missingUsernamePayload.UserId = 0
|
||||||
request := makeRequest(t, http.MethodPost, "/merge_requests_by_username", missingUsernamePayload)
|
request := makeRequest(t, http.MethodPost, "/merge_requests_by_username", missingUsernamePayload)
|
||||||
svc := mergeRequestListerByUsernameService{testProjectData, fakeMergeRequestListerByUsername{}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
mergeRequestListerByUsernameService{testProjectData, fakeMergeRequestListerByUsername{}},
|
||||||
assert(t, data.Message, "user_id is required")
|
withPayloadValidation(methodToPayload{http.MethodPost: &MergeRequestByUsernameRequest{}}),
|
||||||
assert(t, data.Details, "user_id is a required payload field")
|
withMethodCheck(http.MethodPost),
|
||||||
assert(t, data.Status, http.StatusBadRequest)
|
)
|
||||||
|
data, status := getFailData(t, svc, request)
|
||||||
|
assert(t, data.Message, "Invalid payload")
|
||||||
|
assert(t, data.Details, "UserId is required")
|
||||||
|
assert(t, status, http.StatusBadRequest)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Should handle error from Gitlab", func(t *testing.T) {
|
t.Run("Should handle error from Gitlab", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/merge_requests_by_username", testListMrsByUsernamePayload)
|
request := makeRequest(t, http.MethodPost, "/merge_requests_by_username", testListMrsByUsernamePayload)
|
||||||
svc := mergeRequestListerByUsernameService{testProjectData, fakeMergeRequestListerByUsername{testBase: testBase{errFromGitlab: true}}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
mergeRequestListerByUsernameService{testProjectData, fakeMergeRequestListerByUsername{testBase: testBase{errFromGitlab: true}}},
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &MergeRequestByUsernameRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
|
data, status := getFailData(t, svc, request)
|
||||||
assert(t, data.Message, "An error occurred")
|
assert(t, data.Message, "An error occurred")
|
||||||
assert(t, data.Details, strings.Repeat("Some error from Gitlab; ", 3))
|
assert(t, data.Details, strings.Repeat("Some error from Gitlab; ", 3))
|
||||||
assert(t, data.Status, http.StatusInternalServerError)
|
assert(t, status, http.StatusInternalServerError)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Handles non-200 from Gitlab", func(t *testing.T) {
|
t.Run("Handles non-200 from Gitlab", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/merge_requests_by_username", testListMrsByUsernamePayload)
|
request := makeRequest(t, http.MethodPost, "/merge_requests_by_username", testListMrsByUsernamePayload)
|
||||||
svc := mergeRequestListerByUsernameService{testProjectData, fakeMergeRequestListerByUsername{testBase: testBase{status: http.StatusSeeOther}}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
mergeRequestListerByUsernameService{testProjectData, fakeMergeRequestListerByUsername{testBase: testBase{status: http.StatusSeeOther}}},
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &MergeRequestByUsernameRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
|
data, status := getFailData(t, svc, request)
|
||||||
assert(t, data.Message, "An error occurred")
|
assert(t, data.Message, "An error occurred")
|
||||||
assert(t, data.Details, strings.Repeat("An error occurred on the /merge_requests_by_username endpoint; ", 3))
|
assert(t, data.Details, strings.Repeat("An error occurred on the /merge_requests_by_username endpoint; ", 3))
|
||||||
assert(t, data.Status, http.StatusInternalServerError)
|
assert(t, status, http.StatusInternalServerError)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
type fakeMergeRequestLister struct {
|
type fakeMergeRequestLister struct {
|
||||||
testBase
|
testBase
|
||||||
emptyResponse bool
|
emptyResponse bool
|
||||||
|
multipleMrs bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f fakeMergeRequestLister) ListProjectMergeRequests(pid interface{}, opt *gitlab.ListProjectMergeRequestsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.MergeRequest, *gitlab.Response, error) {
|
func (f fakeMergeRequestLister) ListProjectMergeRequests(pid interface{}, opt *gitlab.ListProjectMergeRequestsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.MergeRequest, *gitlab.Response, error) {
|
||||||
@@ -22,6 +23,10 @@ func (f fakeMergeRequestLister) ListProjectMergeRequests(pid interface{}, opt *g
|
|||||||
return []*gitlab.MergeRequest{}, resp, err
|
return []*gitlab.MergeRequest{}, resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if f.multipleMrs {
|
||||||
|
return []*gitlab.MergeRequest{{IID: 10}, {IID: 11}}, resp, err
|
||||||
|
}
|
||||||
|
|
||||||
return []*gitlab.MergeRequest{{IID: 10}}, resp, err
|
return []*gitlab.MergeRequest{{IID: 10}}, resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,30 +34,45 @@ func TestMergeRequestHandler(t *testing.T) {
|
|||||||
var testListMergeRequestsRequest = gitlab.ListProjectMergeRequestsOptions{}
|
var testListMergeRequestsRequest = gitlab.ListProjectMergeRequestsOptions{}
|
||||||
t.Run("Should fetch merge requests", func(t *testing.T) {
|
t.Run("Should fetch merge requests", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/merge_requests", testListMergeRequestsRequest)
|
request := makeRequest(t, http.MethodPost, "/merge_requests", testListMergeRequestsRequest)
|
||||||
svc := mergeRequestListerService{testProjectData, fakeMergeRequestLister{}}
|
svc := middleware(
|
||||||
|
mergeRequestListerService{testProjectData, fakeMergeRequestLister{}},
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &gitlab.ListProjectMergeRequestsOptions{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
data := getSuccessData(t, svc, request)
|
data := getSuccessData(t, svc, request)
|
||||||
assert(t, data.Status, http.StatusOK)
|
|
||||||
assert(t, data.Message, "Merge requests fetched successfully")
|
assert(t, data.Message, "Merge requests fetched successfully")
|
||||||
})
|
})
|
||||||
t.Run("Handles error from Gitlab client", func(t *testing.T) {
|
t.Run("Handles error from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/merge_requests", testListMergeRequestsRequest)
|
request := makeRequest(t, http.MethodPost, "/merge_requests", testListMergeRequestsRequest)
|
||||||
svc := mergeRequestListerService{testProjectData, fakeMergeRequestLister{testBase: testBase{errFromGitlab: true}}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
mergeRequestListerService{testProjectData, fakeMergeRequestLister{testBase: testBase{errFromGitlab: true}}},
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &gitlab.ListProjectMergeRequestsOptions{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
|
data, status := getFailData(t, svc, request)
|
||||||
checkErrorFromGitlab(t, data, "Failed to list merge requests")
|
checkErrorFromGitlab(t, data, "Failed to list merge requests")
|
||||||
assert(t, data.Status, http.StatusInternalServerError)
|
assert(t, status, http.StatusInternalServerError)
|
||||||
})
|
})
|
||||||
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/merge_requests", testListMergeRequestsRequest)
|
request := makeRequest(t, http.MethodPost, "/merge_requests", testListMergeRequestsRequest)
|
||||||
svc := mergeRequestListerService{testProjectData, fakeMergeRequestLister{testBase: testBase{status: http.StatusSeeOther}}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
mergeRequestListerService{testProjectData, fakeMergeRequestLister{testBase: testBase{status: http.StatusSeeOther}}},
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &gitlab.ListProjectMergeRequestsOptions{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
|
data, status := getFailData(t, svc, request)
|
||||||
checkNon200(t, data, "Failed to list merge requests", "/merge_requests")
|
checkNon200(t, data, "Failed to list merge requests", "/merge_requests")
|
||||||
assert(t, data.Status, http.StatusSeeOther)
|
assert(t, status, http.StatusSeeOther)
|
||||||
})
|
})
|
||||||
t.Run("Should handle not having any merge requests with 404", func(t *testing.T) {
|
t.Run("Should handle not having any merge requests with 404", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/merge_requests", testListMergeRequestsRequest)
|
request := makeRequest(t, http.MethodPost, "/merge_requests", testListMergeRequestsRequest)
|
||||||
svc := mergeRequestListerService{testProjectData, fakeMergeRequestLister{emptyResponse: true}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
mergeRequestListerService{testProjectData, fakeMergeRequestLister{emptyResponse: true}},
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &gitlab.ListProjectMergeRequestsOptions{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
|
data, status := getFailData(t, svc, request)
|
||||||
assert(t, data.Message, "No merge requests found")
|
assert(t, data.Message, "No merge requests found")
|
||||||
assert(t, data.Status, http.StatusNotFound)
|
assert(t, status, http.StatusNotFound)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
173
cmd/app/middleware.go
Normal file
173
cmd/app/middleware.go
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/xanzy/go-gitlab"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mw func(http.Handler) http.Handler
|
||||||
|
|
||||||
|
type payload string
|
||||||
|
|
||||||
|
// Wraps a series of middleware around the base handler. Functions are called from bottom to top.
|
||||||
|
// The middlewares should call the serveHTTP method on their http.Handler argument to pass along the request.
|
||||||
|
func middleware(h http.Handler, middlewares ...mw) http.HandlerFunc {
|
||||||
|
for _, middleware := range middlewares {
|
||||||
|
h = middleware(h)
|
||||||
|
}
|
||||||
|
return h.ServeHTTP
|
||||||
|
}
|
||||||
|
|
||||||
|
var validate = validator.New()
|
||||||
|
|
||||||
|
type methodToPayload map[string]any
|
||||||
|
|
||||||
|
type validatorMiddleware struct {
|
||||||
|
validate *validator.Validate
|
||||||
|
methodToPayload methodToPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validates the fields in a payload and then attaches the validated payload to the request context so that
|
||||||
|
// subsequent handlers can use it.
|
||||||
|
func (p validatorMiddleware) handle(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
if p.methodToPayload[r.Method] == nil { // If no payload to validate for this method type...
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pl := p.methodToPayload[r.Method]
|
||||||
|
err = json.Unmarshal(body, &pl)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, "Could not parse JSON request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.validate.Struct(pl)
|
||||||
|
if err != nil {
|
||||||
|
switch err := err.(type) {
|
||||||
|
case validator.ValidationErrors:
|
||||||
|
handleError(w, formatValidationErrors(err), "Invalid payload", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
case *validator.InvalidValidationError:
|
||||||
|
handleError(w, err, "Invalid validation error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass the parsed data so we don't have to re-parse it in the handler
|
||||||
|
ctx := context.WithValue(r.Context(), payload(payload("payload")), pl)
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func withPayloadValidation(mtp methodToPayload) mw {
|
||||||
|
return validatorMiddleware{validate: validate, methodToPayload: mtp}.handle
|
||||||
|
}
|
||||||
|
|
||||||
|
type withMrMiddleware struct {
|
||||||
|
data data
|
||||||
|
client MergeRequestLister
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets the current merge request ID and attaches it to the projectInfo
|
||||||
|
func (m withMrMiddleware) handle(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// If the merge request is already attached, skip the middleware logic
|
||||||
|
if m.data.projectInfo.MergeId == 0 {
|
||||||
|
options := gitlab.ListProjectMergeRequestsOptions{
|
||||||
|
Scope: gitlab.Ptr("all"),
|
||||||
|
SourceBranch: &m.data.gitInfo.BranchName,
|
||||||
|
TargetBranch: pluginOptions.ChosenTargetBranch,
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeRequests, _, err := m.client.ListProjectMergeRequests(m.data.projectInfo.ProjectId, &options)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, fmt.Errorf("Failed to list merge requests: %w", err), "Failed to list merge requests", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(mergeRequests) == 0 {
|
||||||
|
err := fmt.Errorf("Branch '%s' does not have any merge requests", m.data.gitInfo.BranchName)
|
||||||
|
handleError(w, err, "No MRs Found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(mergeRequests) > 1 {
|
||||||
|
err := errors.New("Please call gitlab.choose_merge_request()")
|
||||||
|
handleError(w, err, "Multiple MRs found", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeIdInt := mergeRequests[0].IID
|
||||||
|
m.data.projectInfo.MergeId = mergeIdInt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the next handler if middleware succeeds
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Att
|
||||||
|
func withMr(data data, client MergeRequestLister) mw {
|
||||||
|
return withMrMiddleware{data, client}.handle
|
||||||
|
}
|
||||||
|
|
||||||
|
type methodMiddleware struct {
|
||||||
|
methods []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m methodMiddleware) handle(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
method := r.Method
|
||||||
|
for _, acceptableMethod := range m.methods {
|
||||||
|
if method == acceptableMethod {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", http.MethodPut)
|
||||||
|
handleError(w, InvalidRequestError{fmt.Sprintf("Expected: %s", strings.Join(m.methods, "; "))}, "Invalid request type", http.StatusMethodNotAllowed)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func withMethodCheck(methods ...string) mw {
|
||||||
|
return methodMiddleware{methods: methods}.handle
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to format validation errors into more readable strings
|
||||||
|
func formatValidationErrors(errors validator.ValidationErrors) error {
|
||||||
|
var s strings.Builder
|
||||||
|
for i, e := range errors {
|
||||||
|
if i > 0 {
|
||||||
|
s.WriteString("; ")
|
||||||
|
}
|
||||||
|
switch e.Tag() {
|
||||||
|
case "required":
|
||||||
|
s.WriteString(fmt.Sprintf("%s is required", e.Field()))
|
||||||
|
default:
|
||||||
|
s.WriteString(fmt.Sprintf("The field '%s' failed on validation on the '%s' tag", e.Field(), e.Tag()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf(s.String())
|
||||||
|
}
|
||||||
114
cmd/app/middleware_test.go
Normal file
114
cmd/app/middleware_test.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/harrisoncramer/gitlab.nvim/cmd/app/git"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FakePayload struct {
|
||||||
|
Foo string `json:"foo" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeHandler struct{}
|
||||||
|
|
||||||
|
func (f fakeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
data := SuccessResponse{Message: "Some message"}
|
||||||
|
j, _ := json.Marshal(data)
|
||||||
|
w.Write(j) // nolint
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMethodMiddleware(t *testing.T) {
|
||||||
|
t.Run("Fails a bad method", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodGet, "/foo", nil)
|
||||||
|
mw := withMethodCheck(http.MethodPost)
|
||||||
|
handler := middleware(fakeHandler{}, mw)
|
||||||
|
data, status := getFailData(t, handler, request)
|
||||||
|
assert(t, data.Message, "Invalid request type")
|
||||||
|
assert(t, data.Details, "Expected: POST")
|
||||||
|
assert(t, status, http.StatusMethodNotAllowed)
|
||||||
|
})
|
||||||
|
t.Run("Fails bad method with multiple", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodGet, "/foo", nil)
|
||||||
|
mw := withMethodCheck(http.MethodPost, http.MethodPatch)
|
||||||
|
handler := middleware(fakeHandler{}, mw)
|
||||||
|
data, status := getFailData(t, handler, request)
|
||||||
|
assert(t, data.Message, "Invalid request type")
|
||||||
|
assert(t, data.Details, "Expected: POST; PATCH")
|
||||||
|
assert(t, status, http.StatusMethodNotAllowed)
|
||||||
|
})
|
||||||
|
t.Run("Allows ok method through", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodGet, "/foo", nil)
|
||||||
|
mw := withMethodCheck(http.MethodGet)
|
||||||
|
handler := middleware(fakeHandler{}, mw)
|
||||||
|
data := getSuccessData(t, handler, request)
|
||||||
|
assert(t, data.Message, "Some message")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithMrMiddleware(t *testing.T) {
|
||||||
|
t.Run("Loads an MR ID into the projectInfo", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodGet, "/foo", nil)
|
||||||
|
d := data{
|
||||||
|
projectInfo: &ProjectInfo{},
|
||||||
|
gitInfo: &git.GitData{BranchName: "foo"},
|
||||||
|
}
|
||||||
|
mw := withMr(d, fakeMergeRequestLister{})
|
||||||
|
handler := middleware(fakeHandler{}, mw)
|
||||||
|
getSuccessData(t, handler, request)
|
||||||
|
if d.projectInfo.MergeId != 10 {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("Handles when there are no MRs", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodGet, "/foo", nil)
|
||||||
|
d := data{
|
||||||
|
projectInfo: &ProjectInfo{},
|
||||||
|
gitInfo: &git.GitData{BranchName: "foo"},
|
||||||
|
}
|
||||||
|
mw := withMr(d, fakeMergeRequestLister{emptyResponse: true})
|
||||||
|
handler := middleware(fakeHandler{}, mw)
|
||||||
|
data, status := getFailData(t, handler, request)
|
||||||
|
assert(t, status, http.StatusNotFound)
|
||||||
|
assert(t, data.Message, "No MRs Found")
|
||||||
|
assert(t, data.Details, "Branch 'foo' does not have any merge requests")
|
||||||
|
})
|
||||||
|
t.Run("Handles when there are too many MRs", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodGet, "/foo", nil)
|
||||||
|
d := data{
|
||||||
|
projectInfo: &ProjectInfo{},
|
||||||
|
gitInfo: &git.GitData{BranchName: "foo"},
|
||||||
|
}
|
||||||
|
mw := withMr(d, fakeMergeRequestLister{multipleMrs: true})
|
||||||
|
handler := middleware(fakeHandler{}, mw)
|
||||||
|
data, status := getFailData(t, handler, request)
|
||||||
|
assert(t, status, http.StatusBadRequest)
|
||||||
|
assert(t, data.Message, "Multiple MRs found")
|
||||||
|
assert(t, data.Details, "Please call gitlab.choose_merge_request()")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidatorMiddleware(t *testing.T) {
|
||||||
|
t.Run("Should error with missing field", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/foo", FakePayload{}) // No Foo field
|
||||||
|
data, status := getFailData(t, middleware(
|
||||||
|
fakeHandler{},
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &FakePayload{}}),
|
||||||
|
), request)
|
||||||
|
assert(t, data.Message, "Invalid payload")
|
||||||
|
assert(t, data.Details, "Foo is required")
|
||||||
|
assert(t, status, http.StatusBadRequest)
|
||||||
|
})
|
||||||
|
t.Run("Should allow valid payload through", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/foo", FakePayload{Foo: "Some payload"})
|
||||||
|
data := getSuccessData(t, middleware(
|
||||||
|
fakeHandler{},
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &FakePayload{}}),
|
||||||
|
), request)
|
||||||
|
assert(t, data.Message, "Some message")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -43,16 +43,12 @@ type pipelineService struct {
|
|||||||
pipelineHandler fetches information about the current pipeline, and retriggers a pipeline run. For more detailed information
|
pipelineHandler fetches information about the current pipeline, and retriggers a pipeline run. For more detailed information
|
||||||
about a given job in a pipeline, see the jobHandler function
|
about a given job in a pipeline, see the jobHandler function
|
||||||
*/
|
*/
|
||||||
func (a pipelineService) handler(w http.ResponseWriter, r *http.Request) {
|
func (a pipelineService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
a.GetPipelineAndJobs(w, r)
|
a.GetPipelineAndJobs(w, r)
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
a.RetriggerPipeline(w, r)
|
a.RetriggerPipeline(w, r)
|
||||||
default:
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", fmt.Sprintf("%s, %s", http.MethodGet, http.MethodPost))
|
|
||||||
handleError(w, InvalidRequestError{}, "Expected GET or POST", http.StatusMethodNotAllowed)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +96,7 @@ func (a pipelineService) GetPipelineAndJobs(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
|
|
||||||
if pipeline == nil {
|
if pipeline == nil {
|
||||||
handleError(w, GenericError{endpoint: "/pipeline"}, fmt.Sprintf("No pipeline found for %s branch", a.gitInfo.BranchName), http.StatusInternalServerError)
|
handleError(w, GenericError{r.URL.Path}, fmt.Sprintf("No pipeline found for %s branch", a.gitInfo.BranchName), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,16 +108,13 @@ func (a pipelineService) GetPipelineAndJobs(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: "/pipeline"}, "Could not get pipeline jobs", res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, "Could not get pipeline jobs", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response := GetPipelineAndJobsResponse{
|
response := GetPipelineAndJobsResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{Message: "Pipeline retrieved"},
|
||||||
Status: http.StatusOK,
|
|
||||||
Message: "Pipeline retrieved",
|
|
||||||
},
|
|
||||||
Pipeline: PipelineWithJobs{
|
Pipeline: PipelineWithJobs{
|
||||||
LatestPipeline: pipeline,
|
LatestPipeline: pipeline,
|
||||||
Jobs: jobs,
|
Jobs: jobs,
|
||||||
@@ -153,16 +146,13 @@ func (a pipelineService) RetriggerPipeline(w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: "/pipeline"}, "Could not retrigger pipeline", res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, "Could not retrigger pipeline", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response := RetriggerPipelineResponse{
|
response := RetriggerPipelineResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{Message: "Pipeline retriggered"},
|
||||||
Message: "Pipeline retriggered",
|
|
||||||
Status: http.StatusOK,
|
|
||||||
},
|
|
||||||
LatestPipeline: pipeline,
|
LatestPipeline: pipeline,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,27 +38,29 @@ func (f fakePipelineManager) RetryPipelineBuild(pid interface{}, pipeline int, o
|
|||||||
func TestPipelineGetter(t *testing.T) {
|
func TestPipelineGetter(t *testing.T) {
|
||||||
t.Run("Gets all pipeline jobs", func(t *testing.T) {
|
t.Run("Gets all pipeline jobs", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodGet, "/pipeline", nil)
|
request := makeRequest(t, http.MethodGet, "/pipeline", nil)
|
||||||
svc := pipelineService{testProjectData, fakePipelineManager{}, FakeGitManager{}}
|
svc := middleware(
|
||||||
|
pipelineService{testProjectData, fakePipelineManager{}, FakeGitManager{}},
|
||||||
|
withMethodCheck(http.MethodGet),
|
||||||
|
)
|
||||||
data := getSuccessData(t, svc, request)
|
data := getSuccessData(t, svc, request)
|
||||||
assert(t, data.Message, "Pipeline retrieved")
|
assert(t, data.Message, "Pipeline retrieved")
|
||||||
assert(t, data.Status, http.StatusOK)
|
|
||||||
})
|
|
||||||
t.Run("Disallows non-GET, non-POST methods", func(t *testing.T) {
|
|
||||||
request := makeRequest(t, http.MethodPatch, "/pipeline", nil)
|
|
||||||
svc := pipelineService{testProjectData, fakePipelineManager{}, FakeGitManager{}}
|
|
||||||
data := getFailData(t, svc, request)
|
|
||||||
checkBadMethod(t, data, http.MethodGet, http.MethodPost)
|
|
||||||
})
|
})
|
||||||
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodGet, "/pipeline", nil)
|
request := makeRequest(t, http.MethodGet, "/pipeline", nil)
|
||||||
svc := pipelineService{testProjectData, fakePipelineManager{testBase{errFromGitlab: true}}, FakeGitManager{}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
pipelineService{testProjectData, fakePipelineManager{testBase{errFromGitlab: true}}, FakeGitManager{}},
|
||||||
|
withMethodCheck(http.MethodGet),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
checkErrorFromGitlab(t, data, "Failed to get latest pipeline for some-branch branch")
|
checkErrorFromGitlab(t, data, "Failed to get latest pipeline for some-branch branch")
|
||||||
})
|
})
|
||||||
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodGet, "/pipeline", nil)
|
request := makeRequest(t, http.MethodGet, "/pipeline", nil)
|
||||||
svc := pipelineService{testProjectData, fakePipelineManager{testBase: testBase{status: http.StatusSeeOther}}, FakeGitManager{}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
pipelineService{testProjectData, fakePipelineManager{testBase{status: http.StatusSeeOther}}, FakeGitManager{}},
|
||||||
|
withMethodCheck(http.MethodGet),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
assert(t, data.Message, "Failed to get latest pipeline for some-branch branch") // Expected, we treat this as an error
|
assert(t, data.Message, "Failed to get latest pipeline for some-branch branch") // Expected, we treat this as an error
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -66,21 +68,29 @@ func TestPipelineGetter(t *testing.T) {
|
|||||||
func TestPipelineTrigger(t *testing.T) {
|
func TestPipelineTrigger(t *testing.T) {
|
||||||
t.Run("Retriggers pipeline", func(t *testing.T) {
|
t.Run("Retriggers pipeline", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/pipeline/trigger/3", nil)
|
request := makeRequest(t, http.MethodPost, "/pipeline/trigger/3", nil)
|
||||||
svc := pipelineService{testProjectData, fakePipelineManager{}, FakeGitManager{}}
|
svc := middleware(
|
||||||
|
pipelineService{testProjectData, fakePipelineManager{}, FakeGitManager{}},
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
data := getSuccessData(t, svc, request)
|
data := getSuccessData(t, svc, request)
|
||||||
assert(t, data.Message, "Pipeline retriggered")
|
assert(t, data.Message, "Pipeline retriggered")
|
||||||
assert(t, data.Status, http.StatusOK)
|
|
||||||
})
|
})
|
||||||
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/pipeline/trigger/3", nil)
|
request := makeRequest(t, http.MethodPost, "/pipeline/trigger/3", nil)
|
||||||
svc := pipelineService{testProjectData, fakePipelineManager{testBase{errFromGitlab: true}}, FakeGitManager{}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
pipelineService{testProjectData, fakePipelineManager{testBase{errFromGitlab: true}}, FakeGitManager{}},
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
checkErrorFromGitlab(t, data, "Could not retrigger pipeline")
|
checkErrorFromGitlab(t, data, "Could not retrigger pipeline")
|
||||||
})
|
})
|
||||||
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/pipeline/trigger/3", nil)
|
request := makeRequest(t, http.MethodPost, "/pipeline/trigger/3", nil)
|
||||||
svc := pipelineService{testProjectData, fakePipelineManager{testBase: testBase{status: http.StatusSeeOther}}, FakeGitManager{}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
pipelineService{testProjectData, fakePipelineManager{testBase{status: http.StatusSeeOther}}, FakeGitManager{}},
|
||||||
checkNon200(t, data, "Could not retrigger pipeline", "/pipeline")
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
|
checkNon200(t, data, "Could not retrigger pipeline", "/pipeline/trigger/3")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -10,8 +9,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ReplyRequest struct {
|
type ReplyRequest struct {
|
||||||
DiscussionId string `json:"discussion_id"`
|
DiscussionId string `json:"discussion_id" validate:"required"`
|
||||||
Reply string `json:"reply"`
|
Reply string `json:"reply" validate:"required"`
|
||||||
IsDraft bool `json:"is_draft"`
|
IsDraft bool `json:"is_draft"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,28 +29,8 @@ type replyService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* replyHandler sends a reply to a note or comment */
|
/* replyHandler sends a reply to a note or comment */
|
||||||
func (a replyService) handler(w http.ResponseWriter, r *http.Request) {
|
func (a replyService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
replyRequest := r.Context().Value(payload("payload")).(*ReplyRequest)
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", http.MethodPost)
|
|
||||||
handleError(w, InvalidRequestError{}, "Expected POST", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer r.Body.Close()
|
|
||||||
var replyRequest ReplyRequest
|
|
||||||
err = json.Unmarshal(body, &replyRequest)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
options := gitlab.AddMergeRequestDiscussionNoteOptions{
|
options := gitlab.AddMergeRequestDiscussionNoteOptions{
|
||||||
@@ -67,16 +46,13 @@ func (a replyService) handler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: "/mr/reply"}, "Could not leave reply", res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, "Could not leave reply", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response := ReplyResponse{
|
response := ReplyResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{Message: "Replied to comment"},
|
||||||
Message: "Replied to comment",
|
|
||||||
Status: http.StatusOK,
|
|
||||||
},
|
|
||||||
Note: note,
|
Note: note,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,22 +24,36 @@ func TestReplyHandler(t *testing.T) {
|
|||||||
var testReplyRequest = ReplyRequest{DiscussionId: "abc123", Reply: "Some Reply", IsDraft: false}
|
var testReplyRequest = ReplyRequest{DiscussionId: "abc123", Reply: "Some Reply", IsDraft: false}
|
||||||
t.Run("Sends a reply", func(t *testing.T) {
|
t.Run("Sends a reply", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/reply", testReplyRequest)
|
request := makeRequest(t, http.MethodPost, "/mr/reply", testReplyRequest)
|
||||||
svc := replyService{testProjectData, fakeReplyManager{}}
|
svc := middleware(
|
||||||
|
replyService{testProjectData, fakeReplyManager{}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &ReplyRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
data := getSuccessData(t, svc, request)
|
data := getSuccessData(t, svc, request)
|
||||||
assert(t, data.Message, "Replied to comment")
|
assert(t, data.Message, "Replied to comment")
|
||||||
assert(t, data.Status, http.StatusOK)
|
|
||||||
})
|
})
|
||||||
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/reply", testReplyRequest)
|
request := makeRequest(t, http.MethodPost, "/mr/reply", testReplyRequest)
|
||||||
svc := replyService{testProjectData, fakeReplyManager{testBase{errFromGitlab: true}}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
replyService{testProjectData, fakeReplyManager{testBase{errFromGitlab: true}}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &ReplyRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
checkErrorFromGitlab(t, data, "Could not leave reply")
|
checkErrorFromGitlab(t, data, "Could not leave reply")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/reply", testReplyRequest)
|
request := makeRequest(t, http.MethodPost, "/mr/reply", testReplyRequest)
|
||||||
svc := replyService{testProjectData, fakeReplyManager{testBase{status: http.StatusSeeOther}}}
|
svc := middleware(
|
||||||
data := getFailData(t, svc, request)
|
replyService{testProjectData, fakeReplyManager{testBase{status: http.StatusSeeOther}}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &ReplyRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
)
|
||||||
|
data, _ := getFailData(t, svc, request)
|
||||||
checkNon200(t, data, "Could not leave reply", "/mr/reply")
|
checkNon200(t, data, "Could not leave reply", "/mr/reply")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,11 @@ package app
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/xanzy/go-gitlab"
|
"github.com/xanzy/go-gitlab"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DiscussionResolveRequest struct {
|
|
||||||
DiscussionID string `json:"discussion_id"`
|
|
||||||
Resolved bool `json:"resolved"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DiscussionResolver interface {
|
type DiscussionResolver interface {
|
||||||
ResolveMergeRequestDiscussion(pid interface{}, mergeRequest int, discussion string, opt *gitlab.ResolveMergeRequestDiscussionOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Discussion, *gitlab.Response, error)
|
ResolveMergeRequestDiscussion(pid interface{}, mergeRequest int, discussion string, opt *gitlab.ResolveMergeRequestDiscussionOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Discussion, *gitlab.Response, error)
|
||||||
}
|
}
|
||||||
@@ -23,40 +17,24 @@ type discussionsResolutionService struct {
|
|||||||
client DiscussionResolver
|
client DiscussionResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DiscussionResolveRequest struct {
|
||||||
|
DiscussionID string `json:"discussion_id" validate:"required"`
|
||||||
|
Resolved bool `json:"resolved"`
|
||||||
|
}
|
||||||
|
|
||||||
/* discussionsResolveHandler sets a discussion to be "resolved" or not resolved, depending on the payload */
|
/* discussionsResolveHandler sets a discussion to be "resolved" or not resolved, depending on the payload */
|
||||||
func (a discussionsResolutionService) handler(w http.ResponseWriter, r *http.Request) {
|
func (a discussionsResolutionService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
payload := r.Context().Value(payload("payload")).(*DiscussionResolveRequest)
|
||||||
if r.Method != http.MethodPut {
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", http.MethodPut)
|
|
||||||
handleError(w, InvalidRequestError{}, "Expected PUT", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
var resolveDiscussionRequest DiscussionResolveRequest
|
|
||||||
err = json.Unmarshal(body, &resolveDiscussionRequest)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, res, err := a.client.ResolveMergeRequestDiscussion(
|
_, res, err := a.client.ResolveMergeRequestDiscussion(
|
||||||
a.projectInfo.ProjectId,
|
a.projectInfo.ProjectId,
|
||||||
a.projectInfo.MergeId,
|
a.projectInfo.MergeId,
|
||||||
resolveDiscussionRequest.DiscussionID,
|
payload.DiscussionID,
|
||||||
&gitlab.ResolveMergeRequestDiscussionOptions{Resolved: &resolveDiscussionRequest.Resolved},
|
&gitlab.ResolveMergeRequestDiscussionOptions{Resolved: &payload.Resolved},
|
||||||
)
|
)
|
||||||
|
|
||||||
friendlyName := "unresolve"
|
friendlyName := "unresolve"
|
||||||
if resolveDiscussionRequest.Resolved {
|
if payload.Resolved {
|
||||||
friendlyName = "resolve"
|
friendlyName = "resolve"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,15 +44,12 @@ func (a discussionsResolutionService) handler(w http.ResponseWriter, r *http.Req
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: "/mr/discussions/resolve"}, fmt.Sprintf("Could not %s discussion", friendlyName), res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, fmt.Sprintf("Could not %s discussion", friendlyName), res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response := SuccessResponse{
|
response := SuccessResponse{Message: fmt.Sprintf("Discussion %sd", friendlyName)}
|
||||||
Message: fmt.Sprintf("Discussion %sd", friendlyName),
|
|
||||||
Status: http.StatusOK,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err = json.NewEncoder(w).Encode(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
84
cmd/app/resolve_discussion_test.go
Normal file
84
cmd/app/resolve_discussion_test.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/xanzy/go-gitlab"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakeDiscussionResolver struct {
|
||||||
|
testBase
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeDiscussionResolver) ResolveMergeRequestDiscussion(pid interface{}, mergeRequest int, discussion string, opt *gitlab.ResolveMergeRequestDiscussionOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Discussion, *gitlab.Response, error) {
|
||||||
|
resp, err := f.handleGitlabError()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &gitlab.Discussion{}, resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveDiscussion(t *testing.T) {
|
||||||
|
var testResolveMergeRequestPayload = DiscussionResolveRequest{
|
||||||
|
DiscussionID: "abc123",
|
||||||
|
Resolved: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Resolves a discussion", func(t *testing.T) {
|
||||||
|
svc := middleware(
|
||||||
|
discussionsResolutionService{testProjectData, fakeDiscussionResolver{}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPut: &DiscussionResolveRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPut),
|
||||||
|
)
|
||||||
|
request := makeRequest(t, http.MethodPut, "/mr/discussions/resolve", testResolveMergeRequestPayload)
|
||||||
|
data := getSuccessData(t, svc, request)
|
||||||
|
assert(t, data.Message, "Discussion resolved")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Unresolves a discussion", func(t *testing.T) {
|
||||||
|
payload := testResolveMergeRequestPayload
|
||||||
|
payload.Resolved = false
|
||||||
|
svc := middleware(
|
||||||
|
discussionsResolutionService{testProjectData, fakeDiscussionResolver{}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPut: &DiscussionResolveRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPut),
|
||||||
|
)
|
||||||
|
request := makeRequest(t, http.MethodPut, "/mr/discussions/resolve", payload)
|
||||||
|
data := getSuccessData(t, svc, request)
|
||||||
|
assert(t, data.Message, "Discussion unresolved")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Requires a discussion ID", func(t *testing.T) {
|
||||||
|
payload := testResolveMergeRequestPayload
|
||||||
|
payload.DiscussionID = ""
|
||||||
|
svc := middleware(
|
||||||
|
discussionsResolutionService{testProjectData, fakeDiscussionResolver{}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPut: &DiscussionResolveRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPut),
|
||||||
|
)
|
||||||
|
request := makeRequest(t, http.MethodPut, "/mr/discussions/resolve", payload)
|
||||||
|
data, status := getFailData(t, svc, request)
|
||||||
|
assert(t, data.Message, "Invalid payload")
|
||||||
|
assert(t, data.Details, "DiscussionID is required")
|
||||||
|
assert(t, status, http.StatusBadRequest)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles error from Gitlab", func(t *testing.T) {
|
||||||
|
svc := middleware(
|
||||||
|
discussionsResolutionService{testProjectData, fakeDiscussionResolver{testBase: testBase{errFromGitlab: true}}},
|
||||||
|
withMr(testProjectData, fakeMergeRequestLister{}),
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPut: &DiscussionResolveRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPut),
|
||||||
|
)
|
||||||
|
request := makeRequest(t, http.MethodPut, "/mr/discussions/resolve", testResolveMergeRequestPayload)
|
||||||
|
data, status := getFailData(t, svc, request)
|
||||||
|
assert(t, data.Message, "Could not resolve discussion")
|
||||||
|
assert(t, data.Details, "Some error from Gitlab")
|
||||||
|
assert(t, status, http.StatusInternalServerError)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -7,12 +7,10 @@ import (
|
|||||||
type ErrorResponse struct {
|
type ErrorResponse struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Details string `json:"details"`
|
Details string `json:"details"`
|
||||||
Status int `json:"status"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SuccessResponse struct {
|
type SuccessResponse struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Status int `json:"status"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type GenericError struct {
|
type GenericError struct {
|
||||||
@@ -23,8 +21,8 @@ func (e GenericError) Error() string {
|
|||||||
return fmt.Sprintf("An error occurred on the %s endpoint", e.endpoint)
|
return fmt.Sprintf("An error occurred on the %s endpoint", e.endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
type InvalidRequestError struct{}
|
type InvalidRequestError struct{ msg string }
|
||||||
|
|
||||||
func (e InvalidRequestError) Error() string {
|
func (e InvalidRequestError) Error() string {
|
||||||
return "Invalid request type"
|
return e.msg
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,13 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/xanzy/go-gitlab"
|
"github.com/xanzy/go-gitlab"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ReviewerUpdateRequest struct {
|
type ReviewerUpdateRequest struct {
|
||||||
Ids []int `json:"ids"`
|
Ids []int `json:"ids" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReviewerUpdateResponse struct {
|
type ReviewerUpdateResponse struct {
|
||||||
@@ -32,31 +31,11 @@ type reviewerService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* reviewersHandler adds or removes reviewers from an MR */
|
/* reviewersHandler adds or removes reviewers from an MR */
|
||||||
func (a reviewerService) handler(w http.ResponseWriter, r *http.Request) {
|
func (a reviewerService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
payload := r.Context().Value(payload("payload")).(*ReviewerUpdateRequest)
|
||||||
if r.Method != http.MethodPut {
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", http.MethodPut)
|
|
||||||
handleError(w, InvalidRequestError{}, "Expected PUT", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer r.Body.Close()
|
|
||||||
var reviewerUpdateRequest ReviewerUpdateRequest
|
|
||||||
err = json.Unmarshal(body, &reviewerUpdateRequest)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mr, res, err := a.client.UpdateMergeRequest(a.projectInfo.ProjectId, a.projectInfo.MergeId, &gitlab.UpdateMergeRequestOptions{
|
mr, res, err := a.client.UpdateMergeRequest(a.projectInfo.ProjectId, a.projectInfo.MergeId, &gitlab.UpdateMergeRequestOptions{
|
||||||
ReviewerIDs: &reviewerUpdateRequest.Ids,
|
ReviewerIDs: &payload.Ids,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -65,16 +44,13 @@ func (a reviewerService) handler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: "/mr/reviewer"}, "Could not modify merge request reviewers", res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, "Could not modify merge request reviewers", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response := ReviewerUpdateResponse{
|
response := ReviewerUpdateResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{Message: "Reviewers updated"},
|
||||||
Message: "Reviewers updated",
|
|
||||||
Status: http.StatusOK,
|
|
||||||
},
|
|
||||||
Reviewers: mr.Reviewers,
|
Reviewers: mr.Reviewers,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,13 +25,7 @@ type revisionsService struct {
|
|||||||
revisionsHandler gets revision information about the current MR. This data is not used directly but is
|
revisionsHandler gets revision information about the current MR. This data is not used directly but is
|
||||||
a precursor API call for other functionality
|
a precursor API call for other functionality
|
||||||
*/
|
*/
|
||||||
func (a revisionsService) handler(w http.ResponseWriter, r *http.Request) {
|
func (a revisionsService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
if r.Method != http.MethodGet {
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", http.MethodGet)
|
|
||||||
handleError(w, InvalidRequestError{}, "Expected GET", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
versionInfo, res, err := a.client.GetMergeRequestDiffVersions(a.projectInfo.ProjectId, a.projectInfo.MergeId, &gitlab.GetMergeRequestDiffVersionsOptions{})
|
versionInfo, res, err := a.client.GetMergeRequestDiffVersions(a.projectInfo.ProjectId, a.projectInfo.MergeId, &gitlab.GetMergeRequestDiffVersionsOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -40,16 +34,13 @@ func (a revisionsService) handler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: "/mr/revisions"}, "Could not get diff version info", res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, "Could not get diff version info", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response := RevisionsResponse{
|
response := RevisionsResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{Message: "Revisions fetched successfully"},
|
||||||
Message: "Revisions fetched successfully",
|
|
||||||
Status: http.StatusOK,
|
|
||||||
},
|
|
||||||
Revisions: versionInfo,
|
Revisions: versionInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,13 +17,7 @@ type mergeRequestRevokerService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* revokeHandler revokes approval for the current merge request */
|
/* revokeHandler revokes approval for the current merge request */
|
||||||
func (a mergeRequestRevokerService) handler(w http.ResponseWriter, r *http.Request) {
|
func (a mergeRequestRevokerService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", http.MethodPost)
|
|
||||||
handleError(w, InvalidRequestError{}, "Expected POST", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := a.client.UnapproveMergeRequest(a.projectInfo.ProjectId, a.projectInfo.MergeId, nil, nil)
|
res, err := a.client.UnapproveMergeRequest(a.projectInfo.ProjectId, a.projectInfo.MergeId, nil, nil)
|
||||||
|
|
||||||
@@ -33,15 +27,12 @@ func (a mergeRequestRevokerService) handler(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: "/mr/revoke"}, "Could not revoke approval", res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, "Could not revoke approval", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response := SuccessResponse{
|
response := SuccessResponse{Message: "Success! Revoked MR approval"}
|
||||||
Message: "Success! Revoked MR approval",
|
|
||||||
Status: http.StatusOK,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err = json.NewEncoder(w).Encode(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ type data struct {
|
|||||||
|
|
||||||
type optFunc func(a *data) error
|
type optFunc func(a *data) error
|
||||||
|
|
||||||
func CreateRouter(gitlabClient *Client, projectInfo *ProjectInfo, s ShutdownHandler, optFuncs ...optFunc) *http.ServeMux {
|
func CreateRouter(gitlabClient *Client, projectInfo *ProjectInfo, s ShutdownHandler, optFuncs ...optFunc) http.Handler {
|
||||||
m := http.NewServeMux()
|
m := http.NewServeMux()
|
||||||
|
|
||||||
d := data{
|
d := data{
|
||||||
@@ -92,37 +92,149 @@ func CreateRouter(gitlabClient *Client, projectInfo *ProjectInfo, s ShutdownHand
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m.HandleFunc("/mr/approve", withMr(mergeRequestApproverService{d, gitlabClient}, d, gitlabClient))
|
m.HandleFunc("/mr/approve", middleware(
|
||||||
m.HandleFunc("/mr/comment", withMr(commentService{d, gitlabClient}, d, gitlabClient))
|
mergeRequestApproverService{d, gitlabClient}, // These functions are called from bottom to top...
|
||||||
m.HandleFunc("/mr/merge", withMr(mergeRequestAccepterService{d, gitlabClient}, d, gitlabClient))
|
withMr(d, gitlabClient),
|
||||||
m.HandleFunc("/mr/discussions/list", withMr(discussionsListerService{d, gitlabClient}, d, gitlabClient))
|
withMethodCheck(http.MethodPost),
|
||||||
m.HandleFunc("/mr/discussions/resolve", withMr(discussionsResolutionService{d, gitlabClient}, d, gitlabClient))
|
))
|
||||||
m.HandleFunc("/mr/info", withMr(infoService{d, gitlabClient}, d, gitlabClient))
|
m.HandleFunc("/mr/comment", middleware(
|
||||||
m.HandleFunc("/mr/assignee", withMr(assigneesService{d, gitlabClient}, d, gitlabClient))
|
commentService{d, gitlabClient},
|
||||||
m.HandleFunc("/mr/summary", withMr(summaryService{d, gitlabClient}, d, gitlabClient))
|
withMr(d, gitlabClient),
|
||||||
m.HandleFunc("/mr/reviewer", withMr(reviewerService{d, gitlabClient}, d, gitlabClient))
|
withPayloadValidation(methodToPayload{
|
||||||
m.HandleFunc("/mr/revisions", withMr(revisionsService{d, gitlabClient}, d, gitlabClient))
|
http.MethodPost: &PostCommentRequest{},
|
||||||
m.HandleFunc("/mr/reply", withMr(replyService{d, gitlabClient}, d, gitlabClient))
|
http.MethodDelete: &DeleteCommentRequest{},
|
||||||
m.HandleFunc("/mr/label", withMr(labelService{d, gitlabClient}, d, gitlabClient))
|
http.MethodPatch: &EditCommentRequest{},
|
||||||
m.HandleFunc("/mr/revoke", withMr(mergeRequestRevokerService{d, gitlabClient}, d, gitlabClient))
|
}),
|
||||||
m.HandleFunc("/mr/awardable/note/", withMr(emojiService{d, gitlabClient}, d, gitlabClient))
|
withMethodCheck(http.MethodPost, http.MethodDelete, http.MethodPatch),
|
||||||
m.HandleFunc("/mr/draft_notes/", withMr(draftNoteService{d, gitlabClient}, d, gitlabClient))
|
))
|
||||||
m.HandleFunc("/mr/draft_notes/publish", withMr(draftNotePublisherService{d, gitlabClient}, d, gitlabClient))
|
m.HandleFunc("/mr/merge", middleware(
|
||||||
|
mergeRequestAccepterService{d, gitlabClient},
|
||||||
|
withMr(d, gitlabClient),
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &AcceptMergeRequestRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
))
|
||||||
|
m.HandleFunc("/mr/discussions/list", middleware(
|
||||||
|
discussionsListerService{d, gitlabClient},
|
||||||
|
withMr(d, gitlabClient),
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &DiscussionsRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
))
|
||||||
|
m.HandleFunc("/mr/discussions/resolve", middleware(
|
||||||
|
discussionsResolutionService{d, gitlabClient},
|
||||||
|
withMr(d, gitlabClient),
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPut: &DiscussionResolveRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPut),
|
||||||
|
))
|
||||||
|
m.HandleFunc("/mr/info", middleware(
|
||||||
|
infoService{d, gitlabClient},
|
||||||
|
withMr(d, gitlabClient),
|
||||||
|
withMethodCheck(http.MethodGet),
|
||||||
|
))
|
||||||
|
m.HandleFunc("/mr/assignee", middleware(
|
||||||
|
assigneesService{d, gitlabClient},
|
||||||
|
withMr(d, gitlabClient),
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPut: &AssigneeUpdateRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPut),
|
||||||
|
))
|
||||||
|
m.HandleFunc("/mr/summary", middleware(
|
||||||
|
summaryService{d, gitlabClient},
|
||||||
|
withMr(d, gitlabClient),
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPut: &SummaryUpdateRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPut),
|
||||||
|
))
|
||||||
|
m.HandleFunc("/mr/reviewer", middleware(
|
||||||
|
reviewerService{d, gitlabClient},
|
||||||
|
withMr(d, gitlabClient),
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPut: &ReviewerUpdateRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPut),
|
||||||
|
))
|
||||||
|
m.HandleFunc("/mr/revisions", middleware(
|
||||||
|
revisionsService{d, gitlabClient},
|
||||||
|
withMr(d, gitlabClient),
|
||||||
|
withMethodCheck(http.MethodGet),
|
||||||
|
))
|
||||||
|
m.HandleFunc("/mr/reply", middleware(
|
||||||
|
replyService{d, gitlabClient},
|
||||||
|
withMr(d, gitlabClient),
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &ReplyRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
))
|
||||||
|
m.HandleFunc("/mr/label", middleware(
|
||||||
|
labelService{d, gitlabClient},
|
||||||
|
withMr(d, gitlabClient),
|
||||||
|
))
|
||||||
|
m.HandleFunc("/mr/revoke", middleware(
|
||||||
|
mergeRequestRevokerService{d, gitlabClient},
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
withMr(d, gitlabClient),
|
||||||
|
))
|
||||||
|
m.HandleFunc("/mr/awardable/note/", middleware(
|
||||||
|
emojiService{d, gitlabClient},
|
||||||
|
withMethodCheck(http.MethodPost, http.MethodDelete),
|
||||||
|
withMr(d, gitlabClient),
|
||||||
|
))
|
||||||
|
m.HandleFunc("/mr/draft_notes/", middleware(
|
||||||
|
draftNoteService{d, gitlabClient},
|
||||||
|
withMr(d, gitlabClient),
|
||||||
|
withPayloadValidation(methodToPayload{
|
||||||
|
http.MethodPost: &PostDraftNoteRequest{},
|
||||||
|
http.MethodPatch: &UpdateDraftNoteRequest{},
|
||||||
|
}),
|
||||||
|
withMethodCheck(http.MethodGet, http.MethodPost, http.MethodPatch, http.MethodDelete),
|
||||||
|
))
|
||||||
|
m.HandleFunc("/mr/draft_notes/publish", middleware(
|
||||||
|
draftNotePublisherService{d, gitlabClient},
|
||||||
|
withMr(d, gitlabClient),
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &DraftNotePublishRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
))
|
||||||
|
|
||||||
m.HandleFunc("/pipeline", pipelineService{d, gitlabClient, git.Git{}}.handler)
|
m.HandleFunc("/pipeline", middleware(
|
||||||
m.HandleFunc("/pipeline/trigger/", pipelineService{d, gitlabClient, git.Git{}}.handler)
|
pipelineService{d, gitlabClient, git.Git{}},
|
||||||
m.HandleFunc("/users/me", meService{d, gitlabClient}.handler)
|
withMethodCheck(http.MethodGet),
|
||||||
m.HandleFunc("/attachment", attachmentService{data: d, client: gitlabClient, fileReader: attachmentReader{}}.handler)
|
))
|
||||||
m.HandleFunc("/create_mr", mergeRequestCreatorService{d, gitlabClient}.handler)
|
m.HandleFunc("/pipeline/trigger/", middleware(
|
||||||
m.HandleFunc("/job", traceFileService{d, gitlabClient}.handler)
|
pipelineService{d, gitlabClient, git.Git{}},
|
||||||
m.HandleFunc("/project/members", projectMemberService{d, gitlabClient}.handler)
|
withMethodCheck(http.MethodPost),
|
||||||
m.HandleFunc("/merge_requests", mergeRequestListerService{d, gitlabClient}.handler)
|
))
|
||||||
m.HandleFunc("/merge_requests_by_username", mergeRequestListerByUsernameService{d, gitlabClient}.handler)
|
m.HandleFunc("/users/me", middleware(
|
||||||
|
meService{d, gitlabClient},
|
||||||
|
withMethodCheck(http.MethodGet),
|
||||||
|
))
|
||||||
|
m.HandleFunc("/attachment", middleware(
|
||||||
|
attachmentService{data: d, client: gitlabClient, fileReader: attachmentReader{}},
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &AttachmentRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
))
|
||||||
|
m.HandleFunc("/create_mr", middleware(
|
||||||
|
mergeRequestCreatorService{d, gitlabClient},
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &CreateMrRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
))
|
||||||
|
m.HandleFunc("/job", middleware(
|
||||||
|
traceFileService{d, gitlabClient},
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodGet: &JobTraceRequest{}}),
|
||||||
|
withMethodCheck(http.MethodGet),
|
||||||
|
))
|
||||||
|
m.HandleFunc("/project/members", middleware(
|
||||||
|
projectMemberService{d, gitlabClient},
|
||||||
|
withMethodCheck(http.MethodGet),
|
||||||
|
))
|
||||||
|
m.HandleFunc("/merge_requests", middleware(
|
||||||
|
mergeRequestListerService{d, gitlabClient},
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &gitlab.ListProjectMergeRequestsOptions{}}), // TODO: How to validate external object
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
))
|
||||||
|
m.HandleFunc("/merge_requests_by_username", middleware(
|
||||||
|
mergeRequestListerByUsernameService{d, gitlabClient},
|
||||||
|
withPayloadValidation(methodToPayload{http.MethodPost: &MergeRequestByUsernameRequest{}}),
|
||||||
|
withMethodCheck(http.MethodPost),
|
||||||
|
))
|
||||||
|
|
||||||
m.HandleFunc("/shutdown", s.shutdownHandler)
|
m.HandleFunc("/shutdown", s.shutdownHandler)
|
||||||
m.Handle("/ping", http.HandlerFunc(pingHandler))
|
m.Handle("/ping", http.HandlerFunc(pingHandler))
|
||||||
|
|
||||||
return m
|
return LoggingServer{handler: m}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Used to check whether the server has started yet */
|
/* Used to check whether the server has started yet */
|
||||||
@@ -155,45 +267,3 @@ func createListener() (l net.Listener) {
|
|||||||
|
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServiceWithHandler interface {
|
|
||||||
handler(http.ResponseWriter, *http.Request)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* withMr is a Middlware that gets the current merge request ID and attaches it to the projectInfo */
|
|
||||||
func withMr(svc ServiceWithHandler, c data, client MergeRequestLister) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// If the merge request is already attached, skip the middleware logic
|
|
||||||
if c.projectInfo.MergeId == 0 {
|
|
||||||
options := gitlab.ListProjectMergeRequestsOptions{
|
|
||||||
Scope: gitlab.Ptr("all"),
|
|
||||||
SourceBranch: &c.gitInfo.BranchName,
|
|
||||||
TargetBranch: pluginOptions.ChosenTargetBranch,
|
|
||||||
}
|
|
||||||
|
|
||||||
mergeRequests, _, err := client.ListProjectMergeRequests(c.projectInfo.ProjectId, &options)
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, fmt.Errorf("Failed to list merge requests: %w", err), "Failed to list merge requests", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(mergeRequests) == 0 {
|
|
||||||
err := fmt.Errorf("No merge requests found for branch '%s'", c.gitInfo.BranchName)
|
|
||||||
handleError(w, err, "No merge requests found", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(mergeRequests) > 1 {
|
|
||||||
err := errors.New("Please call gitlab.choose_merge_request()")
|
|
||||||
handleError(w, err, "Multiple MRs found", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mergeIdInt := mergeRequests[0].IID
|
|
||||||
c.projectInfo.MergeId = mergeIdInt
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call the next handler if middleware succeeds
|
|
||||||
svc.handler(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -69,10 +69,7 @@ func (s shutdown) shutdownHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response := SuccessResponse{
|
response := SuccessResponse{Message: text}
|
||||||
Message: text,
|
|
||||||
Status: http.StatusOK,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err = json.NewEncoder(w).Encode(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/xanzy/go-gitlab"
|
"github.com/xanzy/go-gitlab"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SummaryUpdateRequest struct {
|
type SummaryUpdateRequest struct {
|
||||||
|
Title string `json:"title" validate:"required"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Title string `json:"title"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SummaryUpdateResponse struct {
|
type SummaryUpdateResponse struct {
|
||||||
@@ -23,33 +22,13 @@ type summaryService struct {
|
|||||||
client MergeRequestUpdater
|
client MergeRequestUpdater
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a summaryService) handler(w http.ResponseWriter, r *http.Request) {
|
func (a summaryService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
if r.Method != http.MethodPut {
|
payload := r.Context().Value(payload("payload")).(*SummaryUpdateRequest)
|
||||||
w.Header().Set("Access-Control-Allow-Methods", http.MethodPut)
|
|
||||||
handleError(w, InvalidRequestError{}, "Expected PUT", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer r.Body.Close()
|
|
||||||
var SummaryUpdateRequest SummaryUpdateRequest
|
|
||||||
err = json.Unmarshal(body, &SummaryUpdateRequest)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mr, res, err := a.client.UpdateMergeRequest(a.projectInfo.ProjectId, a.projectInfo.MergeId, &gitlab.UpdateMergeRequestOptions{
|
mr, res, err := a.client.UpdateMergeRequest(a.projectInfo.ProjectId, a.projectInfo.MergeId, &gitlab.UpdateMergeRequestOptions{
|
||||||
Description: &SummaryUpdateRequest.Description,
|
Description: &payload.Description,
|
||||||
Title: &SummaryUpdateRequest.Title,
|
Title: &payload.Title,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -58,17 +37,14 @@ func (a summaryService) handler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode >= 300 {
|
if res.StatusCode >= 300 {
|
||||||
handleError(w, GenericError{endpoint: "/summary"}, "Could not edit merge request summary", res.StatusCode)
|
handleError(w, GenericError{r.URL.Path}, "Could not edit merge request summary", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
response := SummaryUpdateResponse{
|
response := SummaryUpdateResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{Message: "Summary updated"},
|
||||||
Message: "Summary updated",
|
|
||||||
Status: http.StatusOK,
|
|
||||||
},
|
|
||||||
MergeRequest: mr,
|
MergeRequest: mr,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/harrisoncramer/gitlab.nvim/cmd/app/git"
|
"github.com/harrisoncramer/gitlab.nvim/cmd/app/git"
|
||||||
@@ -63,9 +62,9 @@ var testProjectData = data{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSuccessData(t *testing.T, svc ServiceWithHandler, request *http.Request) SuccessResponse {
|
func getSuccessData(t *testing.T, svc http.Handler, request *http.Request) SuccessResponse {
|
||||||
res := httptest.NewRecorder()
|
res := httptest.NewRecorder()
|
||||||
svc.handler(res, request)
|
svc.ServeHTTP(res, request)
|
||||||
|
|
||||||
var data SuccessResponse
|
var data SuccessResponse
|
||||||
err := json.Unmarshal(res.Body.Bytes(), &data)
|
err := json.Unmarshal(res.Body.Bytes(), &data)
|
||||||
@@ -75,16 +74,16 @@ func getSuccessData(t *testing.T, svc ServiceWithHandler, request *http.Request)
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFailData(t *testing.T, svc ServiceWithHandler, request *http.Request) ErrorResponse {
|
func getFailData(t *testing.T, svc http.Handler, request *http.Request) (errResponse ErrorResponse, status int) {
|
||||||
res := httptest.NewRecorder()
|
res := httptest.NewRecorder()
|
||||||
svc.handler(res, request)
|
svc.ServeHTTP(res, request)
|
||||||
|
|
||||||
var data ErrorResponse
|
var data ErrorResponse
|
||||||
err := json.Unmarshal(res.Body.Bytes(), &data)
|
err := json.Unmarshal(res.Body.Bytes(), &data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
return data
|
return data, res.Result().StatusCode
|
||||||
}
|
}
|
||||||
|
|
||||||
type testBase struct {
|
type testBase struct {
|
||||||
@@ -105,22 +104,12 @@ func (f *testBase) handleGitlabError() (*gitlab.Response, error) {
|
|||||||
|
|
||||||
func checkErrorFromGitlab(t *testing.T, data ErrorResponse, msg string) {
|
func checkErrorFromGitlab(t *testing.T, data ErrorResponse, msg string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
assert(t, data.Status, http.StatusInternalServerError)
|
|
||||||
assert(t, data.Message, msg)
|
assert(t, data.Message, msg)
|
||||||
assert(t, data.Details, errorFromGitlab.Error())
|
assert(t, data.Details, errorFromGitlab.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkBadMethod(t *testing.T, data ErrorResponse, methods ...string) {
|
|
||||||
t.Helper()
|
|
||||||
assert(t, data.Status, http.StatusMethodNotAllowed)
|
|
||||||
assert(t, data.Details, "Invalid request type")
|
|
||||||
expectedMethods := strings.Join(methods, " or ")
|
|
||||||
assert(t, data.Message, fmt.Sprintf("Expected %s", expectedMethods))
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkNon200(t *testing.T, data ErrorResponse, msg, endpoint string) {
|
func checkNon200(t *testing.T, data ErrorResponse, msg, endpoint string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
assert(t, data.Status, http.StatusSeeOther)
|
|
||||||
assert(t, data.Message, msg)
|
assert(t, data.Message, msg)
|
||||||
assert(t, data.Details, fmt.Sprintf("An error occurred on the %s endpoint", endpoint))
|
assert(t, data.Details, fmt.Sprintf("An error occurred on the %s endpoint", endpoint))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,13 +21,7 @@ type meService struct {
|
|||||||
client MeGetter
|
client MeGetter
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a meService) handler(w http.ResponseWriter, r *http.Request) {
|
func (a meService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
if r.Method != http.MethodGet {
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", http.MethodGet)
|
|
||||||
handleError(w, InvalidRequestError{}, "Expected GET", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user, res, err := a.client.CurrentUser()
|
user, res, err := a.client.CurrentUser()
|
||||||
|
|
||||||
@@ -42,10 +36,7 @@ func (a meService) handler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
response := UserResponse{
|
response := UserResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{Message: "User fetched successfully"},
|
||||||
Message: "User fetched successfully",
|
|
||||||
Status: http.StatusOK,
|
|
||||||
},
|
|
||||||
User: user,
|
User: user,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -152,7 +152,12 @@ you call this function with no values the defaults will be used:
|
|||||||
port = nil, -- The port of the Go server, which runs in the background, if omitted or `nil` the port will be chosen automatically
|
port = nil, -- The port of the Go server, which runs in the background, if omitted or `nil` the port will be chosen automatically
|
||||||
log_path = vim.fn.stdpath("cache") .. "/gitlab.nvim.log", -- Log path for the Go server
|
log_path = vim.fn.stdpath("cache") .. "/gitlab.nvim.log", -- Log path for the Go server
|
||||||
config_path = nil, -- Custom path for `.gitlab.nvim` file, please read the "Connecting to Gitlab" section
|
config_path = nil, -- Custom path for `.gitlab.nvim` file, please read the "Connecting to Gitlab" section
|
||||||
debug = { go_request = false, go_response = false }, -- Which values to log
|
debug = {
|
||||||
|
request = false, -- Requests to/from Go server
|
||||||
|
response = false,
|
||||||
|
gitlab_request = false, -- Requests to/from Gitlab
|
||||||
|
gitlab_response = false,
|
||||||
|
},
|
||||||
attachment_dir = nil, -- The local directory for files (see the "summary" section)
|
attachment_dir = nil, -- The local directory for files (see the "summary" section)
|
||||||
reviewer_settings = {
|
reviewer_settings = {
|
||||||
jump_with_no_diagnostics = false, -- Jump to last position in discussion tree if true, otherwise stay in reviewer and show warning.
|
jump_with_no_diagnostics = false, -- Jump to last position in discussion tree if true, otherwise stay in reviewer and show warning.
|
||||||
|
|||||||
10
go.mod
10
go.mod
@@ -3,16 +3,24 @@ module github.com/harrisoncramer/gitlab.nvim
|
|||||||
go 1.19
|
go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/go-playground/validator/v10 v10.22.1
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.7
|
github.com/hashicorp/go-retryablehttp v0.7.7
|
||||||
github.com/xanzy/go-gitlab v0.108.0
|
github.com/xanzy/go-gitlab v0.108.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
golang.org/x/net v0.8.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
golang.org/x/crypto v0.19.0 // indirect
|
||||||
|
golang.org/x/net v0.21.0 // indirect
|
||||||
golang.org/x/oauth2 v0.6.0 // indirect
|
golang.org/x/oauth2 v0.6.0 // indirect
|
||||||
|
golang.org/x/sys v0.20.0 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
golang.org/x/time v0.3.0 // indirect
|
golang.org/x/time v0.3.0 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
google.golang.org/protobuf v1.29.1 // indirect
|
google.golang.org/protobuf v1.29.1 // indirect
|
||||||
|
|||||||
22
go.sum
22
go.sum
@@ -1,5 +1,14 @@
|
|||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
|
||||||
|
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
@@ -14,22 +23,29 @@ github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/S
|
|||||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
|
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
|
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/xanzy/go-gitlab v0.108.0 h1:IEvEUWFR5G1seslRhJ8gC//INiIUqYXuSUoBd7/gFKE=
|
github.com/xanzy/go-gitlab v0.108.0 h1:IEvEUWFR5G1seslRhJ8gC//INiIUqYXuSUoBd7/gFKE=
|
||||||
github.com/xanzy/go-gitlab v0.108.0/go.mod h1:wKNKh3GkYDMOsGmnfuX+ITCmDuSDWFO0G+C4AygL9RY=
|
github.com/xanzy/go-gitlab v0.108.0/go.mod h1:wKNKh3GkYDMOsGmnfuX+ITCmDuSDWFO0G+C4AygL9RY=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||||
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
|
golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
|
||||||
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
|
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||||
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
--- to this module the data required to make the API calls
|
--- to this module the data required to make the API calls
|
||||||
local Popup = require("nui.popup")
|
local Popup = require("nui.popup")
|
||||||
local Layout = require("nui.layout")
|
local Layout = require("nui.layout")
|
||||||
|
local diffview_lib = require("diffview.lib")
|
||||||
local state = require("gitlab.state")
|
local state = require("gitlab.state")
|
||||||
local job = require("gitlab.job")
|
local job = require("gitlab.job")
|
||||||
local u = require("gitlab.utils")
|
local u = require("gitlab.utils")
|
||||||
@@ -153,17 +154,45 @@ end
|
|||||||
|
|
||||||
---@class LayoutOpts
|
---@class LayoutOpts
|
||||||
---@field ranged boolean
|
---@field ranged boolean
|
||||||
---@field discussion_id string|nil
|
|
||||||
---@field unlinked boolean
|
---@field unlinked boolean
|
||||||
|
---@field discussion_id string|nil
|
||||||
|
|
||||||
---This function sets up the layout and popups needed to create a comment, note and
|
---This function sets up the layout and popups needed to create a comment, note and
|
||||||
---multi-line comment. It also sets up the basic keybindings for switching between
|
---multi-line comment. It also sets up the basic keybindings for switching between
|
||||||
---window panes, and for the non-primary sections.
|
---window panes, and for the non-primary sections.
|
||||||
---@param opts LayoutOpts|nil
|
---@param opts LayoutOpts
|
||||||
---@return NuiLayout
|
---@return NuiLayout|nil
|
||||||
M.create_comment_layout = function(opts)
|
M.create_comment_layout = function(opts)
|
||||||
if opts == nil then
|
if opts.unlinked ~= true then
|
||||||
opts = {}
|
-- Check that diffview is initialized
|
||||||
|
if reviewer.tabnr == nil then
|
||||||
|
u.notify("Reviewer must be initialized first", vim.log.levels.ERROR)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check that Diffview is the current view
|
||||||
|
local view = diffview_lib.get_current_view()
|
||||||
|
if view == nil then
|
||||||
|
u.notify("Comments should be left in the reviewer pane", vim.log.levels.ERROR)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check that we are in the diffview tab
|
||||||
|
local tabnr = vim.api.nvim_get_current_tabpage()
|
||||||
|
if tabnr ~= reviewer.tabnr then
|
||||||
|
u.notify("Line location can only be determined within reviewer window", vim.log.levels.ERROR)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check that we are hovering over the code
|
||||||
|
local filetype = vim.bo[0].filetype
|
||||||
|
if filetype == "DiffviewFiles" or filetype == "gitlab" then
|
||||||
|
u.notify(
|
||||||
|
"Comments can only be left on the code. To leave unlinked comments, use gitlab.create_note() instead",
|
||||||
|
vim.log.levels.ERROR
|
||||||
|
)
|
||||||
|
return
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local title = opts.discussion_id and "Reply" or "Comment"
|
local title = opts.discussion_id and "Reply" or "Comment"
|
||||||
@@ -229,7 +258,8 @@ M.create_comment = function()
|
|||||||
if err ~= nil then
|
if err ~= nil then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local is_modified = vim.api.nvim_buf_get_option(0, "modified")
|
|
||||||
|
local is_modified = vim.bo[0].modified
|
||||||
if state.settings.reviewer_settings.diffview.imply_local and (is_modified or not has_clean_tree) then
|
if state.settings.reviewer_settings.diffview.imply_local and (is_modified or not has_clean_tree) then
|
||||||
u.notify(
|
u.notify(
|
||||||
"Cannot leave comments on changed files. \n Please stash all local changes or push them to the feature branch.",
|
"Cannot leave comments on changed files. \n Please stash all local changes or push them to the feature branch.",
|
||||||
@@ -243,7 +273,9 @@ M.create_comment = function()
|
|||||||
end
|
end
|
||||||
|
|
||||||
local layout = M.create_comment_layout({ ranged = false, unlinked = false })
|
local layout = M.create_comment_layout({ ranged = false, unlinked = false })
|
||||||
|
if layout ~= nil then
|
||||||
layout:mount()
|
layout:mount()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- This function will open a multi-line comment popup in order to create a multi-line comment
|
--- This function will open a multi-line comment popup in order to create a multi-line comment
|
||||||
@@ -257,14 +289,18 @@ M.create_multiline_comment = function()
|
|||||||
end
|
end
|
||||||
|
|
||||||
local layout = M.create_comment_layout({ ranged = true, unlinked = false })
|
local layout = M.create_comment_layout({ ranged = true, unlinked = false })
|
||||||
|
if layout ~= nil then
|
||||||
layout:mount()
|
layout:mount()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- This function will open a a popup to create a "note" (e.g. unlinked comment)
|
--- This function will open a a popup to create a "note" (e.g. unlinked comment)
|
||||||
--- on the changed/updated line in the current MR
|
--- on the changed/updated line in the current MR
|
||||||
M.create_note = function()
|
M.create_note = function()
|
||||||
local layout = M.create_comment_layout({ ranged = false, unlinked = true })
|
local layout = M.create_comment_layout({ ranged = false, unlinked = true })
|
||||||
|
if layout ~= nil then
|
||||||
layout:mount()
|
layout:mount()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
---Given the current visually selected area of text, builds text to fill in the
|
---Given the current visually selected area of text, builds text to fill in the
|
||||||
@@ -319,7 +355,9 @@ M.create_comment_suggestion = function()
|
|||||||
local suggestion_lines, range_length = build_suggestion()
|
local suggestion_lines, range_length = build_suggestion()
|
||||||
|
|
||||||
local layout = M.create_comment_layout({ ranged = range_length > 0, unlinked = false })
|
local layout = M.create_comment_layout({ ranged = range_length > 0, unlinked = false })
|
||||||
|
if layout ~= nil then
|
||||||
layout:mount()
|
layout:mount()
|
||||||
|
end
|
||||||
vim.schedule(function()
|
vim.schedule(function()
|
||||||
if suggestion_lines then
|
if suggestion_lines then
|
||||||
vim.api.nvim_buf_set_lines(M.comment_popup.bufnr, 0, -1, false, suggestion_lines)
|
vim.api.nvim_buf_set_lines(M.comment_popup.bufnr, 0, -1, false, suggestion_lines)
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ end
|
|||||||
|
|
||||||
---Publishes all draft notes and comments. Re-renders all discussion views.
|
---Publishes all draft notes and comments. Re-renders all discussion views.
|
||||||
M.confirm_publish_all_drafts = function()
|
M.confirm_publish_all_drafts = function()
|
||||||
local body = { publish_all = true }
|
local body = {}
|
||||||
job.run_job("/mr/draft_notes/publish", "POST", body, function(data)
|
job.run_job("/mr/draft_notes/publish", "POST", body, function(data)
|
||||||
u.notify(data.message, vim.log.levels.INFO)
|
u.notify(data.message, vim.log.levels.INFO)
|
||||||
state.DRAFT_NOTES = {}
|
state.DRAFT_NOTES = {}
|
||||||
@@ -109,7 +109,7 @@ M.confirm_publish_draft = function(tree)
|
|||||||
|
|
||||||
---@type integer
|
---@type integer
|
||||||
local note_id = note_node.is_root and root_node.id or note_node.id
|
local note_id = note_node.is_root and root_node.id or note_node.id
|
||||||
local body = { note = note_id, publish_all = false }
|
local body = { note = note_id }
|
||||||
job.run_job("/mr/draft_notes/publish", "POST", body, function(data)
|
job.run_job("/mr/draft_notes/publish", "POST", body, function(data)
|
||||||
u.notify(data.message, vim.log.levels.INFO)
|
u.notify(data.message, vim.log.levels.INFO)
|
||||||
|
|
||||||
|
|||||||
@@ -226,6 +226,8 @@
|
|||||||
---@class DebugSettings: table
|
---@class DebugSettings: table
|
||||||
---@field go_request? boolean -- Log the requests to Gitlab sent by the Go server
|
---@field go_request? boolean -- Log the requests to Gitlab sent by the Go server
|
||||||
---@field go_response? boolean -- Log the responses received from Gitlab to the Go server
|
---@field go_response? boolean -- Log the responses received from Gitlab to the Go server
|
||||||
|
---@field request? boolean -- Log the requests to the Go server
|
||||||
|
---@field response? boolean -- Log the responses from the Go server
|
||||||
|
|
||||||
---@class PopupSettings: table
|
---@class PopupSettings: table
|
||||||
---@field width? string -- The width of the popup, by default "40%"
|
---@field width? string -- The width of the popup, by default "40%"
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ M.run_job = function(endpoint, method, body, callback)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
local data_ok, data = pcall(vim.json.decode, output)
|
local data_ok, data = pcall(vim.json.decode, output)
|
||||||
|
|
||||||
|
-- Failing to unmarshal JSON
|
||||||
if not data_ok then
|
if not data_ok then
|
||||||
local msg = string.format("Failed to parse JSON from %s endpoint", endpoint)
|
local msg = string.format("Failed to parse JSON from %s endpoint", endpoint)
|
||||||
if type(output) == "string" then
|
if type(output) == "string" then
|
||||||
@@ -34,18 +36,23 @@ M.run_job = function(endpoint, method, body, callback)
|
|||||||
u.notify(string.format(msg, endpoint, output), vim.log.levels.WARN)
|
u.notify(string.format(msg, endpoint, output), vim.log.levels.WARN)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- If JSON provided, handle success or error cases
|
||||||
if data ~= nil then
|
if data ~= nil then
|
||||||
local status = (tonumber(data.status) >= 200 and tonumber(data.status) < 300) and "success" or "error"
|
if data.details == nil then
|
||||||
if status == "success" and callback ~= nil then
|
if callback then
|
||||||
callback(data)
|
callback(data)
|
||||||
elseif status == "success" then
|
return
|
||||||
|
end
|
||||||
local message = string.format("%s", data.message)
|
local message = string.format("%s", data.message)
|
||||||
u.notify(message, vim.log.levels.INFO)
|
u.notify(message, vim.log.levels.INFO)
|
||||||
else
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Handle error case
|
||||||
local message = string.format("%s: %s", data.message, data.details)
|
local message = string.format("%s: %s", data.message, data.details)
|
||||||
u.notify(message, vim.log.levels.ERROR)
|
u.notify(message, vim.log.levels.ERROR)
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end, 0)
|
end, 0)
|
||||||
end,
|
end,
|
||||||
on_stderr = function()
|
on_stderr = function()
|
||||||
|
|||||||
@@ -67,11 +67,11 @@ M.open = function()
|
|||||||
end
|
end
|
||||||
|
|
||||||
if state.INFO.state == "closed" then
|
if state.INFO.state == "closed" then
|
||||||
u.notify(string.format("This MR was closed on %s", u.format_date(state.INFO.closed_at)), vim.log.levels.WARN)
|
u.notify(string.format("This MR was closed %s", u.time_since(state.INFO.closed_at)), vim.log.levels.WARN)
|
||||||
end
|
end
|
||||||
|
|
||||||
if state.INFO.state == "merged" then
|
if state.INFO.state == "merged" then
|
||||||
u.notify(string.format("This MR was merged on %s", u.format_date(state.INFO.merged_at)), vim.log.levels.WARN)
|
u.notify(string.format("This MR was merged %s", u.time_since(state.INFO.merged_at)), vim.log.levels.WARN)
|
||||||
end
|
end
|
||||||
|
|
||||||
if state.settings.discussion_diagnostic ~= nil or state.settings.discussion_sign ~= nil then
|
if state.settings.discussion_diagnostic ~= nil or state.settings.discussion_sign ~= nil then
|
||||||
@@ -151,25 +151,7 @@ end
|
|||||||
---other modules such as the comment module to create line codes or set diagnostics
|
---other modules such as the comment module to create line codes or set diagnostics
|
||||||
---@return DiffviewInfo | nil
|
---@return DiffviewInfo | nil
|
||||||
M.get_reviewer_data = function()
|
M.get_reviewer_data = function()
|
||||||
if M.tabnr == nil then
|
|
||||||
u.notify("Diffview reviewer must be initialized first", vim.log.levels.ERROR)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Check if we are in the diffview tab
|
|
||||||
local tabnr = vim.api.nvim_get_current_tabpage()
|
|
||||||
if tabnr ~= M.tabnr then
|
|
||||||
u.notify("Line location can only be determined within reviewer window", vim.log.levels.ERROR)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Check if we are in the diffview buffer
|
|
||||||
local view = diffview_lib.get_current_view()
|
local view = diffview_lib.get_current_view()
|
||||||
if view == nil then
|
|
||||||
u.notify("Could not find Diffview view", vim.log.levels.ERROR)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local layout = view.cur_layout
|
local layout = view.cur_layout
|
||||||
local old_win = u.get_window_id_by_buffer_id(layout.a.file.bufnr)
|
local old_win = u.get_window_id_by_buffer_id(layout.a.file.bufnr)
|
||||||
local new_win = u.get_window_id_by_buffer_id(layout.b.file.bufnr)
|
local new_win = u.get_window_id_by_buffer_id(layout.b.file.bufnr)
|
||||||
@@ -321,7 +303,7 @@ local set_keymaps = function(bufnr, keymaps)
|
|||||||
if keymaps.reviewer.create_comment ~= false then
|
if keymaps.reviewer.create_comment ~= false then
|
||||||
-- Set keymap for repeated operator keybinding
|
-- Set keymap for repeated operator keybinding
|
||||||
vim.keymap.set("o", keymaps.reviewer.create_comment, function()
|
vim.keymap.set("o", keymaps.reviewer.create_comment, function()
|
||||||
vim.api.nvim_cmd({ cmd = "normal", bang = true, args = { tostring(vim.v.count1) .. "j" } }, {})
|
vim.api.nvim_cmd({ cmd = "normal", bang = true, args = { tostring(vim.v.count1) .. "$" } }, {})
|
||||||
end, {
|
end, {
|
||||||
buffer = bufnr,
|
buffer = bufnr,
|
||||||
desc = "Create comment for [count] lines",
|
desc = "Create comment for [count] lines",
|
||||||
@@ -351,7 +333,7 @@ local set_keymaps = function(bufnr, keymaps)
|
|||||||
if keymaps.reviewer.create_suggestion ~= false then
|
if keymaps.reviewer.create_suggestion ~= false then
|
||||||
-- Set keymap for repeated operator keybinding
|
-- Set keymap for repeated operator keybinding
|
||||||
vim.keymap.set("o", keymaps.reviewer.create_suggestion, function()
|
vim.keymap.set("o", keymaps.reviewer.create_suggestion, function()
|
||||||
vim.api.nvim_cmd({ cmd = "normal", bang = true, args = { tostring(vim.v.count1) .. "j" } }, {})
|
vim.api.nvim_cmd({ cmd = "normal", bang = true, args = { tostring(vim.v.count1) .. "$" } }, {})
|
||||||
end, {
|
end, {
|
||||||
buffer = bufnr,
|
buffer = bufnr,
|
||||||
desc = "Create suggestion for [count] lines",
|
desc = "Create suggestion for [count] lines",
|
||||||
|
|||||||
@@ -47,8 +47,10 @@ M.settings = {
|
|||||||
file_separator = u.path_separator,
|
file_separator = u.path_separator,
|
||||||
port = nil, -- choose random port
|
port = nil, -- choose random port
|
||||||
debug = {
|
debug = {
|
||||||
go_request = false,
|
request = false,
|
||||||
go_response = false,
|
response = false,
|
||||||
|
gitlab_request = false,
|
||||||
|
gitlab_response = false,
|
||||||
},
|
},
|
||||||
log_path = (vim.fn.stdpath("cache") .. "/gitlab.nvim.log"),
|
log_path = (vim.fn.stdpath("cache") .. "/gitlab.nvim.log"),
|
||||||
config_path = nil,
|
config_path = nil,
|
||||||
|
|||||||
Reference in New Issue
Block a user