diff --git a/.github/workflows/pact-provider-verification.yml b/.github/workflows/pact-provider-verification.yml new file mode 100644 index 00000000..ac960d59 --- /dev/null +++ b/.github/workflows/pact-provider-verification.yml @@ -0,0 +1,53 @@ +name: Pact Provider Verification + +on: + repository_dispatch: + types: [provider-verification] + pull_request: + branches: + - main + push: + branches: + - main + +jobs: + test: + name: Provider verification + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: make build up + - run: go build -o ./api-test/tester ./api-test && chmod +x ./api-test/tester + - run: export JWT=$(./api-test/tester -jwtSecret=secret JWT) + - name: Verify specified Pact + if: ${{ github.event_name == 'repository_dispatch' }} + run: | + docker-compose run --rm pact-verifier \ + --header="X-Jwt-Authorization: Bearer $JWT" \ + --provider-version=$(git rev-parse HEAD) \ + --provider-branch=main \ + --publish \ + --user=admin \ + --password=${{ secrets.PACT_BROKER_PASSWORD }} \ + --url=${{ github.event.client_payload.pact_url }} + - name: Verify pacts, including pending + if: ${{ github.event_name == 'push' }} + run: | + docker-compose run --rm pact-verifier \ + --header="X-Jwt-Authorization: Bearer $JWT" \ + --provider-version=$(git rev-parse HEAD) \ + --provider-branch=main \ + --publish \ + --user=admin \ + --password=${{ secrets.PACT_BROKER_PASSWORD }} \ + --consumer-version-selectors='{"mainBranch": true}' \ + --enable-pending + - name: Verify pacts are still upheld + if: ${{ github.event_name == 'pull_request' }} + run: | + docker-compose run --rm pact-verifier \ + --header="X-Jwt-Authorization=Bearer $JWT" \ + --provider-version=$(git rev-parse HEAD) \ + --provider-branch=${{ github.head_ref }} \ + --consumer-version-selectors='{"branch": "VEGA2174_lpastore_service"}' + # --consumer-version-selectors='{"mainBranch": true}' diff --git a/api-test/main.go b/api-test/main.go index 93b3459e..c2ac2772 100644 --- a/api-test/main.go +++ b/api-test/main.go @@ -17,8 +17,11 @@ import ( ) // ./api-test/tester UID -> generate a UID +// ./api-test/tester JWT -> generate a JWT // ./api-test/tester -jwtSecret=secret -expectedStatus=200 REQUEST -// -> make a test request with a JWT generated using secret "secret" and expected status 200 +// +// -> make a test request with a JWT generated using secret "secret" and expected status 200 +// // note that the jwtSecret sends a boilerplate JWT for now with valid iat, exp, iss and sub fields func main() { expectedStatusCode := flag.Int("expectedStatus", 200, "Expected response status code") @@ -33,6 +36,11 @@ func main() { os.Exit(0) } + if args[0] == "JWT" { + fmt.Print(makeJwt([]byte(*jwtSecret))) + os.Exit(0) + } + if args[0] != "REQUEST" { panic("Unrecognised command") } @@ -49,19 +57,9 @@ func main() { req.Header.Add("Content-type", "application/json") if *jwtSecret != "" { - secretKey := []byte(*jwtSecret) - - claims := jwt.MapClaims{ - "exp": time.Now().Add(time.Hour * 24).Unix(), - "iat": time.Now().Add(time.Hour * -24).Unix(), - "iss": "opg.poas.sirius", - "sub": "someone@someplace.somewhere.com", - } + tokenString := makeJwt([]byte(*jwtSecret)) - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - tokenString, _ := token.SignedString(secretKey) - - req.Header.Add("X-Jwt-Authorization", fmt.Sprintf("Bearer: %s", tokenString)) + req.Header.Add("X-Jwt-Authorization", fmt.Sprintf("Bearer %s", tokenString)) } sess := session.Must(session.NewSession()) @@ -88,6 +86,25 @@ func main() { log.Printf("invalid status code %d; expected: %d", resp.StatusCode, *expectedStatusCode) log.Printf("error response: %s", buf.String()) } else { + log.Print(resp.Header) log.Printf("Test passed - %s to %s - %d: %s", method, url, resp.StatusCode, buf.String()) } } + +func makeJwt(secretKey []byte) string { + claims := jwt.MapClaims{ + "exp": time.Now().Add(time.Hour * 24).Unix(), + "iat": time.Now().Add(time.Hour * -24).Unix(), + "iss": "opg.poas.sirius", + "sub": "someone@someplace.somewhere.com", + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString(secretKey) + + if err != nil { + panic(err) + } + + return tokenString +} diff --git a/docker-compose.yml b/docker-compose.yml index bae43919..452b5572 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,7 +17,7 @@ services: AWS_ACCESS_KEY_ID: X AWS_SECRET_ACCESS_KEY: X DDB_TABLE_NAME_DEEDS: deeds - JWT_SECRET_KEY: ${JWT_SECRET_KEY} + JWT_SECRET_KEY: ${JWT_SECRET_KEY:-secret} volumes: - "./lambda/.aws-lambda-rie:/aws-lambda" entrypoint: /aws-lambda/aws-lambda-rie /var/task/main @@ -35,7 +35,7 @@ services: AWS_ACCESS_KEY_ID: X AWS_SECRET_ACCESS_KEY: X DDB_TABLE_NAME_DEEDS: deeds - JWT_SECRET_KEY: ${JWT_SECRET_KEY} + JWT_SECRET_KEY: ${JWT_SECRET_KEY:-secret} volumes: - "./lambda/.aws-lambda-rie:/aws-lambda" entrypoint: /aws-lambda/aws-lambda-rie /var/task/main @@ -53,7 +53,7 @@ services: AWS_ACCESS_KEY_ID: X AWS_SECRET_ACCESS_KEY: X DDB_TABLE_NAME_DEEDS: deeds - JWT_SECRET_KEY: ${JWT_SECRET_KEY} + JWT_SECRET_KEY: ${JWT_SECRET_KEY:-secret} volumes: - "./lambda/.aws-lambda-rie:/aws-lambda" entrypoint: /aws-lambda/aws-lambda-rie /var/task/main @@ -88,3 +88,14 @@ services: volumes: - .:/app command: -exclude-dir=.gocache -fmt sarif -out /app/results.sarif /app/... + + pact-verifier: + image: pactfoundation/pact-ref-verifier + entrypoint: + - pact_verifier_cli + - --hostname=apigw + - --port=8080 + - --base-path=/ + - --broker-url=https://pact-broker.api.opg.service.justice.gov.uk/ + - --provider-name=data-lpa-store + - --state-change-url=http://apigw:8080/_pact_state diff --git a/mock-apigw/main.go b/mock-apigw/main.go index 96981fa4..b9faa20a 100644 --- a/mock-apigw/main.go +++ b/mock-apigw/main.go @@ -21,6 +21,18 @@ func delegateHandler(w http.ResponseWriter, r *http.Request) { lambdaName := "" uid := "" + if r.URL.Path == "/_pact_state" { + err := handlePactState(r) + if err != nil { + log.Printf("Error setting up state: %s", err.Error()) + http.Error(w, err.Error(), 500) + } else { + w.WriteHeader(200) + } + + return + } + if LPAPath.MatchString(r.URL.Path) && r.Method == http.MethodPut { uid = LPAPath.FindStringSubmatch(r.URL.Path)[1] lambdaName = "create" @@ -71,6 +83,7 @@ func delegateHandler(w http.ResponseWriter, r *http.Request) { var respBody events.APIGatewayProxyResponse _ = json.Unmarshal(encodedRespBody, &respBody) + w.Header().Set("Content-Type", "application/json") w.WriteHeader(respBody.StatusCode) _, err = w.Write([]byte(respBody.Body)) @@ -79,6 +92,66 @@ func delegateHandler(w http.ResponseWriter, r *http.Request) { } } +func handlePactState(r *http.Request) error { + var state struct { + State string `json:"state"` + } + + if err := json.NewDecoder(r.Body).Decode(&state); err != nil { + return err + } + + re := regexp.MustCompile(`^An LPA with UID (M-[A-Z0-9-]+) exists$`) + if match := re.FindStringSubmatch(state.State); len(match) > 0 { + url := fmt.Sprintf("http://localhost:8080/lpas/%s", match[1]) + body := `{ + "donor": { + "firstNames": "Homer", + "surname": "Zoller", + "dateOfBirth": "1960-04-06", + "address": { + "line1": "79 Bury Rd", + "town": "Hampton Lovett", + "postcode": "WR9 2PF", + "country": "GB" + } + }, + "attorneys": [ + { + "firstNames": "Jake", + "surname": "Vallar", + "dateOfBirth": "2001-01-17", + "status": "active", + "address": { + "line1": "71 South Western Terrace", + "town": "Milton", + "country": "AU" + } + } + ] + }` + + req, err := http.NewRequest("PUT", url, strings.NewReader(body)) + if err != nil { + return err + } + + req.Header = r.Header.Clone() + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + + if resp.StatusCode >= 400 { + return fmt.Errorf("request failed with status code %d", resp.StatusCode) + } + } + + return nil +} + func main() { http.HandleFunc("/", delegateHandler)