Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: GraphQL interface #200

Merged
merged 8 commits into from
Jun 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
on: [push, pull_request]
name: Test

env:
REACT_APP_API_BASE_URL: http://localhost:8080
APP_SHA: ${{ github.sha }}
APP_BRANCH: ${{ github.ref }}
LD_LIBRARY_PATH: /tmp
PACT_GO_LIB_DOWNLOAD_PATH: /tmp
LOG_LEVEL: trace
GIT_COMMIT: ${{ github.sha }}
GIT_REF: ${{ github.ref }}
COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}

jobs:
test:
strategy:
matrix:
go-version: [1.17.x, 1.18.x]
# os: [ubuntu-latest, macos-latest, windows-latest]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
- name: Test
run: GIT_BRANCH=${GIT_REF:11} DOCKER_GATEWAY_HOST=172.17.0.1 DOCKER_HOST_HTTP="http://172.17.0.1" make
- name: Install goveralls
run: go install github.com/mattn/goveralls@latest
- name: Send coverage
run: goveralls -coverprofile=coverage.txt -service=github -parallel

finish:
needs: test
runs-on: ubuntu-latest
steps:
- name: Coveralls Finished
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
parallel-finished: true
46 changes: 22 additions & 24 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
include make/config.mk

TEST?=./...

.DEFAULT_GOAL := ci
DOCKER_HOST_HTTP?="http://host.docker.internal"
PACT_CLI="docker run --rm -v ${PWD}:${PWD} -e PACT_BROKER_BASE_URL=$(DOCKER_HOST_HTTP) -e PACT_BROKER_USERNAME -e PACT_BROKER_PASSWORD pactfoundation/pact-cli"

ci:: docker deps snyk clean bin test pact goveralls
ci:: docker deps clean bin test pact

docker:
@echo "--- 🛠 Starting docker"
Expand All @@ -22,26 +23,31 @@ bin:
clean:
rm -rf build output dist

deps: snyk-install
deps:
@echo "--- 🐿 Fetching build dependencies "
go get github.com/axw/gocov/gocov
go get github.com/mattn/goveralls
go get golang.org/x/tools/cmd/cover
go get github.com/modocache/gover
go get github.com/mitchellh/gox

goveralls:
goveralls -service="travis-ci" -coverprofile=coverage.txt -repotoken $(COVERALLS_TOKEN)
cd /tmp; \
go install github.com/mitchellh/gox@latest; \
cd -

install:
@if [ ! -d pact/bin ]; then\
echo "--- 🐿 Installing Pact CLI dependencies"; \
curl -fsSL https://raw.githubusercontent.com/pact-foundation/pact-ruby-standalone/master/install.sh | bash -x; \
fi
fi

publish_pacts:
@echo "\n========== STAGE: publish pacts ==========\n"
@"${PACT_CLI}" publish ${PWD}/examples/pacts --consumer-app-version ${GIT_COMMIT} --tag ${GIT_BRANCH} --tag dev --tag prod

pact_local:
GIT_COMMIT=`git rev-parse --short HEAD`+`date +%s` \
GIT_BRANCH=`git rev-parse --abbrev-ref HEAD` \
make pact

pact: install docker
@echo "--- 🔨 Running Pact examples"
go test -tags=consumer -count=1 github.com/pact-foundation/pact-go/examples/... -run TestExample
make publish_pacts
go test -tags=provider -count=1 github.com/pact-foundation/pact-go/examples/... -run TestExample

release:
Expand All @@ -54,13 +60,13 @@ test: deps install
@echo "mode: count" > coverage.txt
@for d in $$(go list ./... | grep -v vendor | grep -v examples); \
do \
go test -race -coverprofile=profile.out -covermode=atomic $$d; \
go test -race -coverprofile=profile.cov -covermode=atomic $$d; \
if [ $$? != 0 ]; then \
exit 1; \
fi; \
if [ -f profile.out ]; then \
cat profile.out | tail -n +2 >> coverage.txt; \
rm profile.out; \
if [ -f profile.cov ]; then \
cat profile.cov | tail -n +2 >> coverage.txt; \
rm profile.cov; \
fi; \
done; \
go tool cover -func coverage.txt
Expand All @@ -71,12 +77,4 @@ testrace:
updatedeps:
go get -d -v -p 2 ./...

