-
-
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.
feat: support graphql in v2 interface
- Loading branch information
Showing
5 changed files
with
304 additions
and
0 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,140 @@ | ||
package graphql | ||
|
||
import ( | ||
"fmt" | ||
"regexp" | ||
|
||
"github.com/pact-foundation/pact-go/v2/consumer" | ||
"github.com/pact-foundation/pact-go/v2/matchers" | ||
) | ||
|
||
// 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 matchers.MapMatcher | ||
|
||
// Path to GraphQL endpoint | ||
Path matchers.Matcher | ||
|
||
// HTTP Query String | ||
QueryString matchers.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 matchers.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 = matchers.MapMatcher{"content-type": matchers.String("application/json")} | ||
|
||
func (r *Query) setHeader(headerName string, value matchers.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) *consumer.Request { | ||
if request.Headers == nil { | ||
request.Headers = defaultHeaders | ||
} | ||
|
||
return &consumer.Request{ | ||
Method: request.Method, | ||
Path: request.Path, | ||
Query: request.QueryString, | ||
Body: graphQLQueryBody{ | ||
Operation: request.Operation, | ||
Query: matchers.Regex(request.Query, escapeGraphQlQuery(request.Query)), | ||
Variables: request.Variables, | ||
}, | ||
Headers: request.Headers, | ||
} | ||
|
||
} | ||
|
||
type graphQLQueryBody struct { | ||
Operation string `json:"operationName,omitempty"` | ||
Query matchers.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,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"` | ||
} |
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,135 @@ | ||
//go:build consumer | ||
// +build consumer | ||
|
||
package graphql | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"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/v2/consumer" | ||
g "github.com/pact-foundation/pact-go/v2/consumer/graphql" | ||
"github.com/pact-foundation/pact-go/v2/matchers" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestGraphQLConsumer(t *testing.T) { | ||
// Create Pact connecting to local Daemon | ||
pact, err := consumer.NewV4Pact(consumer.MockHTTPProviderConfig{ | ||
Consumer: "GraphQLConsumer", | ||
Provider: "GraphQLProvider", | ||
}) | ||
assert.NoError(t, err) | ||
|
||
// Set up our expected interactions. | ||
err = pact. | ||
AddInteraction(). | ||
Given("User foo exists"). | ||
UponReceiving("A request to get foo"). | ||
WithCompleteRequest(*g.Interaction(g.Query{ | ||
Method: "POST", | ||
Path: matchers.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", | ||
}, | ||
})). | ||
WithCompleteResponse(consumer.Response{ | ||
Status: 200, | ||
Headers: matchers.MapMatcher{"Content-Type": matchers.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", | ||
}, | ||
}, | ||
}, | ||
}, | ||
}}). | ||
ExecuteTest(t, func(s consumer.MockServerConfig) error { | ||
res, err := executeQuery(fmt.Sprintf("http://%s:%d", s.Host, s.Port)) | ||
|
||
fmt.Println(res) | ||
assert.NoError(t, err) | ||
assert.NotNil(t, res.Hero.ID) | ||
|
||
return nil | ||
}) | ||
|
||
assert.NoError(t, 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)"` | ||
} |
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
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