Skip to content

Commit

Permalink
Merge pull request #294 from Interhyp/RELTEC-11838-pr-validation-part2
Browse files Browse the repository at this point in the history
fix(#292): add tests
  • Loading branch information
StephanHCB authored Jun 13, 2024
2 parents 3c2cac5 + 5c70bce commit fe49140
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 33 deletions.
31 changes: 31 additions & 0 deletions api/openapi-v3-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -1874,6 +1874,37 @@
}
}
}
},
"/webhook/bitbucket": {
"post": {
"tags": [
"webhook"
],
"operationId": "gitNotifyBitBucket",
"description": "webhook notification endpoint to be configured in bitbucket server to notify the service that it should check for new commits, or that a PR needs to be validated",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
},
"responses": {
"204": {
"description": "No content"
},
"400": {
"description": "Payload parse failure - need to send a valid BitBucket webhook payload"
},
"500": {
"description": "Internal Server Error"
}
}
}
}
},
"components": {
Expand Down
69 changes: 40 additions & 29 deletions internal/web/controller/webhookctl/webhookctl.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ type Impl struct {
Timestamp librepo.Timestamp
Updater service.Updater
PRValidator service.PRValidator

EnableAsync bool
}

func New(
Expand All @@ -33,6 +35,7 @@ func New(
Timestamp: timestamp,
Updater: updater,
PRValidator: prValidator,
EnableAsync: true,
}
}

Expand Down Expand Up @@ -82,40 +85,48 @@ func (c *Impl) WebhookBitBucket(w http.ResponseWriter, r *http.Request) {
return
}

routineCtx, routineCtxCancel := contexthelper.AsyncCopyRequestContext(ctx, "webhookBitbucket", "backgroundJob")
go func() {
defer routineCtxCancel()
if c.EnableAsync {
routineCtx, routineCtxCancel := contexthelper.AsyncCopyRequestContext(ctx, "webhookBitbucket", "backgroundJob")
go func() {
defer routineCtxCancel()

switch eventPayload.(type) {
case bitbucketserver.PullRequestOpenedPayload:
payload, ok := eventPayload.(bitbucketserver.PullRequestOpenedPayload)
c.validatePullRequest(routineCtx, "opened", ok, payload.PullRequest)
case bitbucketserver.PullRequestModifiedPayload:
payload, ok := eventPayload.(bitbucketserver.PullRequestModifiedPayload)
c.validatePullRequest(routineCtx, "modified", ok, payload.PullRequest)
case bitbucketserver.PullRequestFromReferenceUpdatedPayload:
payload, ok := eventPayload.(bitbucketserver.PullRequestFromReferenceUpdatedPayload)
c.validatePullRequest(routineCtx, "from_reference", ok, payload.PullRequest)
case bitbucketserver.RepositoryReferenceChangedPayload:
payload, ok := eventPayload.(bitbucketserver.RepositoryReferenceChangedPayload)
if !ok || len(payload.Changes) < 1 || payload.Changes[0].ReferenceID == "" {
aulogging.Logger.Ctx(routineCtx).Error().Printf("bad request while processing bitbucket webhook - got reference changed with invalid info - ignoring webhook")
return
}
aulogging.Logger.Ctx(routineCtx).Info().Printf("got repository reference changed, refreshing caches")

err = c.Updater.PerformFullUpdateWithNotifications(routineCtx)
if err != nil {
aulogging.Logger.Ctx(routineCtx).Error().WithErr(err).Printf("webhook error")
}
default:
// ignore unknown events
}
}()
c.WebhookBitBucketProcessSync(routineCtx, eventPayload)
}()
} else {
c.WebhookBitBucketProcessSync(ctx, eventPayload)
}

util.SuccessNoBody(ctx, w, r, http.StatusNoContent)
}

