graph_weaver 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +4 -4
  3. data/README.md +40 -88
  4. data/docs/alternatives.md +1 -7
  5. data/docs/cassettes.md +54 -59
  6. data/docs/editors.md +32 -47
  7. data/docs/errors.md +261 -369
  8. data/docs/federation.md +650 -837
  9. data/docs/generated_modules.md +380 -463
  10. data/docs/getting_started.md +211 -428
  11. data/docs/i18n.md +114 -177
  12. data/docs/logging.md +127 -116
  13. data/docs/real_world.md +26 -39
  14. data/docs/scalars.md +277 -310
  15. data/docs/testing.md +343 -486
  16. data/docs/transports.md +203 -268
  17. data/docs/upgrading.md +211 -560
  18. data/examples/README.md +38 -0
  19. data/examples/countries.rb +39 -0
  20. data/examples/federation.rb +62 -0
  21. data/examples/github/generate.rb +20 -0
  22. data/examples/github/generated/star_mutation.rb +126 -0
  23. data/examples/github/generated/stargazers_query.rb +232 -0
  24. data/examples/github/generated/starred_query.rb +151 -0
  25. data/examples/github/queries/star.graphql +8 -0
  26. data/examples/github/queries/stargazers.graphql +22 -0
  27. data/examples/github/queries/starred.graphql +11 -0
  28. data/examples/github/run.rb +43 -0
  29. data/examples/github/setup.rb +18 -0
  30. data/examples/rick_and_morty.rb +57 -0
  31. data/graph_weaver.gemspec +12 -3
  32. data/lib/graph_weaver/client.rb +30 -1
  33. data/lib/graph_weaver/codegen/emit.rb +5 -11
  34. data/lib/graph_weaver/codegen.rb +23 -55
  35. data/lib/graph_weaver/context_seam.rb +54 -0
  36. data/lib/graph_weaver/errors.rb +23 -15
  37. data/lib/graph_weaver/federation.rb +11 -2
  38. data/lib/graph_weaver/graph.rb +39 -29
  39. data/lib/graph_weaver/in_process.rb +15 -9
  40. data/lib/graph_weaver/internal/endpoint.rb +7 -5
  41. data/lib/graph_weaver/internal/headers.rb +19 -0
  42. data/lib/graph_weaver/internal/test_clients.rb +7 -11
  43. data/lib/graph_weaver/internal.rb +81 -13
  44. data/lib/graph_weaver/log_subscriber.rb +10 -2
  45. data/lib/graph_weaver/logging.rb +33 -13
  46. data/lib/graph_weaver/query_module.rb +44 -23
  47. data/lib/graph_weaver/retry.rb +12 -8
  48. data/lib/graph_weaver/rspec.rb +13 -24
  49. data/lib/graph_weaver/schema_loader.rb +52 -14
  50. data/lib/graph_weaver/tasks.rb +10 -2
  51. data/lib/graph_weaver/testing/cassette.rb +28 -5
  52. data/lib/graph_weaver/testing/endpoint.rb +14 -13
  53. data/lib/graph_weaver/testing/fake_client.rb +33 -3
  54. data/lib/graph_weaver/testing/router.rb +7 -3
  55. data/lib/graph_weaver/testing.rb +12 -4
  56. data/lib/graph_weaver/transport/http.rb +2 -2
  57. data/lib/graph_weaver/transport.rb +47 -23
  58. data/lib/graph_weaver/version.rb +1 -1
  59. data/lib/graph_weaver.rb +32 -10
  60. metadata +16 -3
  61. data/CHANGELOG.md +0 -3801
data/docs/testing.md CHANGED
@@ -3,9 +3,6 @@
3
3
  How to run a spec that executes a GraphQL query without a server — against
4
4
  fabricated data, against your own resolvers, across a federated graph, or
5
5
  through your own transport with any of those behind it.
6
- Setup is one require and one tag (`:wire` alone adds two test gems); the rest of
7
- this page is what you reach for when an example is *about* the data, the
8
- resolvers, or the transport.
9
6
 
10
7
  One line in your spec helper:
11
8
 
@@ -14,8 +11,8 @@ One line in your spec helper:
14
11
  require "graph_weaver/rspec"
15
12
  ```
16
13
 
17
- Then **one tag says what an example runs against** — on the example, or on
18
- the group it belongs to, since rspec metadata inherits:
14
+ Then **one tag says what an example runs against** — on the example, or on the
15
+ group it belongs to, since rspec metadata inherits:
19
16
 
20
17
  ```ruby
21
18
  describe "checkout", graphql: :router do
@@ -36,63 +33,47 @@ it "sends the caller tag", graphql: :wire do … end
36
33
  | `:live` | the app's own client is the point, or this one example wants out of `config.default_mode` | whatever your client does — this is the default |
37
34
  | [cassettes](cassettes.md) | pinning a real server's exact response | must be re-recorded when the query changes |
38
35
 