snyk-install:
which snyk || npm i -g snyk

snyk:
@if [ "$$TRAVIS_PULL_REQUEST" != "false" ]; then\
snyk test; \
fi

.PHONY: install bin default dev test pact updatedeps clean release
2 changes: 1 addition & 1 deletion command/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"github.com/spf13/cobra"
)

var version = "v1.6.9"
var version = "v1.7.0"
var cliToolsVersion = "1.82.3"
var versionCmd = &cobra.Command{
Use: "version",
Expand Down
139 changes: 139 additions & 0 deletions dsl/graphql/interaction.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package graphql

import (
"fmt"
"regexp"

"github.com/pact-foundation/pact-go/dsl"
)

// Variables represents values to be substituted into the query
type Variables map[string]interface{}

// Query is the main implementation of the Pact interface.
type Query struct {
// HTTP Headers
Headers dsl.MapMatcher

// Path to GraphQL endpoint
Path dsl.Matcher

// HTTP Query String
QueryString dsl.MapMatcher

// GraphQL Query
Query string

// GraphQL Variables
Variables Variables

// GraphQL Operation
Operation string

// GraphQL method (usually POST, but can be get with a query string)
// NOTE: for query string users, the standard HTTP interaction should suffice
Method string

// Supports graphql extensions such as https://www.apollographql.com/docs/apollo-server/performance/apq/
Extensions Extensions
}
type Extensions map[string]interface{}

// Specify the operation (if any)
func (r *Query) WithOperation(operation string) *Query {
r.Operation = operation

return r
}

// WithContentType overrides the default content-type (application/json)
// for the GraphQL Query
func (r *Query) WithContentType(contentType dsl.Matcher) *Query {
r.setHeader("content-type", contentType)

return r
}

// Specify the method (defaults to POST)
func (r *Query) WithMethod(method string) *Query {
r.Method = method

return r
}

// Given specifies a provider state. Optional.
func (r *Query) WithQuery(query string) *Query {
r.Query = query

return r
}

// Given specifies a provider state. Optional.
func (r *Query) WithVariables(variables Variables) *Query {
r.Variables = variables

return r
}

// Set the query extensions
func (r *Query) WithExtensions(extensions Extensions) *Query {
r.Extensions = extensions

return r
}

var defaultHeaders = dsl.MapMatcher{"content-type": dsl.String("application/json")}

func (r *Query) setHeader(headerName string, value dsl.Matcher) *Query {
if r.Headers == nil {
r.Headers = defaultHeaders
}

r.Headers[headerName] = value

return r
}

// Construct a Pact HTTP request for a GraphQL interaction
func Interaction(request Query) *dsl.Request {
if request.Headers == nil {
request.Headers = defaultHeaders
}

return &dsl.Request{
Method: request.Method,
Path: request.Path,
Query: request.QueryString,
Body: graphQLQueryBody{
Operation: request.Operation,
Query: dsl.Regex(request.Query, escapeGraphQlQuery(request.Query)),
Variables: request.Variables,
},
Headers: request.Headers,
}

}

type graphQLQueryBody struct {
Operation string `json:"operationName,omitempty"`
Query dsl.Matcher `json:"query"`
Variables Variables `json:"variables,omitempty"`
}

func escapeSpace(s string) string {
r := regexp.MustCompile(`\s+`)
return r.ReplaceAllString(s, `\s*`)
}

func escapeRegexChars(s string) string {
r := regexp.MustCompile(`(?m)[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]`)

f := func(s string) string {
return fmt.Sprintf(`\%s`, s)
}
return r.ReplaceAllStringFunc(s, f)
}

func escapeGraphQlQuery(s string) string {
return escapeSpace(escapeRegexChars(s))
}
9 changes: 9 additions & 0 deletions dsl/graphql/response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package graphql

// GraphQLRseponse models the GraphQL Response format.
// See also http://spec.graphql.org/October2021/#sec-Response-Format
type Response struct {
Data interface{} `json:"data,omitempty"`
Errors []interface{} `json:"errors,omitempty"`
Extensions map[string]interface{} `json:"extensions,omitempty"`
}
28 changes: 1 addition & 27 deletions examples/consumer/goconsumer/user_service_test.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
//go:build consumer
// +build consumer

package goconsumer

import (
"errors"
"fmt"
"log"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"time"

"github.com/pact-foundation/pact-go/dsl"
ex "github.com/pact-foundation/pact-go/examples/types"
"github.com/pact-foundation/pact-go/types"
)

// Common test data
Expand Down Expand Up @@ -59,29 +56,6 @@ func TestMain(m *testing.M) {
pact.WritePact()
pact.Teardown()

// Enable when running E2E/integration tests before a release
version := fmt.Sprintf("1.0.%s-%d", os.Getenv("TRAVIS_COMMIT"), time.Now().Unix())

// Publish the Pacts...
p := dsl.Publisher{
LogLevel: "DEBUG",
}

err := p.Publish(types.PublishRequest{
PactURLs: []string{filepath.FromSlash(fmt.Sprintf("%s/jmarie-loginprovider.json", pactDir))},
PactBroker: fmt.Sprintf("%s://%s", os.Getenv("PACT_BROKER_PROTO"), os.Getenv("PACT_BROKER_URL")),
ConsumerVersion: version,
Tags: []string{"dev", "prod"},
BrokerToken: os.Getenv("PACT_BROKER_TOKEN"),
BrokerUsername: os.Getenv("PACT_BROKER_USERNAME"),
BrokerPassword: os.Getenv("PACT_BROKER_PASSWORD"),
})

if err != nil {
log.Println("ERROR: ", err)
os.Exit(1)
}

os.Exit(code)
}

Expand Down
5 changes: 3 additions & 2 deletions examples/gin/provider/user_service_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//go:build provider
// +build provider

package provider
Expand Down Expand Up @@ -41,7 +42,7 @@ func TestExample_GinProvider(t *testing.T) {
// Verify the Provider - Latest Published Pacts for any known consumers
_, err := pact.VerifyProvider(t, types.VerifyRequest{
ProviderBaseURL: fmt.Sprintf("http://127.0.0.1:%d", port),
BrokerURL: fmt.Sprintf("%s://%s", os.Getenv("PACT_BROKER_PROTO"), os.Getenv("PACT_BROKER_URL")),
BrokerURL: os.Getenv("PACT_BROKER_BASE_URL"),
BrokerToken: os.Getenv("PACT_BROKER_TOKEN"),
BrokerUsername: os.Getenv("PACT_BROKER_USERNAME"),
BrokerPassword: os.Getenv("PACT_BROKER_PASSWORD"),
Expand All @@ -58,7 +59,7 @@ func TestExample_GinProvider(t *testing.T) {
// Verify the Provider - Tag-based Published Pacts for any known consumers
_, err = pact.VerifyProvider(t, types.VerifyRequest{
ProviderBaseURL: fmt.Sprintf("http://127.0.0.1:%d", port),
BrokerURL: fmt.Sprintf("%s://%s", os.Getenv("PACT_BROKER_PROTO"), os.Getenv("PACT_BROKER_URL")),
BrokerURL: os.Getenv("PACT_BROKER_BASE_URL"),
// Use ConsumerVersionSelectors instead of Tags for
ConsumerVersionSelectors: selectors,
BrokerToken: os.Getenv("PACT_BROKER_TOKEN"),
Expand Down
Loading