Skip to content

Commit

Permalink
Merge pull request #90 from CircleCI-Public/introspect
Browse files Browse the repository at this point in the history
diagnostic command should test API availability
  • Loading branch information
Zachary Scott authored Sep 4, 2018
2 parents 68f6a97 + ccbe2d9 commit d9f59f7
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 58 deletions.
63 changes: 61 additions & 2 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,26 @@ type GQLResponseErrors struct {
}
}

// IntrospectionResponse matches the result from making an introspection query
type IntrospectionResponse struct {
Schema struct {
MutationType struct {
Name string
}
QueryType struct {
Name string
}
Types []struct {
Description string
Fields []struct {
Name string
}
Kind string
Name string
}
} `json:"__schema"`
}

// ConfigResponse is a structure that matches the result of the GQL
// query, so that we can use mapstructure to convert from
// nested maps to a strongly typed struct.
Expand Down Expand Up @@ -134,13 +154,13 @@ func GraphQLServerAddress(endpoint string, host string) (string, error) {
// 1. Parse the endpoint
e, err := url.Parse(endpoint)
if err != nil {
return e.String(), errors.Wrapf(err, "Parsing endpoint '%s'", endpoint)
return "", errors.Wrapf(err, "Parsing endpoint '%s'", endpoint)
}

// 2. Parse the host
h, err := url.Parse(host)
if err != nil {
return h.String(), errors.Wrapf(err, "Parsing host '%s'", host)
return "", errors.Wrapf(err, "Parsing host '%s'", host)
}
if !h.IsAbs() {
return h.String(), fmt.Errorf("Host (%s) must be absolute URL, including scheme", host)
Expand Down Expand Up @@ -810,3 +830,42 @@ query namespaceOrbs ($namespace: String, $after: String!) {

return orbs, nil
}

// IntrospectionQuery makes a query on the API asking for bits of the schema
// This query isn't intended to get the entire schema, there are better tools for that.
func IntrospectionQuery(ctx context.Context, logger *logger.Logger) (*IntrospectionResponse, error) {
var response struct {
IntrospectionResponse
}

query := `query IntrospectionQuery {
__schema {
queryType { name }
mutationType { name }
types {
...FullType
}
}
}
fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: true) {
name
}
}`

request := client.NewAuthorizedRequest(viper.GetString("token"), query)

address, err := GraphQLServerAddress(EnvEndpointHost())
if err != nil {
return nil, err
}
graphQLclient := client.NewClient(address, logger)

err = graphQLclient.Run(ctx, request, &response)

return &response.IntrospectionResponse, err
}
45 changes: 45 additions & 0 deletions api/api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package api

import (
"regexp"
"testing"
)

func TestGraphQLServerAddress(t *testing.T) {
var (
addr string
expected string
err error
)

addr, _ = GraphQLServerAddress("", "https://example.com/graphql")

expected = "https://example.com/graphql"
if addr != expected {
t.Errorf("Expected %s, got %s", expected, addr)
}

addr, _ = GraphQLServerAddress("graphql-unstable", "https://example.com")
expected = "https://example.com/graphql-unstable"
if addr != expected {
t.Errorf("Expected %s, got %s", expected, addr)
}

addr, _ = GraphQLServerAddress("https://circleci.com/graphql", "https://example.com/graphql-unstable")
expected = "https://circleci.com/graphql"
if addr != expected {
t.Errorf("Expected %s, got %s", expected, addr)
}

_, err = GraphQLServerAddress("", "")
expected = "Host () must be absolute URL, including scheme"
if err.Error() != expected {
t.Errorf("Expected error without absolute URL")
}

_, err = GraphQLServerAddress(":foo", "")
matched, _ := regexp.MatchString("Parsing endpoint", err.Error())
if !matched {
t.Errorf("Expected parsing endpoint error")
}
}
14 changes: 14 additions & 0 deletions cmd/diagnostic.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package cmd

import (
"context"

"github.com/CircleCI-Public/circleci-cli/api"
"github.com/pkg/errors"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -36,5 +38,17 @@ func diagnostic(cmd *cobra.Command, args []string) error {
Logger.Infoln("OK, got a token.")
Logger.Infof("Verbose mode: %v\n", viper.GetBool("verbose"))

Logger.Infoln("Trying an introspection query on API... ")
response, err := api.IntrospectionQuery(context.Background(), Logger)
if response.Schema.QueryType.Name == "" {
Logger.Infoln("Unable to make a query against the GraphQL API, please check your settings")
if err != nil {
return err
}
}

Logger.Infoln("Ok.")

Logger.Debug("Introspection query result with Schema.QueryType of %s", response.Schema.QueryType.Name)
return nil
}
103 changes: 52 additions & 51 deletions cmd/diagnostic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmd_test
import (
"fmt"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path/filepath"
Expand All @@ -11,26 +12,46 @@ import (
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"github.com/onsi/gomega/gexec"
"github.com/onsi/gomega/ghttp"
)

var _ = Describe("Diagnostic", func() {
var (
tempHome string
command *exec.Cmd
tempHome string
command *exec.Cmd
testServer *ghttp.Server
)

BeforeEach(func() {
var err error
tempHome, err = ioutil.TempDir("", "circleci-cli-test-")
Expect(err).ToNot(HaveOccurred())

command = exec.Command(pathCLI, "diagnostic")
testServer = ghttp.NewServer()

command = exec.Command(pathCLI,
"diagnostic",
"--endpoint", testServer.URL())

command.Env = append(os.Environ(),
fmt.Sprintf("HOME=%s", tempHome),
)

tmpBytes, err := ioutil.ReadFile(filepath.Join("testdata/diagnostic", "response.json"))
Expect(err).ShouldNot(HaveOccurred())
mockResponse := string(tmpBytes)

testServer.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("POST", "/"),
ghttp.VerifyContentType("application/json; charset=utf-8"),
ghttp.RespondWith(http.StatusOK, `{ "data": `+mockResponse+`}`),
),
)
})

AfterEach(func() {
testServer.Close()
Expect(os.RemoveAll(tempHome)).To(Succeed())
})

Expand All @@ -54,12 +75,9 @@ var _ = Describe("Diagnostic", func() {
Expect(err).ToNot(HaveOccurred())
})

Describe("token and endpoint set in config file", func() {
Describe("token set in config file", func() {
BeforeEach(func() {
_, err := config.Write([]byte(`
endpoint: https://example.com/graphql
token: mytoken
`))
_, err := config.Write([]byte(`token: mytoken`))
Expect(err).ToNot(HaveOccurred())
Expect(config.Close()).To(Succeed())
})
Expand All @@ -68,16 +86,16 @@ token: mytoken
session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
Expect(err).ShouldNot(HaveOccurred())
Eventually(session.Err.Contents()).Should(BeEmpty())
Eventually(session.Out).Should(gbytes.Say("GraphQL API address: https://example.com/graphql"))
Eventually(session.Out).Should(gbytes.Say(
fmt.Sprintf("GraphQL API address: %s", testServer.URL())))
Eventually(session.Out).Should(gbytes.Say("OK, got a token."))
Eventually(session).Should(gexec.Exit(0))
})
})

Describe("token, host, and old endpoint set in config file", func() {
Describe("fully-qualified address from --endpoint preferred over host in config ", func() {
BeforeEach(func() {
_, err := config.Write([]byte(`
endpoint: https://example.com/graphql
host: https://circleci.com/
token: mytoken
`))
Expand All @@ -89,69 +107,52 @@ token: mytoken
session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
Expect(err).ShouldNot(HaveOccurred())
Eventually(session.Err.Contents()).Should(BeEmpty())
Eventually(session.Out).Should(gbytes.Say("GraphQL API address: https://example.com/graphql"))
Eventually(session.Out).Should(gbytes.Say(
fmt.Sprintf("GraphQL API address: %s", testServer.URL())))
Eventually(session.Out).Should(gbytes.Say("OK, got a token."))
Eventually(session).Should(gexec.Exit(0))
})
})

Describe("token, host, and new endpoint set in config file", func() {
Context("empty token in config file", func() {
BeforeEach(func() {
_, err := config.Write([]byte(`
endpoint: graphql
host: https://circleci.com/
token: mytoken
`))
_, err := config.Write([]byte(`token: `))
Expect(err).ToNot(HaveOccurred())
Expect(config.Close()).To(Succeed())
})

It("print success", func() {
It("print error", func() {
session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
Expect(err).ShouldNot(HaveOccurred())
Eventually(session.Err.Contents()).Should(BeEmpty())
Eventually(session.Out).Should(gbytes.Say("GraphQL API address: https://circleci.com/graphql"))
Eventually(session.Out).Should(gbytes.Say("OK, got a token."))
Eventually(session).Should(gexec.Exit(0))
Eventually(session.Err).Should(gbytes.Say("Error: please set a token"))
Eventually(session.Out).Should(gbytes.Say(
fmt.Sprintf("GraphQL API address: %s", testServer.URL())))
Eventually(session).Should(gexec.Exit(255))
})
})

Describe("token, host, and missing endpoint set in config file", func() {
Context("debug outputs introspection query results", func() {
BeforeEach(func() {
_, err := config.Write([]byte(`
host: https://circleci.com/
token: mytoken
`))
_, err := config.Write([]byte(`token: zomg`))
Expect(err).ToNot(HaveOccurred())
Expect(config.Close()).To(Succeed())
})

It("print success", func() {
session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
Expect(err).ShouldNot(HaveOccurred())
Eventually(session.Err.Contents()).Should(BeEmpty())
Eventually(session.Out).Should(gbytes.Say("GraphQL API address: https://circleci.com/graphql-unstable"))
Eventually(session.Out).Should(gbytes.Say("OK, got a token."))
Eventually(session).Should(gexec.Exit(0))
})
})
command = exec.Command(pathCLI,
"diagnostic",
"--endpoint", testServer.URL(),
"--verbose",
)

Context("token set to empty string in config file", func() {
BeforeEach(func() {
_, err := config.Write([]byte(`
endpoint: https://example.com/graphql
token:
`))
Expect(err).ToNot(HaveOccurred())
Expect(config.Close()).To(Succeed())
command.Env = append(os.Environ(),
fmt.Sprintf("HOME=%s", tempHome),
)
})

It("print error", func() {
It("print success", func() {
session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
Expect(err).ShouldNot(HaveOccurred())
Eventually(session.Err).Should(gbytes.Say("Error: please set a token"))
Eventually(session.Out).Should(gbytes.Say("GraphQL API address: https://example.com/graphql"))
Eventually(session).Should(gexec.Exit(255))
Eventually(session.Out).Should(gbytes.Say("Trying an introspection query on API..."))
Eventually(session.Err).Should(gbytes.Say("Introspection query result with Schema.QueryType of QueryRoot"))
Eventually(session).Should(gexec.Exit(0))
})
})
})
Expand Down
5 changes: 0 additions & 5 deletions testquery.gql → cmd/testdata/diagnostic/query.gql
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,9 @@
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
...FullType
}
directives {
name
description
}
}
}

Expand Down
27 changes: 27 additions & 0 deletions cmd/testdata/diagnostic/response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"__schema": {
"mutationType": {
"name": "MutationRoot"
},
"queryType": {
"name": "QueryRoot"
},
"types": [
{
"description": "account",
"fields": [
{
"name": "contextsHmb"
},
{
"name": "groups"
},
{
"name": "id"
}
],
"kind": "OBJECT",
"name": "Account"
}]
}
}

0 comments on commit d9f59f7

Please sign in to comment.