-
-
Notifications
You must be signed in to change notification settings - Fork 111
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1,065 changed files
with
177,057 additions
and
162,611 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
package consumer | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"log" | ||
"net/http" | ||
"testing" | ||
|
||
graphqlserver "github.com/graph-gophers/graphql-go" | ||
"github.com/graph-gophers/graphql-go/example/starwars" | ||
"github.com/graph-gophers/graphql-go/relay" | ||
graphql "github.com/hasura/go-graphql-client" | ||
"github.com/pact-foundation/pact-go/dsl" | ||
g "github.com/pact-foundation/pact-go/dsl/graphql" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestGraphQLConsumer(t *testing.T) { | ||
// Create Pact connecting to local Daemon | ||
pact := &dsl.Pact{ | ||
Consumer: "GraphQLConsumer", | ||
Provider: "GraphQLProvider", | ||
Host: "localhost", | ||
} | ||
defer pact.Teardown() | ||
|
||
// Set up our expected interactions. | ||
pact. | ||
AddInteraction(). | ||
Given("User foo exists"). | ||
UponReceiving("A request to get foo"). | ||
WithRequest(*g.Interaction(g.Query{ | ||
Method: "POST", | ||
Path: dsl.String("/query"), | ||
Query: `query ($characterID:ID!){ | ||
hero { | ||
id, | ||
name | ||
}, | ||
character(id: $characterID) | ||
{ | ||
name, | ||
friends{ | ||
name, | ||
__typename | ||
}, | ||
appearsIn | ||
} | ||
}`, | ||
// Operation: "SomeOperation", // if needed | ||
Variables: g.Variables{ | ||
"characterID": "1003", | ||
}, | ||
})). | ||
WillRespondWith(dsl.Response{ | ||
Status: 200, | ||
Headers: dsl.MapMatcher{"Content-Type": dsl.String("application/json")}, | ||
Body: g.Response{ | ||
Data: heroQuery{ | ||
Hero: hero{ | ||
ID: graphql.ID("1003"), | ||
Name: "Darth Vader", | ||
}, | ||
Character: character{ | ||
Name: "Darth Vader", | ||
AppearsIn: []graphql.String{ | ||
"EMPIRE", | ||
}, | ||
Friends: []friend{ | ||
{ | ||
Name: "Wilhuff Tarkin", | ||
Typename: "friends", | ||
}, | ||
}, | ||
}, | ||
}, | ||
}}) | ||
|
||
// assert on the response | ||
var test = func() error { | ||
res, err := executeQuery(fmt.Sprintf("http://localhost:%d", pact.Server.Port)) | ||
|
||
fmt.Println(res) | ||
assert.NoError(t, err) | ||
assert.NotNil(t, res.Hero.ID) | ||
|
||
return nil | ||
} | ||
|
||
// Verify | ||
if err := pact.Verify(test); err != nil { | ||
log.Fatalf("Error on Verify: %v", err) | ||
} | ||
} | ||
|
||
func executeQuery(baseURL string) (heroQuery, error) { | ||
var q heroQuery | ||
|
||
// Set up a GraphQL server. | ||
schema, err := graphqlserver.ParseSchema(starwars.Schema, &starwars.Resolver{}) | ||
if err != nil { | ||
return q, err | ||
} | ||
mux := http.NewServeMux() | ||
mux.Handle("/query", &relay.Handler{Schema: schema}) | ||
|
||
client := graphql.NewClient(fmt.Sprintf("%s/query", baseURL), nil) | ||
|
||
variables := map[string]interface{}{ | ||
"characterID": graphql.ID("1003"), | ||
} | ||
err = client.Query(context.Background(), &q, variables) | ||
if err != nil { | ||
return q, err | ||
} | ||
|
||
return q, nil | ||
} | ||
|
||
type hero struct { | ||
ID graphql.ID `json:"ID"` | ||
Name graphql.String `json:"Name"` | ||
} | ||
type friend struct { | ||
Name graphql.String `json:"Name"` | ||
Typename graphql.String `json:"__typename" graphql:"__typename"` | ||
} | ||
type character struct { | ||
Name graphql.String `json:"Name"` | ||
Friends []friend `json:"Friends"` | ||
AppearsIn []graphql.String `json:"AppearsIn"` | ||
} | ||
|
||
type heroQuery struct { | ||
Hero hero `json:"Hero"` | ||
Character character `json:"character" graphql:"character(id: $characterID)"` | ||
} |
64 changes: 64 additions & 0 deletions
64
examples/graphql/consumer/pacts/graphqlconsumer-graphqlprovider.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
{ | ||
"consumer": { | ||
"name": "GraphQLConsumer" | ||
}, | ||
"provider": { | ||
"name": "GraphQLProvider" | ||
}, | ||
"interactions": [ | ||
{ | ||
"description": "A request to get foo", | ||
"providerState": "User foo exists", | ||
"request": { | ||
"method": "POST", | ||
"path": "/query", | ||
"headers": { | ||
"content-type": "application/json" | ||
}, | ||
"body": { | ||
"query": "query ($characterID:ID!){\n\t\t\t\thero {\n\t\t\t\t\tid,\n\t\t\t\t\tname\n\t\t\t\t},\n\t\t\t\tcharacter(id: $characterID)\n\t\t\t\t{\n\t\t\t\t\tname,\n\t\t\t\t\tfriends{\n\t\t\t\t\t\tname,\n\t\t\t\t\t\t__typename\n\t\t\t\t\t},\n\t\t\t\t\tappearsIn\n\t\t\t\t}\n\t\t\t}", | ||
"variables": { | ||
"characterID": "1003" | ||
} | ||
}, | ||
"matchingRules": { | ||
"$.body.query": { | ||
"match": "regex", | ||
"regex": "query\\s*\\(\\$characterID:ID!\\)\\{\\s*hero\\s*\\{\\s*id,\\s*name\\s*\\},\\s*character\\(id:\\s*\\$characterID\\)\\s*\\{\\s*name,\\s*friends\\{\\s*name,\\s*__typename\\s*\\},\\s*appearsIn\\s*\\}\\s*\\}" | ||
} | ||
} | ||
}, | ||
"response": { | ||
"status": 200, | ||
"headers": { | ||
"Content-Type": "application/json" | ||
}, | ||
"body": { | ||
"data": { | ||
"Hero": { | ||
"ID": "1003", | ||
"Name": "Darth Vader" | ||
}, | ||
"character": { | ||
"Name": "Darth Vader", | ||
"Friends": [ | ||
{ | ||
"Name": "Wilhuff Tarkin", | ||
"__typename": "friends" | ||
} | ||
], | ||
"AppearsIn": [ | ||
"EMPIRE" | ||
] | ||
} | ||
} | ||
} | ||
} | ||
} | ||
], | ||
"metadata": { | ||
"pactSpecification": { | ||
"version": "2.0.0" | ||
} | ||
} | ||
} |
53 changes: 53 additions & 0 deletions
53
examples/graphql/consumer/pacts/myconsumer-myprovider.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
{ | ||
"consumer": { | ||
"name": "MyConsumer" | ||
}, | ||
"provider": { | ||
"name": "MyProvider" | ||
}, | ||
"interactions": [ | ||
{ | ||
"description": "A request to get foo", | ||
"providerState": "User foo exists", | ||
"request": { | ||
"method": "POST", | ||
"path": "/query", | ||
"body": { | ||
"query": "query ($characterID:ID!){\n\t\t\t\thero {\n\t\t\t\t\tid,\n\t\t\t\t\tname\n\t\t\t\t},\n\t\t\t\tcharacter(id: $characterID)\n\t\t\t\t{\n\t\t\t\t\tname,\n\t\t\t\t\tfriends{\n\t\t\t\t\t\tname,\n\t\t\t\t\t\t__typename\n\t\t\t\t\t},\n\t\t\t\t\tappearsIn\n\t\t\t\t}\n\t\t\t}", | ||
"variables": { | ||
"characterID": "1003" | ||
} | ||
}, | ||
"matchingRules": { | ||
"$.body.query": { | ||
"match": "regex", | ||
"regex": "query\\s*\\(\\$characterID:ID!\\)\\{\\s*hero\\s*\\{\\s*id,\\s*name\\s*\\},\\s*character\\(id:\\s*\\$characterID\\)\\s*\\{\\s*name,\\s*friends\\{\\s*name,\\s*__typename\\s*\\},\\s*appearsIn\\s*\\}\\s*\\}" | ||
} | ||
} | ||
}, | ||
"response": { | ||
"status": 200, | ||
"headers": { | ||
"Content-Type": "application/json" | ||
}, | ||
"body": { | ||
"lastName": "sampson", | ||
"name": "billy" | ||
}, | ||
"matchingRules": { | ||
"$.body.lastName": { | ||
"match": "type" | ||
}, | ||
"$.body.name": { | ||
"match": "type" | ||
} | ||
} | ||
} | ||
} | ||
], | ||
"metadata": { | ||
"pactSpecification": { | ||
"version": "2.0.0" | ||
} | ||
} | ||
} |
Oops, something went wrong.