graphql: add a general test for the handler/resolvers

This commit is contained in:
Michael Muré 2018-09-26 15:55:14 +02:00
parent 879e147e2b
commit f969370901
No known key found for this signature in database
GPG Key ID: A4457C029293126F
13 changed files with 1794 additions and 0 deletions

17
Gopkg.lock generated
View File

@ -156,6 +156,14 @@
revision = "9e777a8366cce605130a531d2cd6363d07ad7317"
version = "v0.0.2"
[[projects]]
branch = "master"
digest = "1:645110e089152bd0f4a011a2648fbb0e4df5977be73ca605781157ac297f50c4"
name = "github.com/mitchellh/mapstructure"
packages = ["."]
pruneopts = "UT"
revision = "fa473d140ef3c6adf42d6b391fe76707f1f243c8"
[[projects]]
branch = "master"
digest = "1:c9b6e36dbd23f8403a04493376916ca5dad8c01b2da5ae0a05e6a468eb0b6f24"
@ -262,6 +270,14 @@
revision = "583c0c0531f06d5278b7d917446061adc344b5cd"
version = "v1.0.1"
[[projects]]
digest = "1:895fa0f564003de2498fbaf00be596fa814655ab445ce7cc215f7724bb6831ae"
name = "github.com/vektah/gqlgen"
packages = ["client"]
pruneopts = "UT"
revision = "636435b68700211441303f1a5ed92f3768ba5774"
version = "v0.5.1"
[[projects]]
branch = "master"
digest = "1:8150271279cc160a41e9aabfee8118c20a0e88894a25b2577f93e7c868e5259c"
@ -380,6 +396,7 @@
"github.com/skratchdot/open-golang/open",
"github.com/spf13/cobra",
"github.com/spf13/cobra/doc",
"github.com/vektah/gqlgen/client",
"github.com/vektah/gqlparser",
"github.com/vektah/gqlparser/ast",
"golang.org/x/crypto/ssh/terminal",

148
tests/graphql_test.go Normal file
View File

@ -0,0 +1,148 @@
package tests
import (
"net/http/httptest"
"testing"
"github.com/MichaelMure/git-bug/graphql"
"github.com/MichaelMure/git-bug/graphql/models"
"github.com/vektah/gqlgen/client"
)
func TestQueries(t *testing.T) {
repo := createFilledRepo(10)
handler, err := graphql.NewHandler(repo)
if err != nil {
t.Fatal(err)
}
srv := httptest.NewServer(handler)
c := client.New(srv.URL)
query := `
query {
defaultRepository {
allBugs(first: 2) {
pageInfo {
endCursor
hasNextPage
startCursor
hasPreviousPage
}
nodes{
author {
name
email
avatarUrl
}
createdAt
humanId
id
lastEdit
status
title
comments(first: 2) {
pageInfo {
endCursor
hasNextPage
startCursor
hasPreviousPage
}
nodes {
files
message
}
}
operations(first: 20) {
pageInfo {
endCursor
hasNextPage
startCursor
hasPreviousPage
}
nodes {
author {
name
email
avatarUrl
}
date
... on CreateOperation {
title
message
files
}
... on SetTitleOperation {
title
was
}
... on AddCommentOperation {
files
message
}
... on SetStatusOperation {
status
}
... on LabelChangeOperation {
added
removed
}
}
}
}
}
}
}`
type Person struct {
Name string `json:"name"`
Email string `json:"email"`
AvatarUrl string `json:"avatarUrl"`
}
var resp struct {
DefaultRepository struct {
AllBugs struct {
PageInfo models.PageInfo
Nodes []struct {
Author Person
CreatedAt string `json:"createdAt"`
HumandId string `json:"humanId"`
Id string
LastEdit string `json:"lastEdit"`
Status string
Title string
Comments struct {
PageInfo models.PageInfo
Nodes []struct {
Files []string
Message string
}
}
Operations struct {
PageInfo models.PageInfo
Nodes []struct {
Author Person
Date string
Title string
Files []string
Message string
Was string
Status string
Added []string
Removed []string
}
}
}
}
}
}
c.MustPost(query, &resp)
}

8
vendor/github.com/mitchellh/mapstructure/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,8 @@
language: go
go:
- 1.9.x
- tip
script:
- go test

21
vendor/github.com/mitchellh/mapstructure/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2013 Mitchell Hashimoto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

46
vendor/github.com/mitchellh/mapstructure/README.md generated vendored Normal file
View File