39
- The tag installs a stand-in per graph, and every generated module of that
40
- graph runs against it with no per-test setup — including one generated *with*
41
- a baked `client:`, since that constant is exactly what the tag means to
42
- replace. `rspec --tag graphql:router` runs one mode's examples.
43
-
44
- **A bad variable never gets as far as a mode.** `execute` coerces the variables
45
- before it asks any client for anything, so a
46
- [client-side `InputError`](errors.md#what-an-inputerror-says-without-reading-english)
47
- raises the same way under every tag and under none. "Which mode do I need for
48
- this" has no answer there — pick `:fake`, the cheapest.
49
-
50
- **The other half is the one `:fake` can't reach.** It fabricates a
51
- shape-correct *success*, so your server's `validates:` rules and custom
52
- validators never run and nothing is ever rejected — a `:fake`-only suite has
53
- zero coverage of server-side refusal. Cover it with
54
- [`Failure.graphql(code:, extensions:)`](#simulating-failures) for the rejection
55
- you expect, or with `:in_process`, where the real validators do run.
36
+ There is nothing else to set up: each mode works out what to run against per
37
+ graph, and [refuses rather than guessing](#nothing-to-configure). The tag
38
+ installs a stand-in per graph, and every generated module of that graph runs
39
+ against it including one whose graph names a `client` of its own, since that
40
+ is exactly what the tag means to replace. `rspec --tag graphql:router`
41
+ runs one mode's examples.
56
42
 
57
43
  **Every example has exactly one mode.** An untagged one takes
58
44
  `config.default_mode`, which is `:live` — your own client, exactly as it is —
59
45
  unless the suite sets another; and **`graphql: :live` is how one example steps
60
46
  back out** of a default the suite did set.
61
47
 
62
- `GraphWeaver.client` is **snapshotted before every example and restored
63
- after** tagged or untagged, whatever its mode, and whatever the example did
64
- to it. So building your own client is a plain assignment, cleaned up like a
65
- tagged one:
66
-
67
- ```ruby
68
- before { GraphWeaver.client = GraphWeaver::Testing::Failure.throttled }
69
- ```
70
-
71
- Both at once and **the tag wins**: a mode's stand-in outranks
72
- `GraphWeaver.client=`, so assigning one inside a tagged example does *not*
73
- change what its generated modules run against the assignment reads back,
74
- and the modules keep using the mode. Two things do step out of a tag, and
75
- `graphql: :live` steps the whole example out:
76
-
77
- ```ruby
78
- DashboardQuery.execute!(client: GraphWeaver::Testing::Failure.throttled) # this call
79
- DashboardQuery.client = GraphWeaver::Testing::Failure.throttled # this module
80
- ```
81
-
82
- [Client resolution](transports.md#client-resolution) has the full order.
48
+ `GraphWeaver.client` is **snapshotted before every example and restored after**,
49
+ whatever the example did to it, so building your own client is a plain
50
+ assignment: `before { GraphWeaver.client = GraphWeaver::Testing::Failure.throttled }`.
51
+ Do both at once and **the tag wins**: the assignment reads back while the modules
52
+ keep using the mode. Two things do step out of a tag — a per-call `client:`, and
53
+ a module [parsed](generated_modules.md#dynamic-mode) from a client of its own,
54
+ which runs against that client
55
+ ([client resolution](transports.md#client-resolution) has the full order).
56
+
57
+ **The half `:fake` can't reach** is refusal. It fabricates a shape-correct
58
+ *success*, so your server's `validates:` rules and custom validators never run —
59
+ a `:fake`-only suite has zero coverage of server-side rejection. Cover it with
60
+ [`Failure.graphql(code:, extensions:)`](#simulating-failures) for the rejection
61
+ you expect, or with `:in_process`, where the real validators do run. (A bad
62
+ *variable* never gets as far as a mode: `execute` coerces before it asks any
63
+ client for anything, so a
64
+ [client-side `InputError`](errors.md#what-an-inputerror-says-without-reading-english)
65
+ raises the same way under every tag and under none.)
83
66
 
84
67
  Everything here is a *client* — the one interface queries run through (the
85
- contract is in [transports](transports.md)). Fakes, the router, failures and
86
- cassettes all slot in wherever a real transport would, so they work outside
87
- rspec too (`require "graph_weaver/testing"` never from production code).
88
- Outside the tags there's no `GraphWeaver.client` to lean on, so parse from
89
- the fake or the router itself — anything holding a schema parses against it,
90
- and the module runs on what parsed it:
68
+ contract is in [transports](transports.md)) — so fakes, the router, failures and
69
+ cassettes work outside rspec too (`require "graph_weaver/testing"`, never from
70
+ production code). There's no `GraphWeaver.client` to lean on there, so parse from
71
+ the fake or the router itself: anything holding a schema parses, and the module
72
+ runs on what parsed it.
91
73
 
92
74
  ```ruby
93
75
  router = GraphWeaver::Testing::Router.new(supergraph: "app/graphql/supergraph.graphql")
94
76
  DashboardQuery = router.parse("query Dashboard { me { username } }")
95
- DashboardQuery.execute!.me.username
96
77
  ```
97
78
 
98
79
  Every mode, tagged and running end to end, is
@@ -100,166 +81,33 @@ Every mode, tagged and running end to end, is
100
81
  [`spec/wire_mode_spec.rb`](https://github.com/dpep/graph_weaver/blob/main/spec/wire_mode_spec.rb) for `:wire`) — the
101
82
  reference for anything this page leaves out.
102
83
 
103
- ## Nothing to configure
104
-
105
- Each mode works out what to run against **per graph** — with more than one, the
106
- honest answer varies per module — and **refuses, naming what it looked for,
107
- rather than guessing**:
108
-
109
- - **the schema** is `config.schema` if you set one, else the one that
110
- [graph](getting_started.md#more-than-one-schema) names, else the committed
111
- dump at `GraphWeaver.schema_path`, else the schema `GraphWeaver.client` talks
112
- to. A fake reads the scalar registrations of the graph it is answering, so it
113
- invents the wire value that graph's generated cast expects. (Pins and
114
- `overrides:` stay suite-wide, keyed by scalar name — one `"Money"` override
115
- for the run.)
116
- - **`:in_process`** needs the live schema *class*, since only that has
117
- resolvers: the one that graph names, else the one your client already runs
118
- in-process, else the loaded class that defines everything the schema
119
- declares — the same derive-verify-refuse rule that
120
- [maps subgraphs](federation.md#which-schema-serves-which-subgraph).
121
- - **`:router`** plans against the composed supergraph **that graph** names,
122
- else `config.router = { supergraph: … }`, else the committed dump when
123
- *that* carries `@join__*` markers — which for a federated app is usually no
124
- config at all. A graph that is in no supergraph is refused **by name**,
125
- rather than planned against another graph's. A client can't stand in for
126
- one: a client's schema is the API schema the router serves, with the
127
- `@join__*` routing table stripped out. Subgraphs are derived either way.
128
-
129
- **The helpers say which graph they mean.** `graphql_fake`,
130
- `graphql_in_process` and `graphql_router` are the stand-in for the modules of
131
- the graph their schema names — for your only graph when they name none — and
132
- they refuse, naming your graphs, when there is none they could reach:
133
-
134
- ```ruby
135
- graphql_fake("Product.name" => "Ada's Book", schema: Catalog::Schema)
136
- graphql_in_process(Accounts::Schema)
137
- ```
138
-
139
- So configure only to override a derivation, or to tune fabricated values — in
140
- the same file as the require, since support files load in sorted order and one
141
- naming `GraphWeaver::Testing` before it dies on `NameError`:
142
-
143
- ```ruby
144
- GraphWeaver::Testing.configure do |config|
145
- # config.schema = MySchema # the live class, rather than the dump
146
- # config.router = { supergraph: Rails.root.join("supergraph.graphql") }
147
- # config.router = { subgraphs: { "reviews" => :fake } } # either key alone
148
- # config.context = { tenant: } # baseline context every example starts from
149
- # config.default_mode = :fake # what an UNtagged example runs against;
150
- # # :live (the default) leaves your client
151
- # # alone, and graphql: :live opts one out
152
- # config.seed = 4242 # defaults to rspec's own --seed
153
- # config.overrides = { "Money" => "12.00", "Person.name" => "Daniel" }
154
- # config.list_size = 1..3
155
- end
156
- ```
157
-
158
- `list_size` is how long an **unbounded** list is — an Integer exactly that
159
- many, a Range randomized within it, or a Hash saying it per list (below). A
160
- list with a `first:`/`last:`/`limit:` argument is that long instead, whatever
161
- this says.
162
-
163
- **Every list the fabricator reaches reads the same setting, so nested lists
164
- multiply.** A query selecting `rows { owner { … } tags }` with `tags`
165
- uncapped fabricates `list_size` rows and `list_size` tags *in each of them* —
166
- at 1600 that is 2.5M tags, and per-row allocations double with every doubling
167
- of the number. Three nested lists cube it. Say it per list instead, keyed the
168
- way a pin is (a `"Type.field"` coordinate or a bare field name), with
169
- `default:` for the rest:
170
-
171
- ```ruby
172
- config.list_size = { "Row.tags" => 3, default: 1000 }
173
- ```
174
-
175
- which holds the inner list at 3 however large the outer one grows — or cap it
176
- in the query (`tags(first: 3)`), where the query is yours to change.
177
-
178
- **Configure at load, or in an `around` — never in a plain `before`.** The tag
179
- builds this example's clients in a `before` hook of its own, and rspec runs
180
- that one ahead of yours, so a `before` setting `config.schema`, `config.router`
181
- or `config.context` arrives after the decision it meant to change. It is
182
- **refused**, not ignored — a green example running against the wrong stand-in
183
- is the expensive outcome. `Testing.configure` in the spec helper is the usual
184
- place; an `around` wraps the tag's setup when one group needs its own:
185
-
186
- ```ruby
187
- around do |example|
188
- GraphWeaver::Testing.configure { |config| config.schema = Catalog::Schema }
189
- example.run
190
- end
191
- ```
192
-
193
- For a single example the helper says it where it varies instead —
194
- `graphql_in_process(MySchema)`, `graphql_fake(schema: MySchema)`,
195
- `graphql_router(fake: …)`, `graphql_context(current_user: …)`.
196
-
197
- **With more than one graph, `graph:` says which one a helper stands in for** —
198
- `graphql_fake(graph: :poke, "pokemon_v2_pokemon.name" => "pikachu")`,
199
- `graphql_in_process(graph: :catalog)`, `graphql_router(graph: :storefront,
200
- fake: …)`. A helper is the stand-in for one graph's modules, so an app with
201
- several is refused, naming them, rather than guessing. A schema class names
202
- its graph and its schema in one word — `graphql_in_process(Reviews::Schema)`
203
- — but only for a graph that runs that class in-process; a graph whose schema
204
- is a dump has no such object, and `graph:` is the handle every graph has.
205
-
206
- **The rule: a helper sets the stand-in for the graph it names; the tag sets
207
- the mode for every graph no helper named.** So one example can run two graphs
208
- in two modes — the federated one through its router, the plain one faked —
209
- and neither helper disturbs the other's graph:
210
-
211
- ```ruby
212
- it "renders the dashboard", graphql: :router do
213
- graphql_fake(graph: :countries, "Country.name" => "Canada")
214
- # :storefront routes through its supergraph (the tag); :countries is faked
215
- end
216
- ```
217
-
218
- A helper naming one graph of several isn't contradicting the tag, so it isn't
219
- refused. A helper that speaks for the whole example still is: with one graph,
220
- or with no `graph:`/schema to narrow it, `graphql: :fake` plus
221
- `graphql_in_process` is two answers to one question, and the later one winning
222
- silently would hide which was the mistake.
223
-
224
- Anything whose honest answer differs per example belongs on the fake instead
225
- — `graphql_fake(null_chance: 1.0)` for the example that's about an empty
226
- state, `graphql_fake(values: :literal)` for the one that reads better without
227
- faker's prose. A suite-wide `null_chance` would sprinkle nils through every
228
- *other* example, one run in ten, on a seed the failure doesn't name.
229
-
230
- Need the schema itself inside an example — to sample a field, or build a
231
- query on the fly? The client in play exposes it as
232
- `GraphWeaver.client.schema`, and `GraphWeaver::Testing.config.schema` reads
233
- back what `config.schema =` set, falling back to the committed dump.
234
-
235
84
  ## Fabricated data — `graphql: :fake`
236
85
 
237
- `FakeClient` fabricates schema-correct responses for whatever query
238
- arrives: real enum values, valid `__typename` members, and — for a custom
239
- scalar — a value the Ruby type you registered it as can hold, so every fake
240
- casts cleanly through your generated structs. `@skip`/`@include` are
241
- evaluated against the variables you passed (defaults included), so a field
242
- the server would leave out is left out. `rspec --seed 1234` reproduces the
243
- values along with test order.
86
+ `FakeClient` fabricates schema-correct responses for whatever query arrives:
87
+ real enum values, valid `__typename` members, and — for a custom scalar — a
88
+ value the Ruby type you registered it as can hold, so every fake casts cleanly
89
+ through your generated structs. `@skip`/`@include` are evaluated against the
90
+ variables you passed (defaults included), so a field the server would leave out
91
+ is left out. `rspec --seed 1234` reproduces the values along with test order.
244
92
 
245
93
  ```ruby
246
- fake = GraphWeaver::Testing::FakeClient.new # schema: falls back to Testing.config
94
+ it "shows the profile", graphql: :fake do
95
+ person = PersonQuery.execute!(id: "1").person
247
96
 
248
- person = PersonQuery.execute!(client: fake, id: "1").person
249
- person.name # a plausible name, when faker is loaded
250
- person.birthday # a real Date
97
+ person.name # a plausible name, when faker is loaded
98
+ person.birthday # a real Date
99
+ end
251
100
  ```
252
101
 
253
- Values are semantic when the [faker](https://github.com/faker-ruby/faker) gem
254
- is loaded — `name` gets a name, `email` an email — and plain and type-derived
255
- (`"name-1"`, seeded numbers) when it isn't. Say `values: :literal` on a fake
256
- that reads better without the prose, or `values: :faker` to insist on the
257
- semantic ones and get told if the gem is missing:
102
+ Values are semantic when the [faker](https://github.com/faker-ruby/faker) gem is
103
+ loaded — `name` gets a name, `email` an email — and plain and type-derived
104
+ (`"name-1"`, seeded numbers) when it isn't. Say `values: :literal` on a fake that
105
+ reads better without the prose, or `values: :faker` to insist on the semantic
106
+ ones and get told if the gem is missing.
258
107
 
259
- ```ruby
260
- graphql_fake(values: :literal)
261
- GraphWeaver::Testing::FakeClient.new(values: :literal)
262
- ```
108
+ Outside a tagged example, build one yourself:
109
+ `GraphWeaver::Testing::FakeClient.new` (its `schema:` falls back to
110
+ `Testing.config`), then pass it as `client:`.
263
111
 
264
112
  ### Pins
265
113
 
@@ -275,90 +123,64 @@ graphql_fake("Money" => "12.00", # every Money field, however deep
275
123
 
276
124
  Keys are schema vocabulary, so they survive query refactors — a type name, or
277
125
  `"Type.field"` (a bare `"field"` pins it on every type) — and they are checked
278
- and spellchecked: `"Person.nmae"` raises rather than quietly pinning nothing
279
- and leaving the example green against random data. **Schema vocabulary, not
280
- Ruby:** a `countries` field generates a `Countries` struct, but the pin is
281
- `"Country"`, the type name the schema uses spelled the way *that* schema
282
- spells it, so a Hasura table type is `"pokemon_v2_pokemon"` and not a
283
- Ruby-cased guess at it.
126
+ and spellchecked: `"Person.nmae"` raises rather than quietly pinning nothing and
127
+ leaving the example green against random data. **Schema vocabulary, not Ruby:** a
128
+ `countries` field generates a `Countries` struct, but the pin is `"Country"`,
129
+ spelled the way *that* schema spells it, so a Hasura table type is
130
+ `"pokemon_v2_pokemon"` and not a Ruby-cased guess at it.
131
+
132
+ A pin **merges**, and pins a **subtree** as readily as a leaf: name the fields
133
+ the example is about and everything else in the selection is still fabricated. A
134
+ pinned list is exactly as long as you write it — `{}` means "another one, all
135
+ fabricated".
136
+
137
+ ```ruby
138
+ graphql_fake("Reader.name" => "Ada",
139
+ "Reader.orders" => [{ "status" => "PAID" }, {}])
140
+ ```
141
+
142
+ Inside a subtree the keys are *response* keys, as they come back on the wire
143
+ (`priceCents`, or an alias you selected); one the query doesn't select is refused
144
+ and spellchecked, same as a typo'd coordinate. At a union or interface, name the
145
+ member with `"__typename"`.
284
146
 
285
147
  An **object pin** is anything answering the field names — a FactoryBot build, a
286
148
  model, a `Struct`, an `OpenStruct`. For each selected field the fake calls the
287
- snake_cased reader, puts a Ruby value on the wire the way its
149
+ snake_cased reader, puts the value on the wire the way its
288
150
  [scalar registration](scalars.md) serializes it (a `Time` as its iso8601 string,
289
151
  a `T::Enum` as its value), recurses into nested objects and arrays of them, and
290
152
  **fabricates any field the object doesn't answer**. Readers are field names, not
291
153
  aliases; `__typename` comes from the key, so at a union or interface pin the
292
- concrete type (`"Person"`, never `"Named"`). One thing rspec's seed can't reach:
293
- a FactoryBot sequence advances on its own counter, so an object built from one
294
- isn't reproduced by `--seed`.
154
+ concrete type (`"Person"`, never `"Named"`). A FactoryBot sequence advances on
155
+ its own counter, which is the one thing `--seed` can't reproduce.
295
156
 
296
157
  A **scalar type pin** is the one thing a scalar registered as *your own class*
297
158
  needs — only `Money.parse` knows what wire value it accepts — so without one
298
- fabrication refuses at the path it reached (`at reader.orders.0.total`) and
299
- names the pin to add, rather than feeding your cast a placeholder that fails
300
- deep inside `from_h`. A scalar registered as `BigDecimal`, `Time`, `Date`,
301
- `Integer`, `Float`, `String` or `T::Boolean` needs nothing. The key is the
302
- **schema's scalar name**, not the Ruby class it maps to, so two scalars that
303
- both deserialize into `Money` want a pin each. Suite-wide, the same hash is
304
- `config.overrides`, and the [cassette anonymizer](cassettes.md) reads it too.
305
-
306
- A pin is **what the wire carries** `"12.00"`, not `Money.parse("12.00")`
307
- but the object is accepted wherever the registration can serialize one, which
308
- is the same rule an object pin's fields already follow. A `serialize:` **Proc**
309
- builds source rather than converting a value, so a registration spelled that
310
- way has nothing to run: pin the wire value there, and the fake says so if you
311
- don't.
159
+ fabrication refuses at the path it reached (`at reader.orders.0.total`) and names
160
+ the pin to add, rather than feeding your cast a placeholder that fails deep
161
+ inside `from_h`. A scalar registered as `BigDecimal`, `Time`, `Date`, `Integer`,
162
+ `Float`, `String` or `T::Boolean` needs nothing. The key is the **schema's scalar
163
+ name**, not the Ruby class it maps to, so two scalars that both deserialize into
164
+ `Money` want a pin each. What you pin there is **what the wire carries**
165
+ `"12.00"`, not `Money.parse("12.00")` though the object is accepted wherever
166
+ the registration can serialize one. (A `serialize:` **Proc** builds source rather
167
+ than converting a value, so a registration spelled that way has nothing to run:
168
+ pin the wire value, and the fake says so if you don't.) Suite-wide, the same hash
169
+ is `config.overrides`, and the [cassette anonymizer](cassettes.md) reads it too.
312
170
 
313
171
  Pins lead and options follow — `graphql_fake("Money" => "12.00", values:
314
- :literal)`. Pins and options are the same keywords, told apart by a lookup: a
315
- key the fake takes is an option, a key **your schema** knows is a pin, and a
316
- key that is neither is refused naming both. So a lowercase type pins as
317
- readily as a capitalized one `graphql_fake("pokemon_v2_pokemon" => …)` for a
318
- Hasura API. `overrides:` takes the same hash by keyword, and the leading pins
319
- win where both name a key; written as that leading hash a key is only ever a
320
- pin, which is the spelling for a schema whose own vocabulary collides with an
321
- option name. The refusal is the same at every door: `FakeClient.new`,
322
- `graphql_fake`, `Router.new(fake:)` and `graphql_router(fake:)`.
172
+ :literal)`. They are the same keywords, told apart by a lookup: a key the fake
173
+ takes is an option, a key **your schema** knows is a pin, and a key that is
174
+ neither is refused naming both. `overrides:` takes the same hash by keyword, and
175
+ the leading pins win where both name a keywrite it as that leading hash when a
176
+ schema's own vocabulary collides with an option name. The refusal is the same at
177
+ every door: `FakeClient.new`, `graphql_fake`, `Router.new(fake:)` and
178
+ `graphql_router(fake:)`.
323
179
 
324
180
  **A pin answers every call the same way**, which is how a paging loop fed by a
325
181
  fake runs forever — page two is as full as page one.
326
- `GraphWeaver::Testing::Sequence` chains clients and repeats the last, so an
327
- empty pin on the second one is what ends the loop:
328
-
329
- ```ruby
330
- page = GraphWeaver::Testing::FakeClient.new(schema:, overrides: { "pokemon_v2_pokemon" => [{ "name" => "pikachu" }] })
331
- empty = GraphWeaver::Testing::FakeClient.new(schema:, overrides: { "pokemon_v2_pokemon" => [] })
332
-
333
- GraphWeaver.client = GraphWeaver::Testing::Sequence.new(page, empty)
334
- ```
335
-
336
- It is the same chain a retry test uses — [simulating
337
- failures](#simulating-failures) has that one.
338
-
339
- ### The example that's *about* the data
340
-
341
- Fabricated data answers "does this render", not "does it render Ada's two
342
- orders". `graphql_fake` is the tag with options — same client, built where
343
- the example can say what it needs:
344
-
345
- ```ruby
346
- it "shows the two paid orders", graphql: :fake do
347
- graphql_fake("Reader.name" => "Ada",
348
- "Reader.orders" => [{ "status" => "PAID" }, {}])
349
-
350
- expect(DashboardQuery.execute!.reader.orders.size).to eq 2
351
- end
352
- ```
353
-
354
- An override pins a **subtree** as readily as a leaf, and **merges**: name
355
- the fields the example is about and everything else in the selection is
356
- still fabricated. A pinned list is exactly as long as you write it — `{}`
357
- means "another one, all fabricated". Inside a subtree the keys are
358
- *response* keys, as they come back on the wire (`priceCents`, or an alias
359
- you selected); one the query doesn't select is refused and spellchecked,
360
- same as a typo'd coordinate. At a union or interface, name the member with
361
- `"__typename"`.
182
+ `GraphWeaver::Testing::Sequence.new(page, empty)` chains clients and repeats the
183
+ last, so a fake whose pin is `[]` on the second one is what ends the loop.
362
184
 
363
185
  `graphql_fake` returns the client, which records what it was asked:
364
186
 
@@ -369,17 +191,16 @@ expect(fake.requests.size).to eq 1 # memoized
369
191
  expect(fake.requests.first[:variables]).to eq({ "id" => "1" })
370
192
  ```
371
193
 
372
- It works in a `before` block, an example body, or a shared context — and
373
- with no tag at all, since it installs the client itself. The tag is
374
- `graphql_fake` with no options.
375
-
376
- One thing to know: **two identical queries fabricate different data**, so
377
- assert a memoization with `requests.size`, not by comparing two responses.
194
+ It works in a `before` block, an example body, or a shared context — and with no
195
+ tag at all, since it installs the client itself. The tag is `graphql_fake` with
196
+ no options. One thing to know: **two identical queries fabricate different
197
+ data**, so assert a memoization with `requests.size`, not by comparing two
198
+ responses.
378
199
 
379
200
  ## Real resolvers — `graphql: :in_process`
380
201
 
381
- Your actual resolvers, your actual `context`, in the same process — no
382
- socket, no serialization, and a resolver's real backtrace when it raises.
202
+ Your actual resolvers, your actual `context`, in the same process — no socket,
203
+ no serialization, and a resolver's real backtrace when it raises.
383
204
 
384
205
  ```ruby
385
206
  it "hides other people's drafts", graphql: :in_process do
@@ -388,73 +209,51 @@ it "hides other people's drafts", graphql: :in_process do
388
209
  end
389
210
  ```
390
211
 
391
- The live schema *class* is found for you (a schema dump has no resolvers,
392
- so it won't do). If two loaded classes match, or none does, it says so and
393
- asks for `config.schema = MySchema` — and in Rails, remember that an
394
- autoloaded schema isn't loaded until something references it.
212
+ The live schema *class* is found for you (a schema dump has no resolvers, so it
213
+ won't do). If two loaded classes match, or none does, it says so and asks for
214
+ `config.schema = MySchema` — and in Rails, remember that an autoloaded schema
215
+ isn't loaded until something references it.
395
216
 
396
217
  A federated app has no one live class, so the example says which subgraph it
397
- means. Testing one subgraph's resolvers directly is a different question from
398
- `graphql: :router`, which plans across the whole graph and stitches; both are
399
- worth asking, and a suite asks them of different subgraphs:
400
-
401
- ```ruby
402
- it "rejects a review from a blocked reader" do
403
- graphql_in_process(Reviews::Schema)
404
-
405
- end
406
- ```
407
-
408
- Like `graphql_fake`, `graphql_in_process` needs no tag, works in a `before`
409
- block, and is restored after the example.
218
+ means: `graphql_in_process(Reviews::Schema)`. Testing one subgraph's resolvers
219
+ directly is a different question from `graphql: :router`, which plans across the
220
+ whole graph and stitches; both are worth asking, and a suite asks them of
221
+ different subgraphs. Like `graphql_fake`, `graphql_in_process` needs no tag,
222
+ works in a `before` block, and is restored after the example.
410
223
 
411
224
  ### The context your resolvers see
412
225
 
413
- `graphql_context` is available in every example. It **merges** onto
414
- `config.context` the baseline survives unless you override a key — reaches
415
- every stand-in the example runs through (all your graphs', and the ones
416
- `:wire` serves behind its endpoints), and is **reset before the next
417
- example**, so one example running as somebody else can't leak into the one
418
- after it.
226
+ `graphql_context` is available in every example, and is the *only* way to set a
227
+ context from inside one — `config.context` is the suite baseline, read when an
228
+ example's clients are built, before any `before` hook runs, so setting it there
229
+ is refused rather than silently dropped. It **merges** onto that baseline,
230
+ reaches every stand-in the example runs through (all your graphs', and the ones
231
+ `:wire` serves behind its endpoints), and is **reset before the next example**,
232
+ so one example running as somebody else can't leak into the one after it.
419
233
 
420
- It is also the *only* way to set a context from inside an example.
421
- `config.context` is the suite baseline, read when an example's clients are
422
- built — before any `before` hook runs — so setting it there is refused rather
423
- than silently dropped.
424
-
425
- Context is setup, so it usually belongs in a `before` block — a group of
426
- examples sharing one identity says who they are once:
234
+ Context is setup, so it usually belongs in a `before` block a group of examples
235
+ sharing one identity says who they are once:
427
236
 
428
237
  ```ruby
429
238
  describe "as the owner", graphql: :in_process do
430
239
  before { graphql_context(current_user: alice) }
431
-
432
- it "shows the drafts" do
433
- expect(DraftsQuery.execute!.drafts.size).to eq 2
434
- end
435
240
  end
436
241
  ```
437
242
 
438
- The reset runs ahead of any group hook, so each example re-applies that
439
- `before` from the same baseline rather than stacking onto the last one's
440
- context. Set it inline for the one-off, or pass a block to scope it for the
441
- example that needs two identities:
442
-
443
- ```ruby
444
- graphql_context(admin: true) { expect(SettingsQuery.execute!.settings).to be_present }
445
- ```
446
-
447
- Called with nothing it reads the context back. Under `graphql: :fake` it
448
- refuses: there are no resolvers to receive a context, and silently ignoring
449
- one would leave an example asserting on data nothing scoped. Pin the data
450
- itself instead — `graphql_fake("Person.name" => "Ada")`, above.
243
+ The reset runs ahead of any group hook, so each example re-applies that `before`
244
+ from the same baseline rather than stacking onto the last one's context. Pass a
245
+ block to scope it for the example that needs two identities
246
+ `graphql_context(admin: true) { }` — and call it with nothing to read the
247
+ context back. Under `graphql: :fake` it refuses: there are no resolvers to
248
+ receive a context, and silently ignoring one would leave an example asserting on
249
+ data nothing scoped. Pin the data itself instead.
451
250
 
452
251
  ## A federated graph — `graphql: :router`
453
252
 
454
253
  Same thing across a federated graph: the tag builds a
455
- [`Testing::Router`](federation.md#the-local-router), which plans the query
456
- across your subgraphs and runs it against those **real resolvers** — no
457
- gateway, no node, no sockets.
254
+ [`Testing::Router`](federation.md#the-local-router), which plans the query across
255
+ your subgraphs and runs it against those **real resolvers** — no gateway, no
256
+ node, no sockets.
458
257
 
459
258
  ```ruby
460
259
  describe "the dashboard", graphql: :router do
@@ -469,19 +268,18 @@ A router is built once per supergraph (parsing one per example would be real
469
268
  time) and stands in for that graph's modules; its context is reset from
470
269
  `config.context` every time.
471
270
 
472
- `graphql_router` is the tag with options, the way `graphql_fake` is — one
473
- option, `fake:`, saying how the subgraphs the router
271
+ `graphql_router` is the tag with options, the way `graphql_fake` is — one option,
272
+ `fake:`, saying how the subgraphs the router
474
273
  [fakes](federation.md#a-supergraph-only-partly-local) fabricate: the pins and
475
- options `graphql_fake` takes, in one hash:
274
+ options `graphql_fake` takes, in one hash.
476
275
 
477
276
  ```ruby
478
277
  graphql_router(fake: { "Shipment.carrier" => "UPS", list_size: 2 })
479
278
  ```
480
279
 
481
- It names no schema, so with more than one graph it refuses: the tag alone
482
- already routes each module through its own graph's supergraph, and
483
- `config.router = { fake: … }` says how the faked subgraphs fabricate for the
484
- suite.
280
+ It names no schema, so with more than one graph it refuses: the tag alone already
281
+ routes each module through its own graph's supergraph, and `config.router = {
282
+ fake: … }` says how the faked subgraphs fabricate for the suite.
485
283
 
486
284
  What it plans, what it **refuses** and why, how subgraphs are matched to your
487
285
  schema classes, and what to do about a supergraph only partly local:
@@ -505,21 +303,14 @@ a production router
505
303
 
506
304
  `path` survives; nothing else about the subgraph does — including the
507
305
  `extensions.code` your own subgraph set, since it is a subgraph like any other.
508
- So assert on `path` and on what your app does with the failure, not on a
509
- message, a code, or the `service` stamp.
510
- An example that needs the redacted shape gets it from
511
- [`Failure`](#simulating-failures), which reproduces it exactly:
306
+ So assert on `path` and on what your app does with the failure, not on a message,
307
+ a code, or the `service` stamp. An example that needs the redacted shape gets it
308
+ from [`Failure`](#simulating-failures), which reproduces it exactly:
512
309
 
513
310
  ```ruby
514
- it "degrades when shipping is unavailable" do
515
- response = ProductQuery.execute(
516
- client: GraphWeaver::Testing::Failure.graphql(
517
- "Subgraph errors redacted", path: ["product", "shippingEstimate"],
518
- ),
519
- upc: "p1",
520
- )
521
- expect(response.errors.first.path).to eq ["product", "shippingEstimate"]
522
- end
311
+ GraphWeaver::Testing::Failure.graphql(
312
+ "Subgraph errors redacted", path: ["product", "shippingEstimate"],
313
+ )
523
314
  ```
524
315
 
525
316
  ## Over the wire — `graphql: :wire`
@@ -528,13 +319,8 @@ Your schema, served at the endpoint your own client posts to — with
528
319
  **`GraphWeaver.client` left exactly where it is**. So the request really is
529
320
  serialized, posted through your middleware, answered at the far end, and read
530
321
  back by `from_h` over the server's own bytes. That is the half the other three
531
- tags skip: they sit *in* the client slot, so the transport your app ships —
532
- APM tracing, a caller tag, mTLS — never runs.
533
-
534
- **It needs [webmock](https://github.com/bblimke/webmock) and
535
- [rack](https://github.com/rack/rack)** in the Gemfile (`group :test`) — webmock
536
- hooks Net::HTTP, Faraday and HTTPX underneath, and its `to_rack` builds the Rack
537
- env with rack.
322
+ tags skip: they sit *in* the client slot, so the transport your app ships — APM
323
+ tracing, a caller tag, mTLS — never runs.
538
324
 
539
325
  ```ruby
540
326
  it "sends the caller tag", graphql: :wire do
@@ -545,21 +331,33 @@ it "sends the caller tag", graphql: :wire do
545
331
  end
546
332
  ```
547
333
 
548
- What sits behind each endpoint is **what that graph is** — decided the way the
549
- other tags already decide it, **per graph**, in descending faithfulness: that
334
+ **It needs [webmock](https://github.com/bblimke/webmock) and
335
+ [rack](https://github.com/rack/rack)** in the Gemfile (`group :test`), and
336
+ webmock has to be *enabled* — `require "webmock/rspec"` in the spec helper, in
337
+ either order with `graph_weaver/rspec`. Having it in the Gemfile is not enough:
338
+ `Bundler.require` loads webmock without installing its adapters, so `:wire`
339
+ checks and refuses *before* the first request rather than letting it leave the
340
+ suite. The tag adds one stub per endpoint and takes each back after the example —
341
+ it never disables net connections on your behalf, and never resets stubs it
342
+ didn't make.
343
+
344
+ **Every endpoint an example can reach is served**, one per graph: the `client`
345
+ each [declared graph](getting_started.md#more-than-one-schema) names, or
346
+ `GraphWeaver.client` for a graph naming none — so a billing module posts to
347
+ billing's url and is answered by billing's schema. What
348
+ sits behind each is **what that graph is**, in descending faithfulness: that
550
349
  graph's [router](#a-federated-graph--graphql-router) when it is in a composed
551
350
  supergraph, its [live schema class](#real-resolvers--graphql-in_process) when it
552
- has one, else a [fake](#fabricated-data--graphql-fake) of its schema. So one
553
- federated graph doesn't put its router behind a plain graph's url, and an app
554
- that is a pure *client* of someone else's API a committed dump and no
555
- resolvers to serve gets a schema-correct server without writing one. A graph
556
- with no schema at all is refused, **by `:wire`'s own name**: the one thing the
557
- other tags can fall back to and this one can't is your client's own schema,
558
- since reading it means introspecting the endpoint `:wire` has just stubbed.
559
- Commit a dump, or set `config.schema`.
560
-
561
- **It says which, on the logger** the choice is the one thing this tag makes
562
- for you, and it is invisible from inside the example. One line per endpoint, at
351
+ has one, else a [fake](#fabricated-data--graphql-fake) of its schema. So an app
352
+ that is a pure *client* of someone else's API gets a schema-correct server
353
+ without writing one. A graph with no schema at all is refused, **by `:wire`'s own
354
+ name** the one fallback the other tags have and this one can't use is your
355
+ client's own schema, since reading it means introspecting the endpoint `:wire`
356
+ has just stubbed. Commit a dump, or set `config.schema`. A graph whose `client`
357
+ posts nowhere is refused by name too.
358
+
359
+ **It says which, on the logger** — the choice is the one thing this tag makes for
360
+ you, and it is invisible from inside the example. One line per endpoint, at
563
361
  `info` (a Rails app already has a logger; elsewhere set `GraphWeaver.logger`):
564
362
 
565
363
  ```
@@ -567,50 +365,22 @@ graph_weaver: :wire serving Shop::Schema (in-process) at https://api.example.com
567
365
  ```
568
366
 
569
367
  When a **fake** stands in while the process has a `GraphQL::Schema` class that
570
- nothing named, that line is a `warn` instead and names the class the case
571
- worth catching, because an app that owns real resolvers otherwise goes green
572
- against fabricated data with nothing said:
368
+ nothing named, that line is a `warn` instead and names the class, telling you to
369
+ set `GraphWeaver::Testing.config.schema` — the case worth catching, because an
370
+ app that owns real resolvers otherwise goes green against fabricated data with
371
+ nothing said. A warning rather than a refusal, because a loaded class isn't proof
372
+ you meant it *here* — a federated suite loads every subgraph's — and a fake
373
+ behind the wire is a thing to want.
573
374
 
574
- ```
575
- graph_weaver: :wire serving a fake at https://api.example.com/graphql Shop::Schema
576
- is loaded and nothing named it, so your resolvers did not run. To serve them,
577
- name it: GraphWeaver::Testing.config.schema = Shop::Schema
578
- ```
579
-
580
- A warning rather than a refusal, because a loaded class isn't proof you meant
581
- it *here* — a federated suite loads every subgraph's — and a fake behind the
582
- wire is a thing to want. Name the class for the suite, and call `graphql_fake`
583
- in the examples that want fabricated data.
375
+ **A helper says what goes behind the wire.** Under the other tags a `graphql_*`
376
+ helper takes the client slot; under `:wire` it is served insteadthe client
377
+ slot has to keep your own client for the transport to run at all — so
378
+ `graphql_fake("Reader.orders" => [{ "status" => "PAID" }, {}])` reads exactly as
379
+ it does under `:fake`, and the helper returns the client it serves, so
380
+ `fake.requests` is what the *endpoint* was asked.
584
381
 
585
- **A helper says what goes behind the wire.** Under the other tags a
586
- `graphql_*` helper takes the client slot; under `:wire` it is served instead
587
- the client slot has to keep your own client for the transport to run at all —
588
- so pins read exactly as they do under `:fake`:
589
-
590
- ```ruby
591
- it "renders two orders, through our own transport", graphql: :wire do
592
- graphql_fake("Reader.orders" => [{ "status" => "PAID" }, {}])
593
-
594
- expect(DashboardQuery.execute!.reader.orders.size).to eq 2
595
- end
596
- ```
597
-
598
- It returns the client it serves, so `fake.requests` is what the *endpoint* was
599
- asked. `graphql_router(fake: …)` and `graphql_in_process(Reviews::Schema)` say
600
- the same thing for the other two.
601
-
602
- **Every endpoint an example can reach is served**, one per graph: the client
603
- each [declared graph](getting_started.md#more-than-one-schema) bakes into its
604
- modules with `client:`, or `GraphWeaver.client` for a graph that bakes none —
605
- so an app whose graphs all bake one needs no app default at all. Each gets
606
- that graph's own schema behind it, so a billing module posts to billing's
607
- url and is answered by billing's schema. A graph whose baked client posts
608
- nowhere is refused by name, rather than its requests quietly leaving the
609
- suite.
610
-
611
- **Identity comes from the request.** A `context:` **proc** is called per
612
- request with the headers as sent, which is the seam nothing above the wire can
613
- test:
382
+ **Identity comes from the request.** A `context:` **proc** is called per request
383
+ with the headers as sent, which is the seam nothing above the wire can test:
614
384
 
615
385
  ```ruby
616
386
  GraphWeaver::Testing.configure do |config|
@@ -621,46 +391,36 @@ end
621
391
  A hash still works, and is still the baseline `graphql_context` merges onto; a
622
392
  proc replaces it, and `graphql_context` then says so rather than merging onto
623
393
  something that isn't there. (Rack drops a header's capitalization, so `X-CALLER`
624
- arrives as `X-Caller`.)
625
-
626
- Like every suite setting, it goes in `Testing.configure` or an `around` —
627
- [never a plain `before`](#nothing-to-configure), `:wire` least of all, since the
628
- tag stubs this example's endpoints in a `before` hook of its own.
629
-
630
- **And webmock has to be *enabled*** — `require "webmock/rspec"` in the spec
631
- helper, in either order with `graph_weaver/rspec`. Having it in the Gemfile is
632
- not enough: `Bundler.require` loads webmock without installing its adapters, so
633
- `:wire` checks and refuses *before* the first request rather than letting it
634
- leave the suite. That is what makes this a *transport* test rather than a mock
635
- of one — every transport [documented here](transports.md) runs unchanged,
636
- pooling and all. The tag adds one stub per endpoint and takes each back after
637
- the example — it never disables net connections on your behalf, and never
638
- resets stubs it didn't make.
394
+ arrives as `X-Caller`.) Like every suite setting, it goes in `Testing.configure`
395
+ or an `around` — [never a plain `before`](#nothing-to-configure), `:wire` least
396
+ of all, since the tag stubs this example's endpoints in a `before` hook of its
397
+ own.
639
398
 
640
399
  **The wire adds a hop, not a capability.** Behind a router, everything
641
- [it refuses](federation.md#what-it-refuses) is still refused, before any
642
- resolver runs. Behind a fake, what you are testing is your *transport* — the
643
- request your middleware wrote, the headers it sent, the retry it does on a 500,
644
- and that `from_h` reads real JSON off a socket rather than a Ruby hash you
645
- handed it. What it can't tell you is whether your `cast:` agrees with the real
646
- server: the fabricated bytes are written to match your own
647
- [scalar registrations](scalars.md), so the round trip agrees with itself. Put
648
- the live schema class behind the wire for that, or pin a real response with a
649
- [cassette](cassettes.md).
400
+ [it refuses](federation.md#what-it-refuses) is still refused, before any resolver
401
+ runs. Behind a fake, what you are testing is your *transport* — the request your
402
+ middleware wrote, the headers it sent, the retry it does on a 500, and that
403
+ `from_h` reads real JSON off a socket. What it can't tell you is whether your
404
+ `cast:` agrees with the real server: the fabricated bytes are written to match
405
+ your own [scalar registrations](scalars.md), so the round trip agrees with
406
+ itself. Put the live schema class behind the wire for that, or pin a real
407
+ response with a [cassette](cassettes.md).
650
408
 
651
409
  `GraphWeaver::Testing::Endpoint` is an ordinary Rack app wrapping anything that
652
- satisfies the [client contract](transports.md) so mount it yourself if you'd
653
- rather have a real socket:
654
-
655
- ```ruby
656
- run GraphWeaver::Testing::Endpoint.new(router) # config.ru, or a Puma in a thread
657
- ```
410
+ satisfies the [client contract](transports.md), so mount it yourself
411
+ (`run GraphWeaver::Testing::Endpoint.new(router)`) if you'd rather have a real
412
+ socket. A client answering a `context:` **proc** is served **one request at a
413
+ time** — answering one means setting the client's context for the length of that
414
+ dispatch, so the identity a request asked for is the identity it gets, whatever
415
+ else is in flight. The lock is the client's own, so it holds however the endpoint
416
+ is mounted. A `context:` hash is served **concurrently**: nothing writes it, so
417
+ there is nothing to serialize.
658
418
 
659
419
  ### Making the served endpoint fail
660
420
 
661
- The tag adds **one stub per endpoint**, and webmock answers with the *last*
662
- stub declared for a url — so an example that wants the server to misbehave
663
- declares its own, and it wins for that example:
421
+ The tag adds **one stub per endpoint**, and webmock answers with the *last* stub
422
+ declared for a url — so an example that wants the server to misbehave declares
423
+ its own, and it wins for that example:
664
424
 
665
425
  ```ruby
666
426
  it "surfaces a 503, after the retries it's allowed", graphql: :wire do
@@ -673,33 +433,28 @@ it "surfaces a 503, after the retries it's allowed", graphql: :wire do
673
433
  }
674
434
  end
675
435
 
676
- it "surfaces a timeout", graphql: :wire do
677
- stub_request(:post, "https://api.example.com/graphql").to_timeout
678
-
679
- expect { PlaceOrderMutation.execute!(input:) }.to raise_error(GraphWeaver::TransportError)
680
- end
436
+ # .to_timeout instead, and the call raises GraphWeaver::TransportError
681
437
  ```
682
438
 
683
- `to_timeout` raises what a timeout raises, instantly it tests what your app
684
- *does* with one. It does not test that a `read_timeout:` of yours is short
685
- enough, and neither does sleeping inside `to_return { |req| … }`: webmock stands
686
- in for the socket, so there is nothing to time out and the call simply takes
687
- that long and succeeds. A timeout *value* can only be proven against a
688
- genuinely slow server — the [`Endpoint`](#over-the-wire--graphql-wire) above on
689
- a real port, or a `TCPServer` that dawdles before it replies.
690
-
691
- That is a **served** failure: your transport reads the status and the headers
692
- off a real response, and your `retries:` budget really spends itself against it
693
- — including [the one attempt a mutation gets](transports.md#retries). That is
694
- the half a [`Failure` client](#simulating-failures) can't reach, since those sit
695
- *in* the client slot and raise above the wire. Use `Failure.server` /
439
+ That is a **served** failure: your transport reads the status and the headers off
440
+ a real response, and your `retries:` budget really spends itself against it
441
+ including [the one attempt a mutation gets](transports.md#retries). That is the
442
+ half a [`Failure` client](#simulating-failures) can't reach, since those sit *in*
443
+ the client slot and raise above the wire. Use `Failure.server` /
696
444
  `Failure.timeout` when the example is about your `rescue`; a stub when it is
697
445
  about the transport.
698
446
 
447
+ `to_timeout` raises instantly, so it tests what your app *does* with a timeout —
448
+ not that a `read_timeout:` of yours is short enough. Sleeping inside
449
+ `to_return { |req| … }` doesn't either: webmock stands in for the socket, so
450
+ there is nothing to time out and the call simply takes that long and succeeds. A
451
+ timeout *value* can only be proven against a genuinely slow server — the
452
+ `Endpoint` above on a real port, or a `TCPServer` that dawdles before it replies.
453
+
699
454
  ## Simulating failures
700
455
 
701
- Every failure mode is just a client, so error-handling paths are testable
702
- without a server that misbehaves on cue:
456
+ Every failure mode is just a client, so error-handling paths are testable without
457
+ a server that misbehaves on cue:
703
458
 
704
459
  ```ruby
705
460
  Failure = GraphWeaver::Testing::Failure
@@ -732,11 +487,10 @@ GraphWeaver::Testing::FakeClient.new(schema:, fail_at: "people.2.pets.name")
732
487
  ```
733
488
 
734
489
  `Failure.graphql` is the **whole response** failing — `data` is null unless you
735
- pass `data:`, which is what makes it a partial one. Shape the error by naming
736
- its wire fields beside the message (`code:`, `extensions:`, `path:`,
737
- `locations:` — anything else is refused rather than swallowed), so a rejection
738
- that follows the [`extensions.input`
739
- convention](errors.md#what-your-server-can-send) is one call:
490
+ pass `data:`, which is what makes it a partial one. Shape the error by naming its
491
+ wire fields beside the message (`code:`, `extensions:`, `path:`, `locations:` —
492
+ anything else is refused rather than swallowed), so a rejection that follows the
493
+ [`extensions.input` convention](errors.md#what-your-server-can-send) is one call:
740
494
 
741
495
  ```ruby
742
496
  # a plain code, the coarse bucket every server states
@@ -757,31 +511,134 @@ Failure.graphql({ message: "boom", path: ["person"] }, "and again", data: { "per
757
511
 
758
512
  **A server's own rejection lands in `#errors` unless it marked it.** A
759
513
  graphql-ruby `validates:` failure carries no `extensions` at all, so it is an
760
- ordinary error in `response.errors` — `#input_errors` is empty, because
761
- "the value was out of range" and "the database is down" are the same bytes.
762
- Assert on `errors` for that, and reach for `#input_errors` only once your
763
- server [says the error is about the input](errors.md#when-the-server-rejects-the-input);
764
- the `Failure.graphql(code:, extensions:)` call above is the shape that says it.
514
+ ordinary error in `response.errors` — `#input_errors` is empty, because "the
515
+ value was out of range" and "the database is down" are the same bytes. Assert on
516
+ `errors` for that, and reach for `#input_errors` only once your server
517
+ [says the error is about the input](errors.md#when-the-server-rejects-the-input).
765
518
 
766
519
  ## Capture and replay
767
520
 
768
- Cassettes record real API responses and replay them offline, above the
769
- transport (no HTTP interception):
521
+ `GraphWeaver::Testing.cassette("github", client: live)` returns a client that
522
+ records real API responses and replays them offline, above the transport (no HTTP
523
+ interception). Re-record with `GRAPHWEAVER_RECORD=1`, and set
524
+ `config.anonymize = true` so the response is scrubbed on its way to disk. The
525
+ full workflow guide is **[cassettes](cassettes.md)**.
526
+
527
+ ## Nothing to configure
528
+
529
+ Each mode works out what to run against **per graph** — with more than one, the
530
+ honest answer varies per module — and **refuses, naming what it looked for,
531
+ rather than guessing**:
532
+
533
+ - **the schema** is `config.schema` if you set one, else the one that
534
+ [graph](getting_started.md#more-than-one-schema) names, else the committed dump
535
+ at `GraphWeaver.schema_path`, else the schema `GraphWeaver.client` talks to. A
536
+ fake reads the scalar registrations of the graph it is answering, so it invents
537
+ the wire value that graph's generated cast expects. (Pins and `overrides:` stay
538
+ suite-wide, keyed by scalar name — one `"Money"` override for the run.)
539
+ - **`:in_process`** needs the live schema *class*, since only that has resolvers:
540
+ the one that graph names, else the one your client already runs in-process,
541
+ else the loaded class that defines everything the schema declares — the same
542
+ derive-verify-refuse rule that
543
+ [maps subgraphs](federation.md#which-schema-serves-which-subgraph).
544
+ - **`:router`** plans against the composed supergraph **that graph** names, else
545
+ `config.router = { supergraph: … }`, else the committed dump when *that*
546
+ carries `@join__*` markers, else the dump your own client was built from
547
+ (`GraphWeaver.new("supergraph.graphql")`) — which for a federated app is
548
+ usually no config at all. A graph that is in no supergraph is refused **by
549
+ name**, rather than planned against another graph's. A client's *schema* can't
550
+ stand in for one — it is the API schema the router serves, with the `@join__*`
551
+ routing table stripped out — but the file it was read from carries the table.
552
+ Subgraphs are derived either way.
553
+
554
+ So configure only to override a derivation, or to tune fabricated values — in the
555
+ same file as the require, since support files load in sorted order and one naming
556
+ `GraphWeaver::Testing` before it dies on `NameError`:
557
+
558
+ ```ruby
559
+ GraphWeaver::Testing.configure do |config|
560
+ # config.schema = MySchema # the live class, rather than the dump
561
+ # config.router = { supergraph: Rails.root.join("supergraph.graphql") }
562
+ # config.router = { subgraphs: { "reviews" => :fake } } # either key alone
563
+ # config.context = { tenant: } # baseline context every example starts from
564
+ # config.default_mode = :fake # what an UNtagged example runs against;
565
+ # # :live (the default) leaves your client
566
+ # # alone, and graphql: :live opts one out
567
+ # config.seed = 4242 # defaults to rspec's own --seed
568
+ # config.overrides = { "Money" => "12.00", "Person.name" => "Daniel" }
569
+ # config.list_size = 1..3
570
+ end
571
+ ```
572
+
573
+ **The rule: a helper sets the stand-in for the graph it names; the tag sets the
574
+ mode for every graph no helper named.** `graphql_fake`, `graphql_in_process` and
575
+ `graphql_router` stand in for the modules of the graph their schema names — your
576
+ only graph when they name none — and with more than one, `graph:` says which:
577
+ `graphql_fake(graph: :poke, "pokemon_v2_pokemon.name" => "pikachu")`. A schema
578
+ class names its graph and its schema in one word
579
+ (`graphql_in_process(Reviews::Schema)`), but only for a graph that runs that
580
+ class in-process; `graph:` is the handle every graph has. Naming none they could
581
+ reach is refused, naming your graphs.
582
+
583
+ So one example can run two graphs in two modes — the federated one through its
584
+ router, the plain one faked — and neither helper disturbs the other's graph:
770
585
 
771
586
  ```ruby
772
- # records against the live client when the file is missing, replays after
773
- client = GraphWeaver::Testing.cassette("github", client: live)
587
+ it "renders the dashboard", graphql: :router do
588
+ graphql_fake(graph: :countries, "Country.name" => "Canada")
589
+ # :storefront routes through its supergraph (the tag); :countries is faked
590
+ end
774
591
  ```
775
592
 
776
- Re-record with `GRAPHWEAVER_RECORD=1`, and set `config.anonymize = true` so the
777
- response is scrubbed on its way to disk the query and its variables are the
778
- replay key and are recorded verbatim, so read a cassette before committing it.
779
- The full workflow guide is **[cassettes](cassettes.md)**.
593
+ A helper that speaks for the whole example *is* refused, though: with one graph,
594
+ or with no `graph:`/schema to narrow it, `graphql: :fake` plus
595
+ `graphql_in_process` is two answers to one question, and the later one winning
596
+ silently would hide which was the mistake.
597
+
598
+ Anything whose honest answer differs per example belongs on the fake instead —
599
+ `graphql_fake(null_chance: 1.0)` for the example that's about an empty state,
600
+ `graphql_fake(values: :literal)` for the one that reads better without faker's
601
+ prose. A suite-wide `null_chance` would sprinkle nils through every *other*
602
+ example, one run in ten, on a seed the failure doesn't name.
603
+
604
+ **Configure at load, or in an `around` — never in a plain `before`.** The tag
605
+ builds this example's clients in a `before` hook of its own, and rspec runs that
606
+ one ahead of yours, so a `before` setting `config.schema`, `config.router` or
607
+ `config.context` arrives after the decision it meant to change. It is
608
+ **refused**, not ignored — a green example running against the wrong stand-in is
609
+ the expensive outcome.
610
+
611
+ ### Fabricated list lengths
612
+
613
+ `list_size` is how long an **unbounded** list is — an Integer exactly that many,
614
+ a Range randomized within it, or a Hash saying it per list. A list with a
615
+ `first:`/`last:`/`limit:` argument is that long instead, whatever this says.
616
+
617
+ **Every list the fabricator reaches reads the same setting, so nested lists
618
+ multiply.** A query selecting `rows { owner { … } tags }` with `tags` uncapped
619
+ fabricates `list_size` rows and `list_size` tags *in each of them* — at 1600 that
620
+ is 2.5M tags, and three nested lists cube it. Say it per list instead, keyed the
621
+ way a pin is (a `"Type.field"` coordinate or a bare field name), with `default:`
622
+ for the rest — `config.list_size = { "Row.tags" => 3, default: 1000 }` holds the
623
+ inner list at 3 however large the outer one grows. Or cap it in the query
624
+ (`tags(first: 3)`), where the query is yours to change.
625
+
626
+ **A list field whose name ends in `errors` fabricates empty** — `userErrors`,
627
+ `errors`, `mutationErrors`. The Relay/Shopify payload
628
+ (`placeOrder { order userErrors }`) is the ecosystem's mutation shape, and a
629
+ fabricated order beside a fabricated failure is a response no server can send;
630
+ [pin it](#pins) to write the failure path
631
+ (`{ "userErrors" => [{ "message" => "Out of stock" }] }`).
632
+
633
+ Need the schema itself inside an example — to sample a field, or build a query on
634
+ the fly? The client in play exposes it as `GraphWeaver.client.schema`, and
635
+ `GraphWeaver::Testing.config.schema` reads back what `config.schema =` set,
636
+ falling back to the committed dump.
780
637
 
781
638
  ## Test-only generated modules
782
639
 
783
- They don't have to live in `app/` — `generated_paths` is an appendable list,
784
- so a support file can register a spec-local set:
640
+ They don't have to live in `app/` — `generated_paths` is an appendable list, so a
641
+ support file can register a spec-local set:
785
642
 
786
643
  ```ruby
787
644
  # spec/support/graph_weaver.rb
@@ -793,5 +650,5 @@ Both lines matter. In Rails the Railtie loads generated modules during boot,
793
650
  which is finished before `spec/support/*.rb` runs — so a path appended here is
794
651
  never loaded unless you load it. And keep the directory *outside*
795
652
  `spec/support/`: rspec-rails requires every `spec/support/**/*.rb` itself, in
796
- sorted order, so a generated module gets required before the shared `types.rb`
797
- it needs and dies on `LoadError`.
653
+ sorted order, so a generated module gets required before the shared `types.rb` it
654
+ needs and dies on `LoadError`.