From 7ad04d7750be3ffcb3692aa9485b001e92df020f Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Sat, 2 Oct 2021 16:46:19 +0100 Subject: [PATCH] Initial commit. --- Makefile | 29 ++- go.mod | 26 ++- go.sum | 64 ++++--- gql.go | 115 ++++------- gql_test.go | 160 +++------------- pkg/logging/logging.go | 76 ++++++++ pkg/logging/logging_test.go | 373 ++++++++++++++++++++++++++++++++++++ query.go | 86 +++++++++ query_bench_test.go | 66 +++++++ query_test.go | 109 +++++++++++ 10 files changed, 845 insertions(+), 259 deletions(-) create mode 100644 pkg/logging/logging.go create mode 100644 pkg/logging/logging_test.go create mode 100644 query.go create mode 100644 query_bench_test.go create mode 100644 query_test.go diff --git a/Makefile b/Makefile index 30811bc..f7d9af3 100644 --- a/Makefile +++ b/Makefile @@ -1,26 +1,21 @@ CI_RUN?=false ADDITIONAL_BUILD_FLAGS="" -SET_PRIVATE="github.com/telegram-bot-app/*" ifeq ($(CI_RUN), true) - ADDITIONAL_BUILD_FLAGS="-test.short" + ADDITIONAL_BUILD_FLAGS="-test.local_endpoint" endif -prepare: - GOPRIVATE=$(SET_PRIVATE) go get github.com/go-critic/go-critic/cmd/gocritic - gocritic check +.PHONY: help +help: ## display this help + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n\nTargets:\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST) -test: lint tidy - GOPRIVATE=$(SET_PRIVATE) go test -race $(ADDITIONAL_BUILD_FLAGS) -cover -coverprofile=coverage.out +.PHONY: test +test: ## run tests on library + @go test $(ADDITIONAL_BUILD_FLAGS) -v -cover ./... -lint: - go fmt +.PHONY: test-packages +test-packages: ## run tests on packages + @go test -v -cover ./pkg/... -readme: - godocdown > README.md - -update: - GOPRIVATE=$(SET_PRIVATE) go get -u ./... - -tidy: - GOPRIVATE=$(SET_PRIVATE) go mod tidy \ No newline at end of file +.PHONY: all +all: test-packages test \ No newline at end of file diff --git a/go.mod b/go.mod index 1260a91..2f47801 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,22 @@ -module github.com/lukaszraczylo/simple-gql-client +module github.com/lukaszraczylo/go-simple-graphql -go 1.16 +go 1.17 require ( - github.com/andybalholm/brotli v1.0.3 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/klauspost/compress v1.13.6 // indirect - github.com/lukaszraczylo/pandati v0.0.10 // indirect + github.com/buger/jsonparser v1.1.1 + github.com/json-iterator/go v1.1.12 + github.com/lukaszraczylo/pandati v0.0.10 + github.com/rs/zerolog v1.25.0 github.com/stretchr/testify v1.7.0 - github.com/tidwall/gjson v1.9.1 - github.com/valyala/fasthttp v1.30.0 - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/imdario/mergo v0.3.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/text v0.3.6 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect ) diff --git a/go.sum b/go.sum index b3a0305..aea4c7b 100644 --- a/go.sum +++ b/go.sum @@ -1,46 +1,62 @@ -github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= -github.com/andybalholm/brotli v1.0.3 h1:fpcw+r1N1h0Poc1F/pHbW40cUm/lMEQslZtCkBQ0UnM= -github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/lukaszraczylo/pandati v0.0.10 h1:ZXmLjxb/UYRBVyAKwXmUcJgZ2FYNwGY4YDJsQQMYAXI= github.com/lukaszraczylo/pandati v0.0.10/go.mod h1:1lE/x4X4LfO1yerAQoiR7AVb/wIabH/zITDGnqTdyH8= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.25.0 h1:Rj7XygbUHKUlDPcVdoLyR91fJBsduXj5fRxyqIQj/II= +github.com/rs/zerolog v1.25.0/go.mod h1:7KHcEGe0QZPOm2IE4Kpb5rTh6n1h2hIgS5OOnu1rUaI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/tidwall/gjson v1.9.1 h1:wrrRk7TyL7MmKanNRck/Mcr3VU1sdMvJHvJXzqBIUNo= -github.com/tidwall/gjson v1.9.1/go.mod h1:jydLKE7s8J0+1/5jC4eXcuFlzKizGrCKvLmBVX/5oXc= -github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE= -github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= -github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.30.0 h1:nBNzWrgZUUHohyLPU/jTvXdhrcaf2m5k3bWk+3Q049g= -github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus= -github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= -golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6 h1:Z04ewVs7JhXaYkmDhBERPi41gnltfQpMWDnTnQbaCqk= +golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/gql.go b/gql.go index b951ec8..e70574a 100644 --- a/gql.go +++ b/gql.go @@ -1,92 +1,61 @@ -// Package / library or rather wrapper for GraphQL queries execution in the painless way. -// Using it is as easy as copy / paste the query itself and set appropriate variables in. +// Package go-simple-graphql is a wrapper for GraphQL queries execution. +// It's sole purpose is to allow developer to execute queries without specific fields / types mapping +// which can be really confusing and even impossible whilst dealing with Hasura custom generated types. // -// Library supports basic error reporting on unsuccessful queries and setting appropriate headers. +// Library supports advanced error reporting on unsuccessful queries. +// Library also uses HTTP2 for communication with GraphQL API if it's supported by server + +// Environment variables: +// GRAPHQL_ENDPOINT - GraphQL endpoint to use. Default: http://127.0.0.1:9090/v1/graphql +// LOG_LEVEL - Log level to use. Default: INFO +// Available log levels: DEBUG, INFO, WARN, ERROR, FATAL + package gql import ( - "bytes" - "encoding/json" - "errors" + "crypto/tls" "fmt" + "net" + "net/http" "os" - "github.com/lukaszraczylo/pandati" - "github.com/tidwall/gjson" - "github.com/valyala/fasthttp" + jsoniter "github.com/json-iterator/go" + "github.com/lukaszraczylo/go-simple-graphql/pkg/logging" + "golang.org/x/net/http2" ) -// Endpoint of your GraphQL server to query -// this variable can be overwritten by setting env variable, for example: -// GRAPHQL_ENDPOINT=http://hasura.local/v1/graphql -var GraphQLUrl string +var ( + json = jsoniter.ConfigCompatibleWithStandardLibrary +) -type requestBase struct { - Query string `json:"query"` - Variables interface{} `json:"variables"` +type connHandler struct { + Endpoint string + HttpClient *http.Client + Log *logging.LogConfig } -func prepare() { +func pickGraphqlEndpoint() (graphqlEndpoint string) { value, present := os.LookupEnv("GRAPHQL_ENDPOINT") - if present { - GraphQLUrl = value - } else if !present && pandati.IsZero(GraphQLUrl) { - GraphQLUrl = "http://127.0.0.1:9090/v1/graphql" - fmt.Println("Setting default endpoint", GraphQLUrl) + if present && value != "" { + graphqlEndpoint = value } else { - fmt.Println("GraphQL endpoint not set.") + graphqlEndpoint = "http://127.0.0.1:9090/v1/graphql" + fmt.Println("Setting default endpoint", graphqlEndpoint) } + return graphqlEndpoint } -// queryBuilder takes query data (string) and variables (interface) as a parameter and assembles -// graphQL query. It also compacts the JSON result. Function returns query as []byte and error if anything went wrong. -func queryBuilder(data string, variables interface{}) ([]byte, error) { - var err error - var qb = &requestBase{ - Query: data, - Variables: variables, +func NewConnection() *connHandler { + return &connHandler{ + Endpoint: pickGraphqlEndpoint(), + HttpClient: &http.Client{ + Transport: &http2.Transport{ + AllowHTTP: true, + DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) { + return net.Dial(network, addr) + }, + }, + }, + Log: logging.NewLogger(), } - j := new(bytes.Buffer) - j2, err := json.Marshal(qb) - if err != nil { - return []byte{}, err - } - if err = json.Compact(j, j2); err != nil { - return []byte{}, err - } - return j.Bytes(), err -} - -// Query allows you to execute the GraphQL query. -// Query is a string ( copy paste from Hasura or any other query builder ) -// Variables and Headers are maps of strings ( see the example ) -// Function returns whatever specified query returns and/or error. -func Query(query string, variables interface{}, headers map[string]interface{}) (string, error) { - prepare() - var err error - readyQuery, err := queryBuilder(query, variables) - if err != nil { - return "", err - } - req := fasthttp.AcquireRequest() - defer fasthttp.ReleaseRequest(req) - req.Header.SetContentType("application/json") - for header, value := range headers { - req.Header.Add(fmt.Sprintf("%v", header), fmt.Sprintf("%v", value)) - } - req.Header.SetMethod("POST") - req.SetBody(readyQuery) - req.SetRequestURI(GraphQLUrl) - res := fasthttp.AcquireResponse() - defer fasthttp.ReleaseResponse(res) - if err := fasthttp.Do(req, res); err != nil { - return "", err - } - body := res.Body() - toReturn := gjson.Get(string(body), "data") - if toReturn.String() == "" { - err = errors.New(string(body)) - return "", err - } - return toReturn.String(), err } diff --git a/gql_test.go b/gql_test.go index d80daae..e9be547 100644 --- a/gql_test.go +++ b/gql_test.go @@ -1,17 +1,23 @@ package gql import ( - "fmt" "net/http" "net/http/httptest" "os" "testing" - "github.com/lukaszraczylo/pandati" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" ) +func mockGraphQLServerResponses(responseQuery string) (*httptest.Server, string) { + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.Write([]byte(responseQuery)) + })) + serverURL := server.URL + "/v1/graphql" + return server, serverURL +} + type TestSuite struct { suite.Suite } @@ -24,156 +30,38 @@ func TestSuiteRun(t *testing.T) { suite.Run(t, new(TestSuite)) } -func mockGraphQLServerResponses(responseQuery string) *httptest.Server { - server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - rw.Write([]byte(responseQuery)) - })) - GraphQLUrl = server.URL + "/v1/graphql" - return server -} - -func (suite *TestSuite) Test_queryBuilder() { +func (suite *TestSuite) TestNewConnection() { type args struct { - data string - variables interface{} + endpoint string + debug bool } tests := []struct { name string args args - want string + want *connHandler }{ { - name: "Simple query", + name: "New connection: Env variable endpoint", args: args{ - data: `query listUserBots { tbl_bots { bot_name } }`, - variables: nil, + endpoint: "http://localhost:8080/graphql", + }, + want: &connHandler{ + Endpoint: "http://localhost:8080/graphql", }, - want: `{"query":"query listUserBots { tbl_bots { bot_name } }","variables":null}`, }, { - name: "Advanced query", - args: args{ - data: `query checkifUserIsAdmin($UserID: bigint, $GroupID: bigint) { tbl_user_group_admins(where: {is_admin: {_eq: true}, user_id: {_eq: $UserID}, group_id: {_eq: $GroupID}}) { id is_admin } }`, - variables: map[string]interface{}{ - "UserID": 37, - "GroupID": 11007, - }, + name: "New connection: No env variable endpoint", + args: args{}, + want: &connHandler{ + Endpoint: "http://127.0.0.1:9090/v1/graphql", }, - want: `{"query":"query checkifUserIsAdmin($UserID: bigint, $GroupID: bigint) { tbl_user_group_admins(where: {is_admin: {_eq: true}, user_id: {_eq: $UserID}, group_id: {_eq: $GroupID}}) { id is_admin } }","variables":{"GroupID":11007,"UserID":37}}`, }, } for _, tt := range tests { suite.T().Run(tt.name, func(t *testing.T) { - gets, err := queryBuilder(tt.args.data, tt.args.variables) - assert.Equal(suite.T(), tt.want, string(gets), "Unexpected query output in test: "+tt.name) - assert.Nil(suite.T(), err) - }, - ) - } -} - -func (suite *TestSuite) Test_queryAgainstDatabaseExecution() { - os.Unsetenv("GRAPHQL_ENDPOINT") - jsonResponse := `{ - "data": {"tbl_user_group_admins":[{"id":109,"is_admin":true}]} - }` - server := mockGraphQLServerResponses(jsonResponse) - defer server.Close() - headers := map[string]interface{}{ - "x-hasura-user-id": 37, - "x-hasura-user-uuid": "bde3262e-b42e-4151-ac10-d43fb38f44a5", - "Authorization": "bearer LaPotatoDiBanani", - } - variables := map[string]interface{}{ - "UserID": 37, - "GroupID": 11007, - } - var query = `query checkifUserIsAdmin($UserID: bigint, $GroupID: bigint) { - tbl_user_group_admins(where: {is_admin: {_eq: true}, user_id: {_eq: $UserID}, group_id: {_eq: $GroupID}}) { - id - is_admin - } - }` - result, err := Query(query, variables, headers) - if err != nil { - suite.T().Log("Query execution errored. Is GQL server up?") - } - expected := `{"tbl_user_group_admins":[{"id":109,"is_admin":true}]}` - assert.Equal(suite.T(), expected, string(result), "Query result execution should be equal") -} - -func ExampleQuery() { - variables := map[string]interface{}{ - "fileHash": "123deadc0w321", - } - var query = `query searchFileKnown($fileHash: String) { - tbl_file_scans(where: {file_hash: {_eq: $fileHash}}) { - porn - racy - violence - virus - } - }` - result, err := Query(query, variables, nil) - if err != nil { - fmt.Println("Query error", err) - return - } - fmt.Println(result) -} - -func (suite *TestSuite) Test_initialize() { - tests := []struct { - name string - expected string - env_endpoint string - local_endpoint string - }{ - { - name: "Setting env variable for endpoint (ENV)", - env_endpoint: "new-hasura.local/v1/graphql", - expected: "new-hasura.local/v1/graphql", - }, - { - name: "Setting default endpoint (ENV)", - env_endpoint: "", - local_endpoint: "", - expected: "http://127.0.0.1:9090/v1/graphql", - }, - { - name: "Setting custom endpoint (ENV)", - env_endpoint: "http://127.0.0.1:8080/v1/graphql", - expected: "http://127.0.0.1:8080/v1/graphql", - }, - { - name: "Setting custom endpoint (VAR)", - local_endpoint: "http://127.0.0.1:8090/v1/graphql", - expected: "http://127.0.0.1:8090/v1/graphql", - }, - { - name: "Setting custom endpoint (VAR)", - local_endpoint: "hasura-def.local/v1/graphql", - expected: "hasura-def.local/v1/graphql", - }, - } - backupEnv, present := os.LookupEnv("GRAPHQL_ENDPOINT") - for _, tt := range tests { - suite.T().Run(tt.name, func(t *testing.T) { - os.Unsetenv("GRAPHQL_ENDPOINT") - GraphQLUrl = "" - if !pandati.IsZero(tt.env_endpoint) { - os.Setenv("GRAPHQL_ENDPOINT", tt.env_endpoint) - t.Log(tt.env_endpoint, os.Getenv("GRAPHQL_ENDPOINT")) - } - if !pandati.IsZero(tt.local_endpoint) { - os.Unsetenv("GRAPHQL_ENDPOINT") - GraphQLUrl = tt.local_endpoint - } - prepare() - assert.Equal(suite.T(), tt.expected, GraphQLUrl, "Unexpected value of env variable: "+tt.name) + os.Setenv("GRAPHQL_ENDPOINT", tt.args.endpoint) + got := NewConnection() + assert.Equal(t, tt.want.Endpoint, got.Endpoint) }) } - if present { - os.Setenv("GRAPHQL_ENDPOINT", backupEnv) - } } diff --git a/pkg/logging/logging.go b/pkg/logging/logging.go new file mode 100644 index 0000000..51604f7 --- /dev/null +++ b/pkg/logging/logging.go @@ -0,0 +1,76 @@ +package logging + +import ( + stdlog "log" + "os" + "time" + + "github.com/lukaszraczylo/pandati" + "github.com/rs/zerolog" +) + +type LogConfig struct { + DefaultOutput *os.File + Logger zerolog.Logger +} + +func NewLogger() *LogConfig { + log := new(LogConfig) + switch dll := os.Getenv("LOG_LEVEL"); dll { + case "debug": + zerolog.SetGlobalLevel(zerolog.DebugLevel) + case "warn": + zerolog.SetGlobalLevel(zerolog.WarnLevel) + case "error": + zerolog.SetGlobalLevel(zerolog.ErrorLevel) + default: + zerolog.SetGlobalLevel(zerolog.InfoLevel) + } + zerolog.TimeFieldFormat = time.RFC3339 + zerolog.MessageFieldName = "short_message" + zerolog.TimestampFieldName = "timestamp" + zerolog.LevelFatalValue = "critical" + log.Logger = log.Logger.With().Timestamp().Logger() + stdlog.SetFlags(0) + stdlog.SetOutput(log.Logger) + return log +} + +func (l *LogConfig) Critical(message string, v ...map[string]interface{}) { + w := l.Logger.Output(os.Stderr) + log(w.Fatal(), message, v...) +} + +func (l *LogConfig) Error(message string, v ...map[string]interface{}) { + w := l.Logger.Output(os.Stderr) + log(w.Error(), message, v...) +} + +func (l *LogConfig) Warning(message string, v ...map[string]interface{}) { + w := l.Logger.Output(os.Stdout) + log(w.Warn(), message, v...) +} + +func (l *LogConfig) Info(message string, v ...map[string]interface{}) { + w := l.Logger.Output(os.Stdout) + log(w.Info(), message, v...) +} + +func (l *LogConfig) Debug(message string, v ...map[string]interface{}) { + w := l.Logger.Output(os.Stdout) + log(w.Debug(), message, v...) +} + +func log(contextLog *zerolog.Event, message string, v ...map[string]interface{}) { + if len(v) > 0 { + for _, m := range v { + for k, v := range m { + if !pandati.IsZero(v) { + contextLog.Str(k, v.(string)) + } + } + } + } + // contextLog.Str("_stack_trace", pandati.Trace()) + contextLog.Msg(message) +} diff --git a/pkg/logging/logging_test.go b/pkg/logging/logging_test.go new file mode 100644 index 0000000..159e118 --- /dev/null +++ b/pkg/logging/logging_test.go @@ -0,0 +1,373 @@ +package logging + +import ( + "errors" + "io/ioutil" + "os" + "reflect" + "testing" + + "github.com/buger/jsonparser" + "github.com/stretchr/testify/suite" +) + +type LoggingTestSuite struct { + suite.Suite +} + +var ( + testsLogger *LogConfig +) + +type stdoutCapture struct { + oldStdout *os.File + readPipe *os.File +} + +func (sc *stdoutCapture) StartCapture() { + sc.oldStdout = os.Stdout + sc.readPipe, os.Stdout, _ = os.Pipe() +} + +func (sc *stdoutCapture) StopCapture() (string, error) { + if sc.oldStdout == nil || sc.readPipe == nil { + return "", errors.New("StartCapture not called before StopCapture on Stdout") + } + os.Stdout.Close() + os.Stdout = sc.oldStdout + bytes, err := ioutil.ReadAll(sc.readPipe) + if err != nil { + return "", err + } + return string(bytes), nil +} + +type stderrCapture struct { + oldStderr *os.File + readPipe *os.File +} + +func (sc *stderrCapture) StartCapture() { + sc.oldStderr = os.Stderr + sc.readPipe, os.Stderr, _ = os.Pipe() +} + +func (sc *stderrCapture) StopCapture() (string, error) { + if sc.oldStderr == nil || sc.readPipe == nil { + return "", errors.New("StartCapture not called before StopCapture on Stderr") + } + os.Stderr.Close() + os.Stderr = sc.oldStderr + bytes, err := ioutil.ReadAll(sc.readPipe) + if err != nil { + return "", err + } + return string(bytes), nil +} + +func (suite *LoggingTestSuite) SetupTest() { +} + +func TestLoggingTestSuite(t *testing.T) { + suite.Run(t, new(LoggingTestSuite)) +} + +func (suite *LoggingTestSuite) TestLogConfig_AllHandlers() { + type args struct { + message string + } + tests := []struct { + name string + args args + wantLevel string + wantMessage string + envMinLogLevel string + stdOutExpect bool + stdErrExpect bool + loggerType string + }{ + { + name: "Test log: Error", + loggerType: "Error", + args: args{ + message: "This is a error message", + }, + wantLevel: "error", + wantMessage: "This is a error message", + stdErrExpect: true, + stdOutExpect: false, + }, + { + name: "Test log: Warning", + loggerType: "Warning", + args: args{ + message: "This is a warning message", + }, + wantLevel: "warn", + wantMessage: "This is a warning message", + stdErrExpect: false, + stdOutExpect: true, + envMinLogLevel: "info", + }, + { + name: "Test log: Warning | Min level: Debug", + loggerType: "Warning", + args: args{ + message: "This is a warning message", + }, + wantLevel: "warn", + wantMessage: "This is a warning message", + stdErrExpect: false, + stdOutExpect: true, + envMinLogLevel: "debug", + }, + { + name: "Test log: Info", + loggerType: "Info", + args: args{ + message: "This is a info message", + }, + wantLevel: "info", + wantMessage: "This is a info message", + stdErrExpect: false, + stdOutExpect: true, + }, + { + name: "Test log: Info | Min level: Warn", + loggerType: "Info", + args: args{ + message: "This is a info message", + }, + wantLevel: "", + wantMessage: "", + stdErrExpect: false, + stdOutExpect: false, + envMinLogLevel: "warn", + }, + { + name: "Test log: Warning | Min level: Warn", + loggerType: "Warning", + args: args{ + message: "This is a warning message", + }, + wantLevel: "warn", + wantMessage: "This is a warning message", + stdErrExpect: false, + stdOutExpect: true, + envMinLogLevel: "warn", + }, + { + name: "Test log: Warning | Min level: Error", + loggerType: "Warning", + args: args{ + message: "This is an error message", + }, + wantLevel: "", + wantMessage: "", + stdErrExpect: false, + stdOutExpect: false, + envMinLogLevel: "error", + }, + { + name: "Test log: Debug | Min level: Debug", + loggerType: "Debug", + args: args{ + message: "This is a debug message", + }, + wantLevel: "debug", + wantMessage: "This is a debug message", + stdErrExpect: false, + stdOutExpect: true, + envMinLogLevel: "debug", + }, + } + + for _, tt := range tests { + suite.T().Run(tt.name, func(t *testing.T) { + if tt.envMinLogLevel != "" { + os.Setenv("LOG_LEVEL", tt.envMinLogLevel) + defer os.Unsetenv("LOG_LEVEL") + } + testsLogger = NewLogger() + + captureStdout := stdoutCapture{} + captureStdout.StartCapture() + captureStderr := stderrCapture{} + captureStderr.StartCapture() + + reflect.ValueOf(testsLogger).MethodByName(tt.loggerType).Call([]reflect.Value{reflect.ValueOf(tt.args.message)}) + + stdoutOut, err := captureStdout.StopCapture() + if err != nil { + suite.T().Fatal(err) + } + + stderrOut, err := captureStderr.StopCapture() + if err != nil { + suite.T().Fatal(err) + } + + if tt.stdErrExpect && !tt.stdOutExpect { + gotLvl, gotMsg, err := getResponseValues(stderrOut, "short_message") + suite.NoError(err, "Failed in [STDERR]: "+tt.name) + suite.Equal(tt.wantLevel, gotLvl, "Failed in [STDERR]: "+tt.name) + suite.Equal(tt.wantMessage, gotMsg, "Failed in [STDERR]: "+tt.name) + suite.Equal("", stdoutOut, "Failed in [STDERR]: "+tt.name) + } + if tt.stdOutExpect && !tt.stdErrExpect { + gotLvl, gotMsg, err := getResponseValues(stdoutOut, "short_message") + suite.NoError(err, "Failed in [STDOUT]: "+tt.name) + suite.Equal(tt.wantLevel, gotLvl, "Failed in [STDOUT]: "+tt.name) + suite.Equal(tt.wantMessage, gotMsg, "Failed in [STDOUT]: "+tt.name) + suite.Equal("", stderrOut, "Failed in [STDOUT]: "+tt.name) + } + if !tt.stdErrExpect && !tt.stdOutExpect { + suite.Equal("", stderrOut, "Failed in [NEITHER]: "+tt.name) + suite.Equal("", stdoutOut, "Failed in [NEITHER]: "+tt.name) + } + os.Unsetenv("LOG_LEVEL") + }) + } +} + +func (suite *LoggingTestSuite) TestFullMessage() { + type args struct { + message string + extraFields map[string]interface{} + } + extraFields := make(map[string]interface{}) + extraFields["_full_message"] = "full message" + + tests := []struct { + name string + args args + wantLevel string + wantMessage string + envMinLogLevel string + stdOutExpect bool + stdErrExpect bool + loggerType string + }{ + { + name: "Test log: Error", + loggerType: "Error", + args: args{ + message: "This is a error message", + extraFields: extraFields, + }, + wantLevel: "error", + wantMessage: extraFields["_full_message"].(string), + stdErrExpect: true, + stdOutExpect: false, + }, + { + name: "Test log: Info", + loggerType: "Info", + args: args{ + message: "This is a info message", + extraFields: extraFields, + }, + wantMessage: extraFields["_full_message"].(string), + stdErrExpect: false, + stdOutExpect: true, + }, + } + + for _, tt := range tests { + suite.T().Run(tt.name, func(t *testing.T) { + if tt.envMinLogLevel != "" { + os.Setenv("LOG_LEVEL", tt.envMinLogLevel) + defer os.Unsetenv("LOG_LEVEL") + } + testsLogger = NewLogger() + + captureStdout := stdoutCapture{} + captureStdout.StartCapture() + captureStderr := stderrCapture{} + captureStderr.StartCapture() + + reflect.ValueOf(testsLogger).MethodByName(tt.loggerType).Call([]reflect.Value{ + reflect.ValueOf(tt.args.message), + reflect.ValueOf(tt.args.extraFields), + }) + + stdoutOut, err := captureStdout.StopCapture() + if err != nil { + suite.T().Fatal(err) + } + + stderrOut, err := captureStderr.StopCapture() + if err != nil { + suite.T().Fatal(err) + } + + if tt.stdErrExpect && !tt.stdOutExpect { + _, gotMsg, err := getResponseValues(stderrOut, "_full_message") + suite.NoError(err, "Failed in [STDERR]: "+tt.name) + suite.Equal(tt.wantMessage, gotMsg, "Failed in [STDERR]: "+tt.name) + } + if tt.stdOutExpect && !tt.stdErrExpect { + _, gotMsg, err := getResponseValues(stdoutOut, "_full_message") + suite.NoError(err, "Failed in [STDOUT]: "+tt.name) + suite.Equal(tt.wantMessage, gotMsg, "Failed in [STDOUT]: "+tt.name) + } + os.Unsetenv("LOG_LEVEL") + }) + } +} + +func Test_getResponseValues(t *testing.T) { + type args struct { + sourceJson string + } + tests := []struct { + name string + args args + wantGotLvl string + wantGotMsg string + wantErr bool + }{ + { + name: "Test with json", + args: args{ + sourceJson: `{"level": "debug", "short_message": "hello world"`, + }, + wantGotLvl: "debug", + wantGotMsg: "hello world", + wantErr: false, + }, + { + name: "Test with json, wrong message field", + args: args{ + sourceJson: `{"level": "debug", "message": "hello world"`, + }, + wantGotLvl: "debug", + wantGotMsg: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotGotLvl, gotGotMsg, err := getResponseValues(tt.args.sourceJson, "short_message") + if (err != nil) != tt.wantErr { + t.Errorf("getResponseValues() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotGotLvl != tt.wantGotLvl { + t.Errorf("getResponseValues() gotGotLvl = %v, want %v", gotGotLvl, tt.wantGotLvl) + } + if gotGotMsg != tt.wantGotMsg { + t.Errorf("getResponseValues() gotGotMsg = %v, want %v", gotGotMsg, tt.wantGotMsg) + } + }) + } +} + +func getResponseValues(sourceJson string, key string) (gotLvl, gotMsg string, err error) { + gotLvl, err = jsonparser.GetString([]byte(sourceJson), "level") + if err != nil { + return + } + gotMsg, err = jsonparser.GetString([]byte(sourceJson), key) + return +} diff --git a/query.go b/query.go new file mode 100644 index 0000000..81491a6 --- /dev/null +++ b/query.go @@ -0,0 +1,86 @@ +package gql + +import ( + "bytes" + "errors" + "fmt" + "io/ioutil" + + "github.com/lukaszraczylo/pandati" +) + +type requestBase struct { + Query string `json:"query"` + Variables interface{} `json:"variables"` +} + +type queryResults struct { + Data interface{} `json:"data"` + Errors []struct { + Message interface{} `json:"message"` + } `json:"errors"` +} + +func (g *connHandler) queryBuilder(queryContent string, queryVariables interface{}) ([]byte, error) { + var qb = &requestBase{ + Query: queryContent, + Variables: queryVariables, + } + + // j := new(bytes.Buffer) + j2, err := json.Marshal(qb) + if err != nil { + g.Log.Critical("Unable to marshal the query", map[string]interface{}{"_error": err.Error(), "_query": queryContent, "_variables": queryVariables}) + return []byte{}, err + } + // if err = json.Compact(j, j2); err != nil { + // g.Log.Critical("Unable to compact the query", map[string]interface{}{"error": err.Error()}) + // return []byte{}, err + // } + return j2, err +} + +func (g *connHandler) Query(queryContent string, queryVariables interface{}) (responseContent string, err error) { + g.Log.Debug("Query details", map[string]interface{}{"_query": queryContent, "_variables": queryVariables}) + query, err := g.queryBuilder(queryContent, queryVariables) + if err != nil { + g.Log.Error("Unable to build the query", map[string]interface{}{"_error": err.Error()}) + return "", err + } + httpResponse, err := g.HttpClient.Post(g.Endpoint, "application/json", bytes.NewBuffer(query)) + if err != nil { + g.Log.Error("Unable to send the query", map[string]interface{}{"_error": err.Error()}) + return "", err + } + defer httpResponse.Body.Close() + + body, err := ioutil.ReadAll(httpResponse.Body) + if err != nil { + g.Log.Critical("Unable to read the response", map[string]interface{}{"_error": err.Error()}) + return "", err + } + var queryResult *queryResults + err = json.Unmarshal(body, &queryResult) + if err != nil { + g.Log.Error("Unable to unmarshal the query", map[string]interface{}{"_error": err.Error()}) + return "", err + } + + if !pandati.IsZero(queryResult.Errors) { + g.Log.Error("Query returned error", map[string]interface{}{"_query": queryContent, "_variables": queryVariables, "_error": fmt.Sprintf("%v", queryResult.Errors)}) + return "", errors.New(fmt.Sprintf("%v", queryResult.Errors[0].Message)) + } + + if pandati.IsZero(queryResult.Data) { + g.Log.Error("Query returned no data", map[string]interface{}{"_query": queryContent, "_variables": queryVariables}) + return "", errors.New("Query returned no data") + } + + responseContent, err = json.MarshalToString(queryResult.Data) + if err != nil { + g.Log.Error("Invalid data result", map[string]interface{}{"_query": queryContent, "_variables": queryVariables, "_result": responseContent}) + return "", err + } + + return +} diff --git a/query_bench_test.go b/query_bench_test.go new file mode 100644 index 0000000..25d2adb --- /dev/null +++ b/query_bench_test.go @@ -0,0 +1,66 @@ +package gql + +import ( + "testing" +) + +func Benchmark_connHandler_Query(t *testing.B) { + type args struct { + queryContent string + queryVariables interface{} + } + tests := []struct { + name string + endpoint string + isLocal bool + args args + wantResult string + mockedReply string + wantErr bool + }{ + { + name: "Valid query", + endpoint: "http://hasura.local/v1/graphql", + isLocal: false, + args: args{ + queryContent: `query listUserBots { + tbl_bots(limit: 1) { + bot_name + } + }`, + queryVariables: nil, + }, + wantResult: `{"tbl_bots":[{"bot_name":"littleMentionBot"}]}`, + mockedReply: `{"data":{"tbl_bots":[{"bot_name":"littleMentionBot"}]}}`, + wantErr: false, + }, + { + name: "Invalid query", + endpoint: "http://hasura.local/v1/graphql", + isLocal: false, + args: args{ + queryContent: `query listUserBots { + tbl_botz(limit: 1) { + bot_name + } + }`, + queryVariables: nil, + }, + mockedReply: `{"errors":[{"message":"Unknown field \"tbl_botz\" on type \"Query\""}]}`, + wantResult: ``, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.B) { + g := NewConnection() + server, serverURL := mockGraphQLServerResponses(tt.mockedReply) + g.Endpoint = serverURL + for n := 0; n < t.N; n++ { + g.Query(tt.args.queryContent, tt.args.queryVariables) + } + server.Close() + }) + } +} diff --git a/query_test.go b/query_test.go new file mode 100644 index 0000000..31049bb --- /dev/null +++ b/query_test.go @@ -0,0 +1,109 @@ +package gql + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func (suite *TestSuite) Test_connHandler_queryBuilder() { + type args struct { + queryContent string + queryVariables interface{} + } + tests := []struct { + name string + args args + want []byte + wantErr bool + }{ + { + name: "Valid query", + args: args{ + queryContent: `query listUserBots { tbl_bots { bot_name } }`, + queryVariables: nil, + }, + want: []byte(`{"query":"query listUserBots { tbl_bots { bot_name } }","variables":null}`), + wantErr: false, + }, + { + name: "Valid query with variables", + args: args{ + queryContent: `query listUserBots { tbl_bots { bot_name } }`, + queryVariables: map[string]interface{}{"user_id": 1}, + }, + want: []byte(`{"query":"query listUserBots { tbl_bots { bot_name } }","variables":{"user_id":1}}`), + wantErr: false, + }, + } + for _, tt := range tests { + suite.T().Run(tt.name, func(t *testing.T) { + q := NewConnection() + got, gotErr := q.queryBuilder(tt.args.queryContent, tt.args.queryVariables) + assert.Equal(t, tt.want, got) + if tt.wantErr { + assert.Error(t, gotErr) + } + }) + } +} + +func (suite *TestSuite) Test_connHandler_Query() { + type args struct { + queryContent string + queryVariables interface{} + } + tests := []struct { + name string + endpoint string + isLocal bool + args args + wantResult string + wantErr bool + }{ + { + name: "Valid query", + endpoint: "http://hasura.local/v1/graphql", + isLocal: true, + args: args{ + queryContent: `query listUserBots { + tbl_bots(limit: 1) { + bot_name + } + }`, + queryVariables: nil, + }, + wantResult: `{"tbl_bots":[{"bot_name":"littleMentionBot"}]}`, + wantErr: false, + }, + { + name: "Invalid query", + endpoint: "http://hasura.local/v1/graphql", + isLocal: true, + args: args{ + queryContent: `query listUserBots { + tbl_botz(limit: 1) { + bot_name + } + }`, + queryVariables: nil, + }, + wantResult: ``, + wantErr: true, + }, + } + for _, tt := range tests { + suite.T().Run(tt.name, func(t *testing.T) { + if tt.isLocal { + os.Setenv("GRAPHQL_ENDPOINT", tt.endpoint) + } + g := NewConnection() + gotResult, gotErr := g.Query(tt.args.queryContent, tt.args.queryVariables) + if tt.wantErr { + assert.Error(t, gotErr) + } + assert.Equal(t, tt.wantResult, gotResult) + }) + } +}