@ -0,0 +1,46 @@
# mapstructure [![Godoc](https://godoc.org/github.com/mitchellh/mapstructure?status.svg)](https://godoc.org/github.com/mitchellh/mapstructure)
mapstructure is a Go library for decoding generic map values to structures
and vice versa, while providing helpful error handling.
This library is most useful when decoding values from some data stream (JSON,
Gob, etc.) where you don't _quite_ know the structure of the underlying data
until you read a part of it. You can therefore read a `map[string]interface{}`
and use this library to decode it into the proper underlying native Go
structure.
## Installation
Standard `go get`:
```
$ go get github.com/mitchellh/mapstructure
```
## Usage & Example
For usage and examples see the [Godoc](http://godoc.org/github.com/mitchellh/mapstructure).
The `Decode` function has examples associated with it there.
## But Why?!
Go offers fantastic standard libraries for decoding formats such as JSON.
The standard method is to have a struct pre-created, and populate that struct
from the bytes of the encoded format. This is great, but the problem is if
you have configuration or an encoding that changes slightly depending on
specific fields. For example, consider this JSON:
```json
{
"type": "person",
"name": "Mitchell"
}
```
Perhaps we can't populate a specific structure without first reading
the "type" field from the JSON. We could always do two passes over the
decoding of the JSON (reading the "type" first, and the rest later).
However, it is much simpler to just decode this into a `map[string]interface{}`
structure, read the "type" key, then use something like this library
to decode it into the proper structure.

View File

@ -0,0 +1,171 @@
package mapstructure
import (
"errors"
"reflect"
"strconv"
"strings"
"time"
)
// typedDecodeHook takes a raw DecodeHookFunc (an interface{}) and turns
// it into the proper DecodeHookFunc type, such as DecodeHookFuncType.
func typedDecodeHook(h DecodeHookFunc) DecodeHookFunc {
// Create variables here so we can reference them with the reflect pkg
var f1 DecodeHookFuncType
var f2 DecodeHookFuncKind
// Fill in the variables into this interface and the rest is done
// automatically using the reflect package.
potential := []interface{}{f1, f2}
v := reflect.ValueOf(h)
vt := v.Type()
for _, raw := range potential {
pt := reflect.ValueOf(raw).Type()
if vt.ConvertibleTo(pt) {
return v.Convert(pt).Interface()
}
}
return nil
}
// DecodeHookExec executes the given decode hook. This should be used
// since it'll naturally degrade to the older backwards compatible DecodeHookFunc
// that took reflect.Kind instead of reflect.Type.
func DecodeHookExec(
raw DecodeHookFunc,
from reflect.Type, to reflect.Type,
data interface{}) (interface{}, error) {
switch f := typedDecodeHook(raw).(type) {
case DecodeHookFuncType:
return f(from, to, data)
case DecodeHookFuncKind:
return f(from.Kind(), to.Kind(), data)
default:
return nil, errors.New("invalid decode hook signature")
}
}
// ComposeDecodeHookFunc creates a single DecodeHookFunc that
// automatically composes multiple DecodeHookFuncs.
//
// The composed funcs are called in order, with the result of the
// previous transformation.
func ComposeDecodeHookFunc(fs ...DecodeHookFunc) DecodeHookFunc {
return func(
f reflect.Type,
t reflect.Type,
data interface{}) (interface{}, error) {
var err error
for _, f1 := range fs {
data, err = DecodeHookExec(f1, f, t, data)
if err != nil {
return nil, err
}
// Modify the from kind to be correct with the new data
f = nil
if val := reflect.ValueOf(data); val.IsValid() {
f = val.Type()
}
}
return data, nil
}
}
// StringToSliceHookFunc returns a DecodeHookFunc that converts
// string to []string by splitting on the given sep.
func StringToSliceHookFunc(sep string) DecodeHookFunc {
return func(
f reflect.Kind,
t reflect.Kind,
data interface{}) (interface{}, error) {
if f != reflect.String || t != reflect.Slice {
return data, nil
}
raw := data.(string)
if raw == "" {
return []string{}, nil
}
return strings.Split(raw, sep), nil
}
}
// StringToTimeDurationHookFunc returns a DecodeHookFunc that converts
// strings to time.Duration.
func StringToTimeDurationHookFunc() DecodeHookFunc {
return func(
f reflect.Type,
t reflect.Type,
data interface{}) (interface{}, error) {
if f.Kind() != reflect.String {
return data, nil
}
if t != reflect.TypeOf(time.Duration(5)) {
return data, nil
}
// Convert it by parsing
return time.ParseDuration(data.(string))
}
}
// StringToTimeHookFunc returns a DecodeHookFunc that converts
// strings to time.Time.
func StringToTimeHookFunc(layout string) DecodeHookFunc {
return func(
f reflect.Type,
t reflect.Type,
data interface{}) (interface{}, error) {
if f.Kind() != reflect.String {
return data, nil
}
if t != reflect.TypeOf(time.Time{}) {
return data, nil
}
// Convert it by parsing
return time.Parse(layout, data.(string))
}
}
// WeaklyTypedHook is a DecodeHookFunc which adds support for weak typing to
// the decoder.
//
// Note that this is significantly different from the WeaklyTypedInput option
// of the DecoderConfig.
func WeaklyTypedHook(
f reflect.Kind,
t reflect.Kind,
data interface{}) (interface{}, error) {
dataVal := reflect.ValueOf(data)
switch t {
case reflect.String:
switch f {
case reflect.Bool:
if dataVal.Bool() {
return "1", nil
}
return "0", nil
case reflect.Float32:
return strconv.FormatFloat(dataVal.Float(), 'f', -1, 64), nil
case reflect.Int:
return strconv.FormatInt(dataVal.Int(), 10), nil
case reflect.Slice:
dataType := dataVal.Type()
elemKind := dataType.Elem().Kind()
if elemKind == reflect.Uint8 {
return string(dataVal.Interface().([]uint8)), nil
}
case reflect.Uint:
return strconv.FormatUint(dataVal.Uint(), 10), nil
}
}
return data, nil
}

50
vendor/github.com/mitchellh/mapstructure/error.go generated vendored Normal file
View File

@ -0,0 +1,50 @@
package mapstructure
import (
"errors"
"fmt"
"sort"
"strings"
)
// Error implements the error interface and can represents multiple
// errors that occur in the course of a single decode.
type Error struct {
Errors []string
}
func (e *Error) Error() string {
points := make([]string, len(e.Errors))
for i, err := range e.Errors {
points[i] = fmt.Sprintf("* %s", err)
}
sort.Strings(points)
return fmt.Sprintf(
"%d error(s) decoding:\n\n%s",
len(e.Errors), strings.Join(points, "\n"))
}
// WrappedErrors implements the errwrap.Wrapper interface to make this
// return value more useful with the errwrap and go-multierror libraries.
func (e *Error) WrappedErrors() []error {
if e == nil {
return nil
}
result := make([]error, len(e.Errors))
for i, e := range e.Errors {
result[i] = errors.New(e)
}
return result
}
func appendErrors(errors []string, err error) []string {
switch e := err.(type) {
case *Error:
return append(errors, e.Errors...)
default:
return append(errors, e.Error())
}
}

1
vendor/github.com/mitchellh/mapstructure/go.mod generated vendored Normal file
View File

@ -0,0 +1 @@
module github.com/mitchellh/mapstructure

1064
vendor/github.com/mitchellh/mapstructure/mapstructure.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

19
vendor/github.com/vektah/gqlgen/LICENSE generated vendored Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2018 Adam Scarr
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

141
vendor/github.com/vektah/gqlgen/client/client.go generated vendored Normal file
View File

@ -0,0 +1,141 @@
// client is used internally for testing. See readme for alternatives
package client
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"github.com/mitchellh/mapstructure"
)
// Client for graphql requests
type Client struct {
url string
client *http.Client
}
// New creates a graphql client
func New(url string, client ...*http.Client) *Client {
p := &Client{
url: url,
}
if len(client) > 0 {
p.client = client[0]
} else {
p.client = http.DefaultClient
}
return p
}
type Request struct {
Query string `json:"query"`
Variables map[string]interface{} `json:"variables,omitempty"`
OperationName string `json:"operationName,omitempty"`
}
type Option func(r *Request)
func Var(name string, value interface{}) Option {
return func(r *Request) {
if r.Variables == nil {
r.Variables = map[string]interface{}{}
}
r.Variables[name] = value
}
}
func Operation(name string) Option {
return func(r *Request) {
r.OperationName = name
}
}
func (p *Client) MustPost(query string, response interface{}, options ...Option) {
if err := p.Post(query, response, options...); err != nil {
panic(err)
}
}
func (p *Client) mkRequest(query string, options ...Option) Request {
r := Request{
Query: query,
}
for _, option := range options {
option(&r)
}
return r
}
func (p *Client) Post(query string, response interface{}, options ...Option) (resperr error) {
r := p.mkRequest(query, options...)
requestBody, err := json.Marshal(r)
if err != nil {
return fmt.Errorf("encode: %s", err.Error())
}
rawResponse, err := p.client.Post(p.url, "application/json", bytes.NewBuffer(requestBody))
if err != nil {
return fmt.Errorf("post: %s", err.Error())
}
defer func() {
_ = rawResponse.Body.Close()
}()
if rawResponse.StatusCode >= http.StatusBadRequest {
responseBody, _ := ioutil.ReadAll(rawResponse.Body)
return fmt.Errorf("http %d: %s", rawResponse.StatusCode, responseBody)
}
responseBody, err := ioutil.ReadAll(rawResponse.Body)
if err != nil {
return fmt.Errorf("read: %s", err.Error())
}
// decode it into map string first, let mapstructure do the final decode
// because it can be much stricter about unknown fields.
respDataRaw := struct {
Data interface{}
Errors json.RawMessage
}{}
err = json.Unmarshal(responseBody, &respDataRaw)
if err != nil {
return fmt.Errorf("decode: %s", err.Error())
}
// we want to unpack even if there is an error, so we can see partial responses
unpackErr := unpack(respDataRaw.Data, response)
if respDataRaw.Errors != nil {
return RawJsonError{respDataRaw.Errors}
}
return unpackErr
}
type RawJsonError struct {
json.RawMessage
}
func (r RawJsonError) Error() string {
return string(r.RawMessage)
}
func unpack(data interface{}, into interface{}) error {
d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
Result: into,
TagName: "json",
ErrorUnused: true,
ZeroFields: true,
})
if err != nil {
return fmt.Errorf("mapstructure: %s", err.Error())
}
return d.Decode(data)
}