func (c *Impl) WebhookBitBucketProcessSync(ctx context.Context, eventPayload any) {
switch eventPayload.(type) {
case bitbucketserver.PullRequestOpenedPayload:
payload, ok := eventPayload.(bitbucketserver.PullRequestOpenedPayload)
c.validatePullRequest(ctx, "opened", ok, payload.PullRequest)
case bitbucketserver.PullRequestModifiedPayload:
payload, ok := eventPayload.(bitbucketserver.PullRequestModifiedPayload)
c.validatePullRequest(ctx, "modified", ok, payload.PullRequest)
case bitbucketserver.PullRequestFromReferenceUpdatedPayload:
payload, ok := eventPayload.(bitbucketserver.PullRequestFromReferenceUpdatedPayload)
c.validatePullRequest(ctx, "from_reference", ok, payload.PullRequest)
case bitbucketserver.RepositoryReferenceChangedPayload:
payload, ok := eventPayload.(bitbucketserver.RepositoryReferenceChangedPayload)
if !ok || len(payload.Changes) < 1 || payload.Changes[0].ReferenceID == "" {
aulogging.Logger.Ctx(ctx).Error().Printf("bad request while processing bitbucket webhook - got reference changed with invalid info - ignoring webhook")
return
}
aulogging.Logger.Ctx(ctx).Info().Printf("got repository reference changed, refreshing caches")

err := c.Updater.PerformFullUpdateWithNotifications(ctx)
if err != nil {
aulogging.Logger.Ctx(ctx).Error().WithErr(err).Printf("webhook error")
}
default:
// ignore unknown events
}
}

