~bigbes/sr-ht-spec

ref: 827cea5692702cbb2f0bfb310056a995dc8ad75d sr-ht-spec/graph/introspection_test.go -rw-r--r-- 4.8 KiB
827cea56 — Eugene Blikh scss: keep a flowed list item's bullet clear of the change bar a day ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
package graph

import (
	"encoding/json"
	"fmt"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// The introspection schema is not ours: it comes from gqlparser's built-in
// prelude, while the code that executes it is generated by gqlgen. The two are
// versioned separately, and gqlgen's generated executor answers a field it does
// not know with panic("unknown field") — recovered into "internal system error"
// rather than into anything a caller can act on. So a gqlparser that has
// learned a newer edition of the introspection schema than the pinned gqlgen
// can execute leaves this endpoint advertising fields that fail when selected.
//
// That is not hypothetical: gqlparser v2.5.22 added the @oneOf directive,
// __Type.isOneOf, __InputValue.isDeprecated and deprecationReason, and the
// includeDeprecated arguments, for the oneOf-input-objects and
// deprecated-arguments spec changes. Against gqlgen v0.17.36 a query selecting
// isDeprecated on an argument answers 422 and "internal system error", which is
// why go.mod holds gqlparser at v2.5.21 — see the note there.
//
// This test derives the field list from whichever prelude is in use instead of
// hard-coding one, so a future gqlparser bump that outpaces the pinned gqlgen
// fails here rather than in a consumer federating this /query.
func TestIntrospectionSchemaIsFullyExecutable(t *testing.T) {
	h := newHarness(t, false)

	// Only __Schema and __Type are reachable from the query root; the rest are
	// reachable only underneath it. Each path is anchored on something this
	// schema actually has, so the selection is executed rather than skipped
	// over an empty list.
	paths := []struct {
		typeName string
		query    string // one %s, for the selection set
	}{
		{"__Schema", `{ __schema { %s } }`},
		{"__Type", `{ __type(name: "Query") { %s } }`},
		{"__Field", `{ __type(name: "Query") { fields { %s } } }`},
		{"__InputValue", `{ __type(name: "Query") { fields { args { %s } } } }`},
		{"__EnumValue", `{ __type(name: "ProposalState") { enumValues { %s } } }`},
		{"__Directive", `{ __schema { directives { %s } } }`},
	}

	for _, p := range paths {
		t.Run(p.typeName, func(t *testing.T) {
			sel, leaves := introspectionSelection(t, h, p.typeName)
			require.NotEmpty(t, sel, "no fields discovered on %s: the check would pass vacuously", p.typeName)
			require.NotEmpty(t, leaves, "no leaf fields on %s, so nothing below proves execution", p.typeName)

			r := query(t, h, fmt.Sprintf(p.query, strings.Join(sel, " ")))

			assert.Equal(t, 200, r.status, "body: %s", r.body)
			assert.Empty(t, r.errText(), "selecting every declared field of %s failed; "+
				"gqlparser's prelude and gqlgen's generated executor have diverged", p.typeName)
			// An anchor that resolved to an empty list would report no error
			// while executing none of the fields under test, so require that at
			// least one of them actually came back.
			assert.Contains(t, r.body, fmt.Sprintf("%q:", leaves[0]),
				"%s: no %q key in the response, so the anchor executed none of its fields",
				p.typeName, leaves[0])
		})
	}
}

// introspectionSelection asks the endpoint which fields it declares on an
// introspection type, and renders them as a selection set: leaf fields bare,
// composite fields with a __typename subselection so the query is valid
// whatever the field's type turns out to be. It returns the selection and the
// names of the leaf fields, which is what the caller can look for in the
// response to know the selection was executed and not skipped.
func introspectionSelection(t *testing.T, h harness, typeName string) (sel, leaves []string) {
	t.Helper()

	r := query(t, h, fmt.Sprintf(`{ __type(name: %q) { fields(includeDeprecated: true) {
		name type { kind ofType { kind ofType { kind ofType { kind } } } }
	} } }`, typeName))
	require.Equal(t, 200, r.status, "discovering fields of %s: %s", typeName, r.body)
	require.Empty(t, r.errText(), "discovering fields of %s", typeName)

	var out struct {
		Type struct {
			Fields []struct {
				Name string `json:"name"`
				Type introspectedType
			} `json:"fields"`
		} `json:"__type"`
	}
	require.NoError(t, json.Unmarshal(r.Data, &out), "decode %s", r.body)

	for _, f := range out.Type.Fields {
		if k := f.Type.baseKind(); k == "SCALAR" || k == "ENUM" {
			sel = append(sel, f.Name)
			leaves = append(leaves, f.Name)
			continue
		}
		sel = append(sel, f.Name+" { __typename }")
	}
	return sel, leaves
}

type introspectedType struct {
	Kind   string            `json:"kind"`
	OfType *introspectedType `json:"ofType"`
}

// baseKind unwraps the NON_NULL and LIST wrappers to the underlying kind.
func (t introspectedType) baseKind() string {
	if (t.Kind == "NON_NULL" || t.Kind == "LIST") && t.OfType != nil {
		return t.OfType.baseKind()
	}
	return t.Kind
}