5
vendor/github.com/vektah/gqlgen/client/readme.md generated vendored Normal file
View File

@ -0,0 +1,5 @@
This client is used internally for testing. I wanted a simple graphql client sent user specified queries.
You might want to look at:
- https://github.com/shurcooL/graphql: Uses reflection to build queries from structs.
- https://github.com/machinebox/graphql: Probably would have been a perfect fit, but it uses form encoding instead of json...

103
vendor/github.com/vektah/gqlgen/client/websocket.go generated vendored Normal file
View File

@ -0,0 +1,103 @@
package client
import (
"encoding/json"
"fmt"
"strings"
"github.com/gorilla/websocket"
"github.com/vektah/gqlparser/gqlerror"
)
const (
connectionInitMsg = "connection_init" // Client -> Server
startMsg = "start" // Client -> Server
connectionAckMsg = "connection_ack" // Server -> Client
dataMsg = "data" // Server -> Client
errorMsg = "error" // Server -> Client
)
type operationMessage struct {
Payload json.RawMessage `json:"payload,omitempty"`
ID string `json:"id,omitempty"`
Type string `json:"type"`
}
type Subscription struct {
Close func() error
Next func(response interface{}) error
}
func errorSubscription(err error) *Subscription {
return &Subscription{
Close: func() error { return nil },
Next: func(response interface{}) error {
return err
},
}
}
func (p *Client) Websocket(query string, options ...Option) *Subscription {
r := p.mkRequest(query, options...)
requestBody, err := json.Marshal(r)
if err != nil {
return errorSubscription(fmt.Errorf("encode: %s", err.Error()))
}
url := strings.Replace(p.url, "http://", "ws://", -1)
url = strings.Replace(url, "https://", "wss://", -1)
c, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
return errorSubscription(fmt.Errorf("dial: %s", err.Error()))
}
if err = c.WriteJSON(operationMessage{Type: connectionInitMsg}); err != nil {
return errorSubscription(fmt.Errorf("init: %s", err.Error()))
}
var ack operationMessage
if err = c.ReadJSON(&ack); err != nil {
return errorSubscription(fmt.Errorf("ack: %s", err.Error()))
}
if ack.Type != connectionAckMsg {
return errorSubscription(fmt.Errorf("expected ack message, got %#v", ack))
}
if err = c.WriteJSON(operationMessage{Type: startMsg, ID: "1", Payload: requestBody}); err != nil {
return errorSubscription(fmt.Errorf("start: %s", err.Error()))
}
return &Subscription{
Close: c.Close,
Next: func(response interface{}) error {
var op operationMessage
c.ReadJSON(&op)
if op.Type != dataMsg {
if op.Type == errorMsg {
return fmt.Errorf(string(op.Payload))
} else {
return fmt.Errorf("expected data message, got %#v", op)
}
}
respDataRaw := map[string]interface{}{}
err = json.Unmarshal(op.Payload, &respDataRaw)
if err != nil {
return fmt.Errorf("decode: %s", err.Error())
}
if respDataRaw["errors"] != nil {
var errs []*gqlerror.Error
if err = unpack(respDataRaw["errors"], &errs); err != nil {
return err
}
if len(errs) > 0 {
return fmt.Errorf("errors: %s", errs)
}
}
return unpack(respDataRaw["data"], response)
},
}
}