func (c *Impl) validatePullRequest(ctx context.Context, operation string, parsedOk bool, pullRequestPayload bitbucketserver.PullRequest) {
description := fmt.Sprintf("id: %d, toRef: %s, fromRef: %s", pullRequestPayload.ID, pullRequestPayload.ToRef.ID, pullRequestPayload.FromRef.ID)
if !parsedOk || pullRequestPayload.ID == 0 || pullRequestPayload.ToRef.ID == "" || pullRequestPayload.FromRef.ID == "" {
Expand Down
6 changes: 6 additions & 0 deletions test/acceptance/util_setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/Interhyp/metadata-service/internal/repository/notifier"
"github.com/Interhyp/metadata-service/internal/service/trigger"
"github.com/Interhyp/metadata-service/internal/web/app"
"github.com/Interhyp/metadata-service/internal/web/controller/webhookctl"
"github.com/Interhyp/metadata-service/internal/web/server"
"github.com/Interhyp/metadata-service/test/mock/bitbucketmock"
"github.com/Interhyp/metadata-service/test/mock/idpmock"
Expand Down Expand Up @@ -72,6 +73,8 @@ func (a *ApplicationWithMocksImpl) Create() error {
return err
}

a.WebhookCtl.(*webhookctl.Impl).EnableAsync = false

return nil
}

Expand Down Expand Up @@ -170,4 +173,7 @@ func tstReset() {
for _, client := range notifierImpl.Clients {
client.(*notifiermock.NotifierClientMock).Reset()
}
bbImpl.Recording = nil
bbImpl.PRHead = ""
bbImpl.ChangedFilesResponse = nil
}
147 changes: 147 additions & 0 deletions test/acceptance/webhook_acc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package acceptance

import (
"bytes"
"encoding/json"
"github.com/Interhyp/metadata-service/internal/acorn/repository"
"github.com/StephanHCB/go-backend-service-common/docs"
bitbucketserver "github.com/go-playground/webhooks/v6/bitbucket-server"
"github.com/stretchr/testify/require"
"net/http"
"testing"
"time"
)

func TestPOSTWebhookBitbucket_Success(t *testing.T) {
tstReset()

docs.Given("Given a pull request in BitBucket with valid file edits")
bbImpl.PRHead = "e2d2000000000000000000000000000000000000"
bbImpl.ChangedFilesResponse = []repository.File{
{
Path: "owners/fun-owner/owner.info.yaml",
Contents: `contact: [email protected]
productOwner: someone
displayName: Test Owner`,
},
{
Path: "owners/fun-owner/services/golang-forever.yaml",
Contents: `description: golang is the best
repositories:
- golang-forever/implementation
alertTarget: [email protected]
developmentOnly: false
operationType: WORKLOAD
lifecycle: experimental`,
},
}

docs.When("When BitBucket sends a webhook with valid payload")
body := bitbucketserver.PullRequestOpenedPayload{
Date: bitbucketserver.Date(time.Now()),
EventKey: bitbucketserver.PullRequestOpenedEvent,
Actor: bitbucketserver.User{
// don't care
},
PullRequest: bitbucketserver.PullRequest{
ID: 42,
Title: "some pr title",
Description: "some pr description",
FromRef: bitbucketserver.RepositoryReference{
ID: "e2d2000000000000000000000000000000000000", // pr head
},
ToRef: bitbucketserver.RepositoryReference{
ID: "e100000000000000000000000000000000000000", // mainline
},
Locked: false,
Author: bitbucketserver.PullRequestParticipant{},
},
}
bodyBytes, err := json.Marshal(&body)
require.Nil(t, err)
request, err := http.NewRequest(http.MethodPost, ts.URL+"/webhook/bitbucket", bytes.NewReader(bodyBytes))
require.Nil(t, err)
request.Header.Set("X-Event-Key", string(bitbucketserver.PullRequestOpenedEvent))
rawResponse, err := http.DefaultClient.Do(request)
require.Nil(t, err)
response, err := tstWebResponseFromResponse(rawResponse)
require.Nil(t, err)

docs.Then("Then the request is successful")
tstAssertNoBody(t, response, err, http.StatusNoContent)

docs.Then("And the expected interactions with the BitBucket API have occurred")
require.EqualValues(t, []string{
"GetChangedFilesOnPullRequest(42)",
"CreatePullRequestComment(42, all changed files are valid|)",
"AddCommitBuildStatus(e2d2000000000000000000000000000000000000, metadata-service, true)",
}, bbImpl.Recording)
}

func TestPOSTWebhookBitbucket_InvalidPR(t *testing.T) {
tstReset()

docs.Given("Given a pull request in BitBucket with invalid file edits")
bbImpl.PRHead = "e2d2000000000000000000000000000000000000"
bbImpl.ChangedFilesResponse = []repository.File{
{
Path: "owners/fun-owner/repositories/golang-forever.implementation.yaml",
Contents: `unknown: field`,
},
}

docs.When("When BitBucket sends a webhook with valid payload")
body := bitbucketserver.PullRequestOpenedPayload{
Date: bitbucketserver.Date(time.Now()),
EventKey: bitbucketserver.PullRequestOpenedEvent,
Actor: bitbucketserver.User{
// don't care
},
PullRequest: bitbucketserver.PullRequest{
ID: 42,
Title: "some pr title",
Description: "some pr description",
FromRef: bitbucketserver.RepositoryReference{
ID: "e2d2000000000000000000000000000000000000", // pr head
},
ToRef: bitbucketserver.RepositoryReference{
ID: "e100000000000000000000000000000000000000", // mainline
},
Locked: false,
Author: bitbucketserver.PullRequestParticipant{},
},
}
bodyBytes, err := json.Marshal(&body)
require.Nil(t, err)
request, err := http.NewRequest(http.MethodPost, ts.URL+"/webhook/bitbucket", bytes.NewReader(bodyBytes))
require.Nil(t, err)
request.Header.Set("X-Event-Key", string(bitbucketserver.PullRequestOpenedEvent))
rawResponse, err := http.DefaultClient.Do(request)
require.Nil(t, err)
response, err := tstWebResponseFromResponse(rawResponse)
require.Nil(t, err)

docs.Then("Then the request is successful")
tstAssertNoBody(t, response, err, http.StatusNoContent)

docs.Then("And the expected interactions with the BitBucket API have occurred")
require.EqualValues(t, []string{
"GetChangedFilesOnPullRequest(42)",
"CreatePullRequestComment(42, # yaml validation failure||There were validation errors in changed files. Please fix yaml syntax and/or remove unknown fields:|| - failed to parse `owners/fun-owner/repositories/golang-forever.implementation.yaml`:| yaml: unmarshal errors:| line 1: field unknown not found in type openapi.RepositoryDto|)",
"AddCommitBuildStatus(e2d2000000000000000000000000000000000000, metadata-service, false)",
}, bbImpl.Recording)
}

func TestPOSTWebhookBitbucket_InvalidPayload(t *testing.T) {
tstReset()

docs.Given("Given an unauthenticated user")
token := tstUnauthenticated()

docs.When("When they send a webhook with invalid payload")
body := bitbucketserver.PullRequestOpenedPayload{} // hopefully invalid
response, err := tstPerformPost("/webhook/bitbucket", token, &body)

docs.Then("Then the request fails and the error response is as expected")
tstAssert(t, response, err, http.StatusBadRequest, "webhook-invalid.json")
}
14 changes: 11 additions & 3 deletions test/mock/bitbucketmock/bitbucketmock.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@ package bitbucketmock

import (
"context"
"fmt"
"github.com/Interhyp/metadata-service/internal/acorn/repository"
"github.com/pkg/errors"
"strings"
)

const FILTER_FAILED_USERNAME = "filterfailedusername"

type BitbucketMock struct {
ChangedFilesResponse []repository.File
PRHead string
Recording []string
}

func New() repository.Bitbucket {
Expand Down Expand Up @@ -51,13 +56,16 @@ func (b *BitbucketMock) FilterExistingUsernames(ctx context.Context, usernames [
}

func (b *BitbucketMock) GetChangedFilesOnPullRequest(ctx context.Context, pullRequestId int) ([]repository.File, string, error) {
return []repository.File{}, "", nil
b.Recording = append(b.Recording, fmt.Sprintf("GetChangedFilesOnPullRequest(%d)", pullRequestId))
return b.ChangedFilesResponse, b.PRHead, nil
}

func (r *BitbucketMock) AddCommitBuildStatus(ctx context.Context, commitHash string, url string, key string, success bool) error {
func (b *BitbucketMock) AddCommitBuildStatus(ctx context.Context, commitHash string, url string, key string, success bool) error {
b.Recording = append(b.Recording, fmt.Sprintf("AddCommitBuildStatus(%s, %s, %t)", commitHash, key, success))
return nil
}

func (r *BitbucketMock) CreatePullRequestComment(ctx context.Context, pullRequestId int, comment string) error {
func (b *BitbucketMock) CreatePullRequestComment(ctx context.Context, pullRequestId int, comment string) error {
b.Recording = append(b.Recording, fmt.Sprintf("CreatePullRequestComment(%d, %s)", pullRequestId, strings.ReplaceAll(comment, "\n", "|")))
return nil
}
5 changes: 5 additions & 0 deletions test/resources/acceptance-expected/webhook-invalid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"details": "parse payload error",
"message": "webhook.payload.invalid",
"timestamp": "2022-11-06T18:14:10Z"
}
2 changes: 1 addition & 1 deletion test/resources/valid-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ PULL_REQUEST_BUILD_URL: https://metadata-service.example.com
AUTH_OIDC_TOKEN_AUDIENCE: some-audience
AUTH_GROUP_WRITE: admin

SSH_METADATA_REPO_URL: git://metadata
SSH_METADATA_REPO_URL: git://er/metadata.git
METADATA_REPO_URL: http://metadata

UPDATE_JOB_INTERVAL_MINUTES: 5
Expand Down

0 comments on commit fe49140

Please sign in to comment.