github.com/Desuuuu/genqlient@v0.5.3/internal/integration/roundtrip.go (about) 1 package integration 2 3 // Machinery for integration tests to round-trip check the JSON-marshalers and 4 // unmarshalers we generate. 5 6 import ( 7 "bytes" 8 "context" 9 "encoding/json" 10 "fmt" 11 "io" 12 "net/http" 13 "testing" 14 15 "github.com/Desuuuu/genqlient/graphql" 16 "github.com/stretchr/testify/assert" 17 ) 18 19 // lastResponseTransport is an HTTP transport that keeps track of the last response 20 // that passed through it. 21 type lastResponseTransport struct { 22 wrapped http.RoundTripper 23 lastResponseBody []byte 24 } 25 26 func (t *lastResponseTransport) RoundTrip(req *http.Request) (*http.Response, error) { 27 resp, err := t.wrapped.RoundTrip(req) 28 if err != nil { 29 return resp, err 30 } 31 defer resp.Body.Close() 32 body, err := io.ReadAll(resp.Body) 33 if err != nil { 34 return resp, fmt.Errorf("roundtrip failed: unreadable body: %w", err) 35 } 36 t.lastResponseBody = body 37 // Restore the body for the next reader: 38 resp.Body = io.NopCloser(bytes.NewBuffer(body)) 39 return resp, err 40 } 41 42 // roundtripClient is a graphql.Client that checks that 43 // unmarshal(marshal(req)) == req && marshal(unmarshal(resp)) == resp 44 // for each request it processes. 45 type roundtripClient struct { 46 wrapped graphql.Client 47 transport *lastResponseTransport 48 t *testing.T 49 } 50 51 // Put JSON in a stable and human-readable format. 52 func (c *roundtripClient) formatJSON(b []byte) []byte { 53 // We don't care about key ordering, so do another roundtrip through 54 // interface{} to drop that. 55 var parsed interface{} 56 err := json.Unmarshal(b, &parsed) 57 if err != nil { 58 c.t.Fatal(err) 59 } 60 61 // When marshaling, add indents to make things human-readable. 62 b, err = json.MarshalIndent(parsed, "", " ") 63 if err != nil { 64 c.t.Fatal(err) 65 } 66 return b 67 } 68 69 func (c *roundtripClient) roundtripResponse(resp interface{}) { 70 var graphqlResponse struct { 71 Data json.RawMessage `json:"data"` 72 } 73 err := json.Unmarshal(c.transport.lastResponseBody, &graphqlResponse) 74 if err != nil { 75 c.t.Error(err) 76 return 77 } 78 body := c.formatJSON(graphqlResponse.Data) 79 80 // resp is constructed to be unmarshal(body), so just use it 81 bodyAgain, err := json.Marshal(resp) 82 if err != nil { 83 c.t.Error(err) 84 return 85 } 86 bodyAgain = c.formatJSON(bodyAgain) 87 88 assert.Equal(c.t, string(body), string(bodyAgain)) 89 } 90 91 func (c *roundtripClient) MakeRequest(ctx context.Context, req *graphql.Request, resp *graphql.Response) error { 92 // TODO(benkraft): Also check the variables round-trip. This is a bit less 93 // important since most of the code is the same (and input types are 94 // strictly simpler), and a bit hard to do because when asserting about 95 // structs we need to worry about things like equality of time.Time values. 96 err := c.wrapped.MakeRequest(ctx, req, resp) 97 if err != nil { 98 return err 99 } 100 c.roundtripResponse(resp.Data) 101 return nil 102 } 103 104 func newRoundtripClients(t *testing.T, endpoint string) []graphql.Client { 105 return []graphql.Client{newRoundtripClient(t, endpoint), newRoundtripGetClient(t, endpoint)} 106 } 107 108 func newRoundtripClient(t *testing.T, endpoint string) graphql.Client { 109 transport := &lastResponseTransport{wrapped: http.DefaultTransport} 110 httpClient := &http.Client{Transport: transport} 111 return &roundtripClient{ 112 wrapped: graphql.NewClient(endpoint, httpClient), 113 transport: transport, 114 t: t, 115 } 116 } 117 118 func newRoundtripGetClient(t *testing.T, endpoint string) graphql.Client { 119 transport := &lastResponseTransport{wrapped: http.DefaultTransport} 120 httpClient := &http.Client{Transport: transport} 121 return &roundtripClient{ 122 wrapped: graphql.NewClientUsingGet(endpoint, httpClient), 123 transport: transport, 124 t: t, 125 } 126 }