graphiti 2.0.0.beta.2 → 2.0.0.beta.4

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 (269) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +30 -86
  3. data/.github/workflows/docs.yml +60 -0
  4. data/.github/workflows/release.yml +8 -8
  5. data/.gitignore +7 -0
  6. data/.npmrc +9 -0
  7. data/.standard.yml +4 -1
  8. data/Appraisals +33 -32
  9. data/CHANGELOG.md +41 -0
  10. data/README.md +13 -2
  11. data/UPGRADING.md +2 -68
  12. data/docs/concepts/backends-and-models.md +122 -0
  13. data/docs/concepts/endpoints.md +183 -0
  14. data/docs/concepts/links.md +212 -0
  15. data/docs/concepts/overview.md +80 -0
  16. data/docs/concepts/persisting.md +376 -0
  17. data/docs/concepts/relationships.md +527 -0
  18. data/docs/concepts/resources.md +677 -0
  19. data/docs/getting-started/first-api.md +289 -0
  20. data/docs/getting-started/installation.md +185 -0
  21. data/docs/intro.md +307 -0
  22. data/docs/js/authentication.md +63 -0
  23. data/docs/js/ddau.md +20 -0
  24. data/docs/js/extra-params.md +41 -0
  25. data/docs/js/index.md +112 -0
  26. data/docs/js/installation.md +120 -0
  27. data/docs/js/middleware.md +72 -0
  28. data/docs/js/models.md +202 -0
  29. data/docs/js/reads.md +494 -0
  30. data/docs/js/state-syncing.md +100 -0
  31. data/docs/js/writes.md +373 -0
  32. data/docs/reference/vandal.md +63 -0
  33. data/docs/reference/why.md +13 -0
  34. data/docs/topics/authorization.md +155 -0
  35. data/docs/topics/caching.md +55 -0
  36. data/docs/topics/customizing-sideloads.md +156 -0
  37. data/docs/topics/debugging.md +216 -0
  38. data/docs/topics/error-handling.md +210 -0
  39. data/docs/topics/etags.md +46 -0
  40. data/docs/topics/hopping-relationships.md +149 -0
  41. data/docs/topics/json-attributes.md +77 -0
  42. data/docs/topics/openstruct-models.md +50 -0
  43. data/docs/topics/remote-resources.md +291 -0
  44. data/docs/topics/testing.md +894 -0
  45. data/docs/topics/without-activerecord.md +324 -0
  46. data/docs/tutorial/index.md +58 -0
  47. data/docs/tutorial/step_0.md +107 -0
  48. data/docs/tutorial/step_1.md +199 -0
  49. data/docs/tutorial/step_2.md +312 -0
  50. data/docs/tutorial/step_3.md +142 -0
  51. data/docs/tutorial/step_4.md +135 -0
  52. data/docs/tutorial/step_5.md +69 -0
  53. data/docs/tutorial/step_6.md +82 -0
  54. data/docs/tutorial/step_7.md +205 -0
  55. data/docs/tutorial/step_8.md +128 -0
  56. data/docs/tutorial/step_9.md +171 -0
  57. data/docs/upgrading.md +265 -0
  58. data/gemfiles/rails_7_1.gemfile +4 -3
  59. data/gemfiles/{rails_7_2_graphiti_rails.gemfile → rails_7_2.gemfile} +3 -3
  60. data/gemfiles/{rails_8_1_graphiti_rails.gemfile → rails_8_0.gemfile} +3 -3
  61. data/gemfiles/{rails_8_0_graphiti_rails.gemfile → rails_8_1.gemfile} +3 -3
  62. data/graphiti.gemspec +7 -5
  63. data/{deprecated_generators → lib/generators}/graphiti/api_test_generator.rb +7 -1
  64. data/{deprecated_generators → lib/generators}/graphiti/generator_mixin.rb +14 -1
  65. data/{deprecated_generators → lib/generators}/graphiti/install_generator.rb +19 -13
  66. data/{deprecated_generators → lib/generators}/graphiti/resource_generator.rb +43 -6
  67. data/{deprecated_generators → lib/generators}/graphiti/templates/index_request_spec.rb.erb +1 -1
  68. data/{deprecated_generators → lib/generators}/graphiti/templates/resource_reads_spec.rb.erb +6 -6
  69. data/{deprecated_generators → lib/generators}/graphiti/templates/show_request_spec.rb.erb +1 -1
  70. data/lib/graphiti/configuration.rb +2 -2
  71. data/lib/graphiti/error_serializers/conflict_request.rb +19 -0
  72. data/lib/graphiti/error_serializers/deprecated_constants.rb +48 -0
  73. data/lib/graphiti/error_serializers/invalid_request.rb +56 -0
  74. data/lib/graphiti/error_serializers/validation.rb +143 -0
  75. data/lib/graphiti/errors.rb +4 -23
  76. data/lib/graphiti/query.rb +1 -1
  77. data/lib/graphiti/rails/context.rb +33 -0
  78. data/lib/graphiti/rails/controller.rb +41 -0
  79. data/lib/graphiti/rails/debugging.rb +18 -0
  80. data/lib/graphiti/rails/exception_handlers.rb +77 -0
  81. data/lib/graphiti/rails/railtie.rb +139 -0
  82. data/lib/graphiti/rails/responders.rb +21 -0
  83. data/lib/graphiti/rails/test_helpers.rb +22 -0
  84. data/lib/graphiti/rails.rb +47 -29
  85. data/lib/graphiti/resource/configuration.rb +1 -0
  86. data/lib/graphiti/resource/interface.rb +2 -2
  87. data/lib/graphiti/resource/persistence.rb +14 -2
  88. data/lib/graphiti/resource/remote.rb +2 -2
  89. data/lib/graphiti/resource/sideloading.rb +1 -1
  90. data/lib/graphiti/resource.rb +13 -1
  91. data/lib/graphiti/responders.rb +7 -20
  92. data/lib/graphiti/schema.rb +5 -1
  93. data/lib/graphiti/schema_diff.rb +4 -0
  94. data/lib/graphiti/scope.rb +45 -37
  95. data/lib/graphiti/serializer.rb +6 -0
  96. data/lib/graphiti/sideload/belongs_to.rb +38 -5
  97. data/lib/graphiti/sideload/polymorphic_belongs_to.rb +27 -23
  98. data/lib/graphiti/sideload.rb +54 -35
  99. data/lib/graphiti/spec_helpers/errors.rb +73 -0
  100. data/lib/graphiti/spec_helpers/errors_proxy.rb +75 -0
  101. data/lib/graphiti/spec_helpers/helpers.rb +107 -0
  102. data/lib/graphiti/spec_helpers/node.rb +88 -0
  103. data/lib/graphiti/spec_helpers/rspec.rb +147 -0
  104. data/lib/graphiti/spec_helpers.rb +53 -0
  105. data/lib/graphiti/util/include_params.rb +2 -2
  106. data/lib/graphiti/util/persistence.rb +10 -11
  107. data/lib/graphiti/util/serializer_relationships.rb +41 -5
  108. data/lib/graphiti/version.rb +1 -1
  109. data/lib/graphiti-rails.rb +11 -0
  110. data/lib/graphiti.rb +34 -10
  111. data/lib/graphiti_errors.rb +11 -0
  112. data/lib/graphiti_spec_helpers/rspec.rb +3 -0
  113. data/lib/graphiti_spec_helpers.rb +11 -0
  114. data/lib/{graphiti/deprecated_tasks.rb → tasks/graphiti.rake} +6 -1
  115. data/package-lock.json +6199 -0
  116. data/package.json +5 -4
  117. data/website/.gitignore +20 -0
  118. data/website/README.md +43 -0
  119. data/website/docusaurus.config.js +141 -0
  120. data/website/package-lock.json +19474 -0
  121. data/website/package.json +46 -0
  122. data/website/sidebars.js +82 -0
  123. data/website/src/css/custom.css +58 -0
  124. data/website/src/pages/markdown-page.mdx +7 -0
  125. data/website/static/.nojekyll +0 -0
  126. data/website/static/1.13/2019/03/31/graphiti-1-0.html +205 -0
  127. data/website/static/1.13/2019/05/08/graphiti-1-1.html +212 -0
  128. data/website/static/1.13/2019/05/20/graphiti-1-2.html +214 -0
  129. data/website/static/1.13/2019/10/14/tutorial.html +198 -0
  130. data/website/static/1.13/CNAME +1 -0
  131. data/website/static/1.13/README.md +16 -0
  132. data/website/static/1.13/assets/css/syntax.css +60 -0
  133. data/website/static/1.13/assets/favicons/android-chrome-192x192.png +0 -0
  134. data/website/static/1.13/assets/favicons/android-chrome-256x256.png +0 -0
  135. data/website/static/1.13/assets/favicons/apple-touch-icon.png +0 -0
  136. data/website/static/1.13/assets/favicons/browserconfig.xml +9 -0
  137. data/website/static/1.13/assets/favicons/favicon-16x16.png +0 -0
  138. data/website/static/1.13/assets/favicons/favicon-32x32.png +0 -0
  139. data/website/static/1.13/assets/favicons/favicon.ico +0 -0
  140. data/website/static/1.13/assets/favicons/mstile-150x150.png +0 -0
  141. data/website/static/1.13/assets/favicons/safari-pinned-tab.svg +1 -0
  142. data/website/static/1.13/assets/favicons/site.webmanifest +19 -0
  143. data/website/static/1.13/assets/img/backend.gif +0 -0
  144. data/website/static/1.13/assets/img/conformity.png +0 -0
  145. data/website/static/1.13/assets/img/error_payload.png +0 -0
  146. data/website/static/1.13/assets/img/gh.png +0 -0
  147. data/website/static/1.13/assets/img/lifecycle.gif +0 -0
  148. data/website/static/1.13/assets/img/logo-500.png +0 -0
  149. data/website/static/1.13/assets/img/logo.png +0 -0
  150. data/website/static/1.13/assets/img/love-graffiti.jpg +0 -0
  151. data/website/static/1.13/assets/img/meta_total_count.png +0 -0
  152. data/website/static/1.13/assets/img/persist.jpg +0 -0
  153. data/website/static/1.13/assets/img/resource.gif +0 -0
  154. data/website/static/1.13/assets/img/rest-graffiti.jpg +0 -0
  155. data/website/static/1.13/assets/img/rest1.gif +0 -0
  156. data/website/static/1.13/assets/img/rest2.gif +0 -0
  157. data/website/static/1.13/assets/img/rest3.gif +0 -0
  158. data/website/static/1.13/assets/img/rethink-rest-graffiti.jpg +0 -0
  159. data/website/static/1.13/assets/img/why.png +0 -0
  160. data/website/static/1.13/assets/js/highlight.pack.js +2 -0
  161. data/website/static/1.13/assets/main.css +15518 -0
  162. data/website/static/1.13/assets/main.css.map +1 -0
  163. data/website/static/1.13/bin/bundle +109 -0
  164. data/website/static/1.13/bin/jekyll +27 -0
  165. data/website/static/1.13/bin/kramdown +27 -0
  166. data/website/static/1.13/bin/listen +27 -0
  167. data/website/static/1.13/bin/rake +27 -0
  168. data/website/static/1.13/bin/rougify +27 -0
  169. data/website/static/1.13/bin/safe_yaml +27 -0
  170. data/website/static/1.13/bin/sass +27 -0
  171. data/website/static/1.13/bin/sass-convert +27 -0
  172. data/website/static/1.13/bin/scss +27 -0
  173. data/website/static/1.13/blog.html +259 -0
  174. data/website/static/1.13/cheatsheet.html +316 -0
  175. data/website/static/1.13/cookbooks/authorization.md +0 -0
  176. data/website/static/1.13/cookbooks/caching.md +0 -0
  177. data/website/static/1.13/cookbooks/customizing-sideloads.html +325 -0
  178. data/website/static/1.13/cookbooks/etags.md +0 -0
  179. data/website/static/1.13/cookbooks/hopping-relationships.html +324 -0
  180. data/website/static/1.13/cookbooks/json_attributes.md +0 -0
  181. data/website/static/1.13/cookbooks/openstruct-models.md +0 -0
  182. data/website/static/1.13/cookbooks/remote-resources.md +0 -0
  183. data/website/static/1.13/cookbooks/without-activerecord.html +510 -0
  184. data/website/static/1.13/features.html +249 -0
  185. data/website/static/1.13/feed.xml +106 -0
  186. data/website/static/1.13/guides/concepts/backends-and-models.html +467 -0
  187. data/website/static/1.13/guides/concepts/debugging.html +440 -0
  188. data/website/static/1.13/guides/concepts/endpoints.html +432 -0
  189. data/website/static/1.13/guides/concepts/error-handling.html +396 -0
  190. data/website/static/1.13/guides/concepts/links.html +501 -0
  191. data/website/static/1.13/guides/concepts/remote-resources.html +536 -0
  192. data/website/static/1.13/guides/concepts/resources.html +2176 -0
  193. data/website/static/1.13/guides/concepts/testing.html +1469 -0
  194. data/website/static/1.13/guides/getting-started/installation.html +420 -0
  195. data/website/static/1.13/guides/graphiti-rails-migration.html +242 -0
  196. data/website/static/1.13/guides/index.html +269 -0
  197. data/website/static/1.13/guides/overview.html +325 -0
  198. data/website/static/1.13/guides/upgrading-2-0.html +193 -0
  199. data/website/static/1.13/guides/upgrading.html +314 -0
  200. data/website/static/1.13/guides/vandal.html +282 -0
  201. data/website/static/1.13/guides/why.html +1121 -0
  202. data/website/static/1.13/index.html +72 -0
  203. data/website/static/1.13/js/authentication.html +295 -0
  204. data/website/static/1.13/js/ddau.html +238 -0
  205. data/website/static/1.13/js/extra-params.html +270 -0
  206. data/website/static/1.13/js/index.html +321 -0
  207. data/website/static/1.13/js/installation.html +637 -0
  208. data/website/static/1.13/js/introduction.html +257 -0
  209. data/website/static/1.13/js/middleware.html +318 -0
  210. data/website/static/1.13/js/reads/fieldsets.html +271 -0
  211. data/website/static/1.13/js/reads/filtering.html +289 -0
  212. data/website/static/1.13/js/reads/includes.html +260 -0
  213. data/website/static/1.13/js/reads/index.html +497 -0
  214. data/website/static/1.13/js/reads/nested-queries.html +353 -0
  215. data/website/static/1.13/js/reads/pagination.html +260 -0
  216. data/website/static/1.13/js/reads/sorting.html +265 -0
  217. data/website/static/1.13/js/reads/statistics.html +289 -0
  218. data/website/static/1.13/js/state-syncing.html +340 -0
  219. data/website/static/1.13/js/writes/deferred.html +296 -0
  220. data/website/static/1.13/js/writes/dirty-tracking.html +399 -0
  221. data/website/static/1.13/js/writes/index.html +391 -0
  222. data/website/static/1.13/js/writes/nested.html +330 -0
  223. data/website/static/1.13/js/writes/validations.html +272 -0
  224. data/website/static/1.13/quickstart.html +660 -0
  225. data/website/static/1.13/template +161 -0
  226. data/website/static/1.13/tutorial/index.html +250 -0
  227. data/website/static/1.13/tutorial/step_0.html +292 -0
  228. data/website/static/1.13/tutorial/step_1.html +517 -0
  229. data/website/static/1.13/tutorial/step_2.html +481 -0
  230. data/website/static/1.13/tutorial/step_3.html +323 -0
  231. data/website/static/1.13/tutorial/step_4.html +318 -0
  232. data/website/static/1.13/tutorial/step_5.html +265 -0
  233. data/website/static/1.13/tutorial/step_6.html +276 -0
  234. data/website/static/1.13/tutorial/step_7.html +390 -0
  235. data/website/static/1.13/tutorial/step_8.html +316 -0
  236. data/website/static/1.13/tutorial/step_9.html +365 -0
  237. data/website/static/assets/img/error_payload.png +0 -0
  238. data/website/static/assets/img/legacy/legacy-0378a3bb39.png +0 -0
  239. data/website/static/assets/img/legacy/legacy-05bbd3e5fd.png +0 -0
  240. data/website/static/assets/img/legacy/legacy-07aa104495.png +0 -0
  241. data/website/static/assets/img/legacy/legacy-0c75a16b3a.gif +0 -0
  242. data/website/static/assets/img/legacy/legacy-3076df6209.png +0 -0
  243. data/website/static/assets/img/legacy/legacy-7f6889bc89.png +0 -0
  244. data/website/static/assets/img/legacy/legacy-a2cc4363c3.png +0 -0
  245. data/website/static/assets/img/legacy/legacy-f67cfa89ab.png +0 -0
  246. data/website/static/assets/img/meta_total_count.png +0 -0
  247. data/website/static/img/docusaurus-social-card.jpg +0 -0
  248. data/website/static/img/docusaurus.png +0 -0
  249. data/website/static/img/favicon.ico +0 -0
  250. data/website/static/img/logo.png +0 -0
  251. data/website/static/img/logo.svg +1 -0
  252. data/website/static/img/undraw_docusaurus_mountain.svg +171 -0
  253. data/website/static/img/undraw_docusaurus_react.svg +170 -0
  254. data/website/static/img/undraw_docusaurus_tree.svg +40 -0
  255. metadata +245 -46
  256. data/gemfiles/rails_6.gemfile +0 -18
  257. data/gemfiles/rails_6_graphiti_rails.gemfile +0 -19
  258. data/gemfiles/rails_7.gemfile +0 -18
  259. data/gemfiles/rails_7_1_graphiti_rails.gemfile +0 -19
  260. data/gemfiles/rails_7_graphiti_rails.gemfile +0 -19
  261. data/lib/graphiti/railtie.rb +0 -121
  262. /data/{deprecated_generators → lib/generators}/graphiti/resource_test_generator.rb +0 -0
  263. /data/{deprecated_generators → lib/generators}/graphiti/templates/application_resource.rb.erb +0 -0
  264. /data/{deprecated_generators → lib/generators}/graphiti/templates/controller.rb.erb +0 -0
  265. /data/{deprecated_generators → lib/generators}/graphiti/templates/create_request_spec.rb.erb +0 -0
  266. /data/{deprecated_generators → lib/generators}/graphiti/templates/destroy_request_spec.rb.erb +0 -0
  267. /data/{deprecated_generators → lib/generators}/graphiti/templates/resource.rb.erb +0 -0
  268. /data/{deprecated_generators → lib/generators}/graphiti/templates/resource_writes_spec.rb.erb +0 -0
  269. /data/{deprecated_generators → lib/generators}/graphiti/templates/update_request_spec.rb.erb +0 -0
@@ -0,0 +1,894 @@
1
+ ---
2
+ title: 'Testing'
3
+ ---
4
+
5
+ ## Overview {#overview}
6
+
7
+ Test first.
8
+
9
+ Wait, hear me out!
10
+
11
+ [Even if you're not a fan of TDD](http://david.heinemeierhansson.com/2014/tdd-is-dead-long-live-testing.html), Graphiti *integration* tests are the easiest, most pleasant way to develop. In fact, most Graphiti development can happen without even opening a browser. And as a side effect, you get a reliable test suite.
12
+
13
+ Let's say we want to filter Employees by `title`, which comes from the `positions` table. Start with a spec:
14
+
15
+ ```ruby
16
+ RSpec.describe EmployeeResource, type: :resource do
17
+ describe 'filtering' do
18
+ context 'by title' do
19
+ # GIVEN some seed data
20
+ let!(:employee1) { create(:employee) }
21
+ let!(:employee2) { create(:employee) }
22
+ let!(:position1) do
23
+ create :position,
24
+ title: 'foo',
25
+ employee: employee1
26
+ end
27
+ let!(:position2) do
28
+ create :position,
29
+ title: 'bar',
30
+ employee: employee2
31
+ end
32
+
33
+ # WHEN a parameter is set
34
+ before do
35
+ params[:filter] = { title: 'bar' }
36
+ end
37
+
38
+ # THEN the query results will be correct
39
+ it 'works' do
40
+ expect(records.map(&:id)).to eq([employee2.id])
41
+ end
42
+ end
43
+ end
44
+ end
45
+ ```
46
+
47
+ By developing test-first:
48
+
49
+ * We don't need to struggle with seeding local development data or finding the right records for specific scenarios - we can seed randomized data on-the-fly with [factories](https://github.com/thoughtbot/factory_bot).
50
+ * There's no need to spin up a server and refresh browser pages, mentally parsing the response payload.
51
+ * We get a high-confidence test "for free".
52
+ * Because our integration test is separate from implementation, we don't need to worry about [test-induced design damage](http://david.heinemeierhansson.com/2014/test-induced-design-damage.html).
53
+
54
+ ### API vs Resource {#api-vs-resource}
55
+
56
+ There are two types of Graphiti tests: **API tests** and **Resource tests**.
57
+
58
+ This is because the same Resource logic can be re-used at multiple endpoints. PostResource can be referenced at `/posts`, `/top_posts`, and `/admin/posts`, but we shouldn't have to test the same filtering and sorting logic over and over. Querying, persistence, and serialization are all Resource responsibilities, tested in Resource tests.
59
+
60
+ We still want API tests, though, to test everything outside of the Resource: routing, middleware, cache rules, response codes, etc…
61
+
62
+ Typically, you'll write the API test **once** and not have to touch it again.
63
+
64
+ ### Factories {#factories}
65
+
66
+ > Note: Factories are not **required**, but they are considered a best practice used by the Graphiti test generator. Read thoughtbot's [Why Factories?](https://robots.thoughtbot.com/why-factories) for more information.
67
+
68
+ We need to seed data into our test database. To do this, we use [Factory Bot](https://github.com/thoughtbot/factory_bot) and [Faker](https://github.com/stympy/faker).
69
+
70
+ When you generate a model, a stub factory will be created. It is highly recommended you edit that factory with randomized data:
71
+
72
+ ```ruby
73
+ # BEFORE
74
+ FactoryBot.define do
75
+ factory :employee do
76
+ first_name { 'MyString' }
77
+ end
78
+ end
79
+
80
+ # AFTER
81
+ FactoryBot.define do
82
+ factory :employee do
83
+ first_name { Faker::Name.first_name }
84
+ end
85
+ end
86
+ ```
87
+
88
+ This will help catch edge cases and provide more clarity than seeing the same `"MyString"` everywhere.
89
+
90
+ It's a best practice that if a factory defines an attribute, there should be a corresponding validation around that attribute. If an attribute is optional, it should not be defaulted in a factory.
91
+
92
+ Finally, Rails requires `belongs_to` associations by default. This means that if Employee `belongs_to :department`, then `create(:employee)` will fail. To ensure a relationship is always seeded:
93
+
94
+ ```ruby
95
+ FactoryBot.define do
96
+ factory :employee do
97
+ department
98
+ # OR association :department, factory: :department
99
+ end
100
+ end
101
+
102
+ ```
103
+
104
+ ### RSpec Setup {#rspec}
105
+
106
+ RSpec is not **required**, but considered a first-class citizen used by the Graphiti test generator.
107
+
108
+ Add the following to your Gemfile:
109
+
110
+ ```ruby
111
+ # Gemfile
112
+ group :development, :test do
113
+ gem 'factory_bot_rails'
114
+ gem 'rspec_rails'
115
+ gem 'faker'
116
+ end
117
+
118
+ group :test do
119
+ gem 'database_cleaner'
120
+ end
121
+ ```
122
+
123
+ Bootstrap RSpec if you haven't already:
124
+
125
+ ```bash
126
+ $ bin/rails g rspec:install
127
+ ```
128
+
129
+ Then wire up Graphiti's helpers and reset your database between examples:
130
+
131
+ ```ruby
132
+ require 'graphiti/spec_helpers/rspec'
133
+
134
+ RSpec.configure do |config|
135
+ config.include FactoryBot::Syntax::Methods
136
+ config.include Graphiti::SpecHelpers::RSpec
137
+ config.include Graphiti::SpecHelpers::Sugar
138
+ config.include Graphiti::Rails::TestHelpers, type: :request
139
+
140
+ # Clean your DB between test runs
141
+ config.before(:suite) do
142
+ DatabaseCleaner.strategy = :transaction
143
+ DatabaseCleaner.clean_with(:truncation)
144
+ end
145
+
146
+ config.around(:each) do |example|
147
+ begin
148
+ DatabaseCleaner.cleaning do
149
+ example.run
150
+ end
151
+ ensure
152
+ DatabaseCleaner.clean
153
+ end
154
+ end
155
+ end
156
+ ```
157
+
158
+ ## Test Helpers {#test-helpers}
159
+
160
+ Tests are run using [JSONAPI standards](http://jsonapi.org/format/#fetching-includes). But the JSONAPI payload can be a pain to deal with. So, we've supplied helpers.
161
+
162
+ These helpers ship with Graphiti, under `Graphiti::SpecHelpers`.
163
+
164
+ ### #jsonapi_data {#jsonapi-data}
165
+
166
+ > Note: for brevity, this method is aliased to `d`
167
+
168
+ The `jsonapi_data` method will parse response data and return a normalized object (`Graphiti::SpecHelpers::Node`). Assert against this the same way you assert against JSON:
169
+
170
+ ```ruby
171
+ data = jsonapi_data[0]
172
+ expect(data.id).to eq(employee.id)
173
+ expect(data.jsonapi_type).to eq('employees')
174
+ expect(data.first_name).to eq('Jane')
175
+ ```
176
+
177
+ * `id` will automatically case to an integer. If you would like to avoid this, use `rawid` instead.
178
+ * `jsonapi_type` is a convenience method for `data/type`, to avoid conflicting with an attribute of the same name.
179
+ * If the `first_name` key was not present in the response, an error will be raised.
180
+
181
+ #### Accessing Sideloads {#accessing-sideloads}
182
+
183
+ To grab a relationship:
184
+
185
+ ```ruby
186
+ sideload = d[0].sideload(:comments)
187
+ expect(sideload.id).to eq(123)
188
+ expect(sideload.jsonapi_type).to eq('comments')
189
+ expect(sideload.body).to eq('body')
190
+ ```
191
+
192
+ The `sideload` method accepts the *name of the relationship*. It returns a normal `jsonapi_data` `Graphiti::SpecHelpers::Node` containing the `include`-ed data.
193
+
194
+ #### Accessing Links {#accessing-links}
195
+
196
+ To grab a Link:
197
+
198
+ ```ruby
199
+ d[0].link(:comments, :related)
200
+ ```
201
+
202
+ This accepts the relationship name and the link type. It will return the link URL.
203
+
204
+ ### #json {#json}
205
+
206
+ To see the raw JSON response, use `json`.
207
+
208
+ ### #date and #datetime {#date-and-datetime}
209
+
210
+ In Graphiti, datetimes are rendered in [ISO 8601 format](https://www.iso.org/iso-8601-date-and-time-format.html). This means that straight date comparisons will fail:
211
+
212
+ ```ruby
213
+ # WRONG
214
+ expect(d[0].created_at).to eq(post.created_at)
215
+ ```
216
+
217
+ Instead, use the `datetime` helper to convert to ISO 8601 and compare apples to apples:
218
+
219
+ ```ruby
220
+ # RIGHT
221
+ expect(d[0].created_at).to eq(datetime(post.created_at))
222
+ ```
223
+
224
+ Similarly, there's a `date` helper as well.
225
+
226
+ ### #jsonapi_errors {#jsonapi-errors}
227
+
228
+ > This method is aliased to `errors` for brevity
229
+
230
+ To parse an [Errors Payload](http://jsonapi.org/format/#errors):
231
+
232
+ ```ruby
233
+ errors = jsonapi_errors
234
+
235
+ # Direct access
236
+ expect(errors.length).to eq(1)
237
+ expect(errors[0].attribute).to eq(:name)
238
+ expect(errors[0].status).to eq('422')
239
+ expect(errors[0].title).to eq('Validation Error')
240
+ expect(errors[0].detail).to eq("Name can't be blank")
241
+ expect(errors[0].code).to eq(:blank)
242
+ expect(errors[0].message).to eq("can't be blank")
243
+
244
+ # By attribute
245
+ expect(errors.name.message).to eq("can't be blank")
246
+ expect(errors.name.code).to eq(:blank)
247
+ # ... etc ...
248
+
249
+ # As a hash
250
+ expect(errors.to_h).to eq({
251
+ name: "can't be blank"
252
+ })
253
+ ```
254
+
255
+ ### Resource Test Helpers {#resource-test-helpers}
256
+
257
+ Resource tests have two helpers, both different ways to execute a query.
258
+
259
+ `render` will fire the query and return a JSON response that can be accessed as normal:
260
+
261
+ ```ruby
262
+ it 'works' do
263
+ render
264
+ expect(d[0].first_name).to eq('Jane')
265
+ json # => { data: { type: 'employees', ... } }
266
+ end
267
+ ```
268
+
269
+ `records` will return model instances:
270
+
271
+ ```ruby
272
+ it 'works' do
273
+ render
274
+ expect(records.map(&:id)).to eq([1, 2, 3])
275
+ end
276
+ ```
277
+
278
+ ### API Test Helpers {#api-test-helpers}
279
+
280
+ When executing an API test request, always use the `jsonapi_` doppelgänger:
281
+
282
+ * `jsonapi_get(url, params:)` instead of `get`
283
+ * `jsonapi_post(url, payload)` instead of `post`
284
+ * `jsonapi_put(url, payload)` instead of `put`
285
+ * `jsonapi_patch(url, payload)` instead of `patch`
286
+ * `jsonapi_delete(url)` instead of `delete`
287
+
288
+ This will set the `CONTENT_TYPE` header to `application/vnd.api+json` and call `to_json` on the payload (when applicable).
289
+
290
+ It also allows overriding `jsonapi_headers`. Use this to manipulate headers for a given request:
291
+
292
+ ```ruby
293
+ def jsonapi_headers
294
+ super.tap do |headers|
295
+ headers['CUSTOM'] = 'foo'
296
+ end
297
+ end
298
+ ```
299
+
300
+ ### Guard Helpers {#guard-helpers}
301
+
302
+ Many teams use [guard](https://github.com/guard/guard) in development to watch their project files and run a smaller set of focused tests as code changes. For those teams leveraging guard and the [guard-rspec plugin](https://github.com/guard/guard-rspec), we offer an additional set of DSL helpers via the [guard-rspec-graphiti plugin](https://github.com/graphiti-api/guard-rspec-graphiti). For more details, check out the [project README](https://github.com/graphiti-api/guard-rspec-graphiti/blob/master/README.md).
303
+
304
+ ## Resource Tests {#resource-tests}
305
+
306
+ There are two test files for each Resource:
307
+
308
+ * `spec/resources/post/reads_spec.rb`
309
+ * `spec/resources/post/writes_spec.rb`
310
+
311
+ ### Reads {#reads}
312
+
313
+ The basic setup for read operations:
314
+
315
+ ```ruby
316
+ # spec/resources/employee/reads_spec.rb
317
+ require 'rails_helper'
318
+
319
+ RSpec.describe EmployeeResource, type: :resource do
320
+ describe 'serialization' do
321
+ # ... code ...
322
+ end
323
+
324
+ describe 'filtering' do
325
+ # ... code ...
326
+ end
327
+
328
+ describe 'sorting' do
329
+ # ... code ...
330
+ end
331
+
332
+ describe 'sideloading' do
333
+ # ... code ...
334
+ end
335
+ end
336
+ ```
337
+
338
+ #### Serialization {#serialization}
339
+
340
+ ```ruby
341
+ describe 'serialization' do
342
+ let!(:employee) { create(:employee, first_name: 'Jane') }
343
+
344
+ it 'works' do
345
+ render
346
+ data = jsonapi_data[0]
347
+ expect(data.id).to eq(employee.id)
348
+ expect(data.jsonapi_type).to eq('employees')
349
+ expect(data.first_name).to eq('Jane')
350
+ end
351
+ end
352
+ ```
353
+
354
+ Best practices:
355
+
356
+ * Assert on all attributes, even if there is no logic. This way adding logic will cause a test failure.
357
+ * When seeding data, manually assign values. This way you can be assured you aren't accidentally testing `nil == nil`
358
+
359
+ If you decide you have a high level of confidence in your factories, you can instead save some keystrokes and assert on randomized data:
360
+
361
+ ```ruby
362
+ expect(data.first_name).to eq(employee.first_name)
363
+ ```
364
+
365
+ > Note: Our schema validation test will ensure no attributes get removed or change types.
366
+
367
+ #### Filtering {#filtering}
368
+
369
+ ```ruby
370
+ describe 'filtering' do
371
+ let!(:employee1) { create(:employee) }
372
+ let!(:employee2) { create(:employee) }
373
+
374
+ context 'by id' do
375
+ before do
376
+ params[:filter] = { id: { eq: employee2.id } }
377
+ end
378
+
379
+ it 'works' do
380
+ render
381
+ expect(d.map(&:id)).to eq([employee2.id])
382
+ end
383
+ end
384
+ end
385
+ ```
386
+
387
+ In general, you only need to test filtering when there is custom logic. Our schema validation test will ensure no filters are removed, guarded, changed operators, etc.
388
+
389
+ #### Sorting {#sorting}
390
+
391
+ ```ruby
392
+ describe 'sorting' do
393
+ describe 'by id' do
394
+ let!(:employee1) { create(:employee) }
395
+ let!(:employee2) { create(:employee) }
396
+
397
+ context 'when ascending' do
398
+ before do
399
+ params[:sort] = 'id'
400
+ end
401
+
402
+ it 'works' do
403
+ render
404
+ expect(d.map(&:id)).to eq([
405
+ employee1.id,
406
+ employee2.id
407
+ ])
408
+ end
409
+ end
410
+
411
+ context 'when descending' do
412
+ before do
413
+ params[:sort] = '-id'
414
+ end
415
+
416
+ it 'works' do
417
+ render
418
+ expect(d.map(&:id)).to eq([
419
+ employee2.id,
420
+ employee1.id
421
+ ])
422
+ end
423
+ end
424
+ end
425
+ end
426
+ ```
427
+
428
+ In general, you only need to test sorting when there is custom logic. Our schema validation test will ensure no sorts are removed, guarded or limited in direction.
429
+
430
+ #### Sideloading {#sideloading}
431
+
432
+ ```ruby
433
+ describe 'sideloading' do
434
+ let!(:employee) { create(:employee) }
435
+
436
+ describe 'current_position' do
437
+ let!(:pos1) do
438
+ create(:position, employee: employee, historical_index: 2)
439
+ end
440
+ let!(:pos2) do
441
+ create(:position, employee: employee, historical_index: 1)
442
+ end
443
+
444
+ before do
445
+ params[:include] = 'current_position'
446
+ end
447
+
448
+ it 'returns position with historical index == 1' do
449
+ render
450
+ sl = d[0].sideload(:current_position)
451
+ expect(sl.jsonapi_type).to eq('positions')
452
+ expect(sl.id).to eq(pos2.id)
453
+ end
454
+ end
455
+ end
456
+ ```
457
+
458
+ There is no need to test each attribute of the sideload - this should be tested in the [Resource Test](#resource-tests) of the sideloaded Resource.
459
+
460
+ In general, you only need to test sideloads when there is custom logic. Our schema validation test will ensure no sideloads are removed or associated to a different Resource.
461
+
462
+ ### Writes {#writes}
463
+
464
+ The basic setup for write operations:
465
+
466
+ ```ruby
467
+ # spec/resources/employee/writes_spec.rb
468
+ require 'rails_helper'
469
+
470
+ RSpec.describe EmployeeResource, type: :resource do
471
+ describe 'creating' do
472
+ let(:payload) { ... }
473
+ # ... code ...
474
+ end
475
+
476
+ describe 'creating' do
477
+ let(:payload) { ... }
478
+ # ... code ...
479
+ end
480
+
481
+ describe 'destroying' do
482
+ # ... code ...
483
+ end
484
+ end
485
+ ```
486
+
487
+ Here `payload` is a [JSONAPI Resource Object](http://jsonapi.org/format/#crud).
488
+
489
+ #### Create {#create}
490
+
491
+ ```ruby
492
+ describe 'creating' do
493
+ let(:payload) do
494
+ {
495
+ data: {
496
+ type: 'employees',
497
+ attributes: { }
498
+ }
499
+ }
500
+ end)
501
+
502
+ let(:instance) do
503
+ EmployeeResource.build(payload)
504
+ end
505
+
506
+ it 'works' do
507
+ expect {
508
+ expect(instance.save).to eq(true)
509
+ }.to change { Employee.count }.by(1)
510
+ end
511
+ end
512
+ ```
513
+
514
+ `payload` starts as an empty Employee [Resource Object](http://jsonapi.org/format/#crud), asserting only that saving it creates an Employee. You'll likely want to add attributes here and ensure they are persisted correctly:
515
+
516
+ ```ruby
517
+ let(:payload) do
518
+ {
519
+ data: {
520
+ type: 'employees',
521
+ attributes: { first_name: 'Jane', age: 30 }
522
+ }
523
+ }
524
+ end
525
+
526
+ # ... code ...
527
+
528
+ it 'works' do
529
+ expect {
530
+ expect(instance.save).to eq(true)
531
+ }.to change { Employee.count }.by(1)
532
+ employee = Employee.last
533
+ expect(employee.first_name).to eq('Jane')
534
+ expect(employee.age).to eq(30)
535
+ end
536
+ ```
537
+
538
+ ##### Required Belongs To {#required-belongs-to}
539
+
540
+ Rails requires `belongs_to` associations by default. This means that if Employee `belongs_to :department`, the above tests will fail (we cannot create the Employee without associating it to Department).
541
+
542
+ You have 3 options here:
543
+
544
+ * Turn off this validation in test mode. Add `config.active_record.belongs_to_required_by_default = false` to `config/environments/test.rb`.
545
+ * Turn off the validation for this specific relationship: `belongs_to :department, optional: true`.
546
+ * Associate as part of the request.
547
+
548
+ We recommend the third option to preserve real-world end-to-end behavior:
549
+
550
+ ```ruby
551
+ describe 'creating' do
552
+ let!(:department) { create(:department) }
553
+
554
+ let(:payload) do
555
+ {
556
+ type: 'employees',
557
+ attributes: { ... },
558
+ relationships: {
559
+ department: {
560
+ data: {
561
+ type: 'departments',
562
+ id: department.id.to_s
563
+ }
564
+ }
565
+ }
566
+ }
567
+ end
568
+
569
+ # ... code ...
570
+ end
571
+ ```
572
+
573
+ This ensures the Employee is created and associated to the given department.
574
+
575
+ #### Update {#update}
576
+
577
+ An update spec looks like the create spec, but finds an existing record instead of building a new one, and asserts the changed attribute rather than a changed count:
578
+
579
+ ```ruby
580
+ describe 'updating' do
581
+ let!(:employee) { create(:employee) }
582
+
583
+ let(:payload) do
584
+ {
585
+ data: {
586
+ id: employee.id.to_s,
587
+ type: 'employees',
588
+ attributes: { first_name: 'changed!' }
589
+ }
590
+ }
591
+ end
592
+
593
+ let(:instance) do
594
+ EmployeeResource.find(payload)
595
+ end
596
+
597
+ it 'works' do
598
+ expect {
599
+ expect(instance.update_attributes).to eq(true)
600
+ }.to change { employee.reload.updated_at }
601
+ .and change { employee.first_name }.to('changed!')
602
+ end
603
+ end
604
+ ```
605
+
606
+ > Note that this test will be pending by default when using the generator, as we require the attributes to be explicitly defined.
607
+
608
+ #### Destroy {#destroy}
609
+
610
+ Destroy specs drop the payload/instance-building entirely and just find and destroy the record, asserting the count decreases:
611
+
612
+ ```ruby
613
+ describe 'destroying' do
614
+ let!(:employee) { create(:employee) }
615
+
616
+ let(:instance) do
617
+ EmployeeResource.find(id: employee.id)
618
+ end
619
+
620
+ it 'works' do
621
+ expect {
622
+ expect(instance.destroy).to eq(true)
623
+ }.to change { Employee.count }.by(-1)
624
+ end
625
+ end
626
+ ```
627
+
628
+ #### Side Effects {#side-effects}
629
+
630
+ ```ruby
631
+ it 'works' do
632
+ # some assertion
633
+ email = ActionMailer::Base.deliveries.last
634
+ expect(email.subject).to eq('Welcome!')
635
+ end
636
+ ```
637
+
638
+ It's common for write operations to cause side-effects, such as sending an email or updating an audit trail. It's recommended to test these *within the same "it" block* unless the logic gets particularly intense. Though "one expectation per test" works well for unit tests, integration tests can take longer to run and the performance penalty isn't worth it.
639
+
640
+ ## API Tests {#api-tests}
641
+
642
+ There are five test files for each Resource:
643
+
644
+ * `spec/api/v1/employees/index_spec.rb`
645
+ * `spec/api/v1/employees/show_spec.rb`
646
+ * `spec/api/v1/employees/create_spec.rb`
647
+ * `spec/api/v1/employees/update_spec.rb`
648
+ * `spec/api/v1/employees/destroy_spec.rb`
649
+
650
+ ### Reads {#api-reads}
651
+
652
+ #### #index {#index}
653
+
654
+ ```ruby
655
+ require 'rails_helper'
656
+
657
+ RSpec.describe "employees#index", type: :request do
658
+ let(:params) { {} }
659
+
660
+ subject(:make_request) do
661
+ jsonapi_get "/api/v1/employees", params: params
662
+ end
663
+
664
+ describe 'basic fetch' do
665
+ let!(:employee1) { create(:employee) }
666
+ let!(:employee2) { create(:employee) }
667
+
668
+ it 'works' do
669
+ expect(EmployeeResource).to receive(:all).and_call_original
670
+ make_request
671
+ expect(response.status).to eq(200)
672
+ expect(d.map(&:jsonapi_type).uniq)
673
+ .to match_array(['employees'])
674
+ expect(d.map(&:id))
675
+ .to match_array([employee1.id, employee2.id])
676
+ end
677
+ end
678
+ end
679
+ ```
680
+
681
+ #### #show {#show}
682
+
683
+ Same shape as `#index`, but requests a single Employee by id and asserts against the singular `d` node instead of an array:
684
+
685
+ ```ruby
686
+ subject(:make_request) do
687
+ jsonapi_get "/api/v1/employees/#{employee.id}", params: params
688
+ end
689
+
690
+ describe 'basic fetch' do
691
+ let!(:employee) { create(:employee) }
692
+
693
+ it 'works' do
694
+ expect(EmployeeResource).to receive(:find).and_call_original
695
+ make_request
696
+ expect(response.status).to eq(200)
697
+ expect(d.jsonapi_type).to eq('employees')
698
+ expect(d.id).to eq(employee.id)
699
+ end
700
+ end
701
+ ```
702
+
703
+ ### Writes {#api-writes}
704
+
705
+ #### #create {#api-create}
706
+
707
+ ```ruby
708
+ require 'rails_helper'
709
+
710
+ RSpec.describe "employees#create", type: :request do
711
+ subject(:make_request) do
712
+ jsonapi_post "/api/v1/employees", payload
713
+ end
714
+
715
+ describe 'basic create' do
716
+ let(:payload) do
717
+ {
718
+ data: {
719
+ type: 'employees',
720
+ attributes: {
721
+ first_name: 'Jane'
722
+ }
723
+ }
724
+ }
725
+ end
726
+
727
+ it 'works' do
728
+ expect(EmployeeResource).to receive(:build).and_call_original
729
+ expect {
730
+ make_request
731
+ }.to change { Employee.count }.by(1)
732
+ expect(response.status).to eq(201)
733
+ end
734
+ end
735
+ end
736
+ ```
737
+
738
+ You probably only want to add attributes required to pass validation, here. We don't assert on attributes of the created record (save this for your Resource test). One easy way to do this is to pass randomized data from your factory:
739
+
740
+ ```ruby
741
+ let(:payload) do
742
+ {
743
+ data: {
744
+ type: 'employees',
745
+ attributes: attributes_for(:employee)
746
+ }
747
+ }
748
+ end
749
+ ```
750
+
751
+ See also: [Dealing with required belongs_to relationships](#required-belongs-to).
752
+
753
+ #### #update {#api-update}
754
+
755
+ Same as `#create`, but the payload finds an existing employee by `id` and the assertion checks that the record's attributes changed, rather than the count:
756
+
757
+ ```ruby
758
+ subject(:make_request) do
759
+ jsonapi_put "/api/v1/employees/#{employee.id}", payload
760
+ end
761
+
762
+ describe 'basic update' do
763
+ let!(:employee) { create(:employee) }
764
+
765
+ let(:payload) do
766
+ {
767
+ data: {
768
+ id: employee.id.to_s,
769
+ type: 'employees',
770
+ attributes: {
771
+ first_name: 'changed!'
772
+ }
773
+ }
774
+ }
775
+ end
776
+
777
+ it 'updates the resource' do
778
+ expect(EmployeeResource).to receive(:find).and_call_original
779
+ expect {
780
+ make_request
781
+ }.to change { employee.reload.attributes }
782
+ expect(response.status).to eq(200)
783
+ end
784
+ end
785
+ ```
786
+
787
+ We don't assert on specific attributes here - save that for your Resource test. Just like `#create`, you may want to use FactoryBot to generate randomized attributes:
788
+
789
+ ```ruby
790
+ let(:payload) do
791
+ {
792
+ data: {
793
+ id: employee.id.to_s,
794
+ type: 'employees',
795
+ attributes: attributes_for(:employee)
796
+ }
797
+ }
798
+ end
799
+ ```
800
+
801
+ #### #destroy {#api-destroy}
802
+
803
+ ```ruby
804
+ subject(:make_request) do
805
+ jsonapi_delete "/api/v1/employees/#{employee.id}"
806
+ end
807
+
808
+ describe 'basic destroy' do
809
+ let!(:employee) { create(:employee) }
810
+
811
+ it 'updates the resource' do
812
+ expect(EmployeeResource).to receive(:find).and_call_original
813
+ expect { make_request }.to change { Employee.count }.by(-1)
814
+ expect { employee.reload }
815
+ .to raise_error(ActiveRecord::RecordNotFound)
816
+ expect(response.status).to eq(200)
817
+ expect(json).to eq('meta' => {})
818
+ end
819
+ end
820
+ ```
821
+
822
+ The response body is asserted to match the [JSONAPI specification for delete responses](http://jsonapi.org/format/#crud-deleting-responses-200): a 200 status with an empty `meta` object.
823
+
824
+ ## Context {#context}
825
+
826
+ Occasionally you'll need to set context for tests. The most common scenario is authorization:
827
+
828
+ ```ruby
829
+ attribute :salary, :integer, readable: :admin?
830
+
831
+ def admin?
832
+ context.current_user.admin?
833
+ end
834
+ ```
835
+
836
+ When using Rails, `context` is the controller associated to the request. We can manually set context in tests:
837
+
838
+ ```ruby
839
+ let(:user) { double(admin?: true) }
840
+ let(:ctx) { double(current_user: user) }
841
+
842
+ it 'works' do
843
+ Graphiti.with_context ctx do
844
+ render
845
+ end
846
+ expect(d[0].salary).to eq(100_000)
847
+ end
848
+ ```
849
+
850
+ ## Schema Validation {#schema-validation}
851
+
852
+ Graphiti comes with built-in backwards-compatibility tests. We do this by comparing the current version of the schema with one previously checked-in.
853
+
854
+ These tests are added at the bottom of `spec/rails_helper.rb`:
855
+
856
+ ```ruby
857
+ Graphiti::SpecHelpers::RSpec.schema!
858
+ ```
859
+
860
+ Whenever you run tests, the schema check will *also* run. If we find any backwards-incompatibilities - attributes removed, types changed, default sort direction modified, etc - the schema test will fail with an output detailing all incompatibilities.
861
+
862
+ When the schema test succeeds, it will overwrite the existing schema file with the new schema. It will not do this on failure.
863
+
864
+ There are times when you want to accept an incompatibility and move on anyway. In this case, use `FORCE_SCHEMA`:
865
+
866
+ ```bash
867
+ $ FORCE_SCHEMA=true bin/rspec
868
+ ```
869
+
870
+ ## Generators {#generators}
871
+
872
+ The [Resource generator](/concepts/resources#generators) will create both Resource and API tests for you. Use these as templates to implement your tests.
873
+
874
+ You can also run
875
+
876
+ ```bash
877
+ $ rails generate graphiti:api_test RESOURCE [options]
878
+ ```
879
+
880
+ For example
881
+
882
+ ```bash
883
+ $ rails generate graphiti:api_test EmployeeResource -a index show
884
+ ```
885
+
886
+ To generate only the API tests. This can be particularly helpful because API tests are mostly boilerplate that does not need to be manually edited. Pass the `-a` option to limit RESTful actions.
887
+
888
+ ## Testing Spectrum {#testing-spectrum}
889
+
890
+ There's no single right level of test coverage. Teams vary. Our guides favor treating logicless configuration (filters, sorts, sideloads) as covered by Graphiti itself and by schema validation, adding Resource/API tests mainly where there's custom logic - but consider heavier coverage if you're doing a major upgrade or swapping datastores.
891
+
892
+ ## Double-Testing Units {#double-testing-units}
893
+
894
+ A custom filter backed by an ActiveRecord scope can feel like it needs both a model unit test and a near-identical Resource integration test. Use [RSpec shared_context](https://relishapp.com/rspec/rspec-core/docs/example-groups/shared-context) to share the seed data between them, or, if the overhead isn't worth it, mark the scope `# @api private` and skip the unit test until the scope needs to be reused elsewhere.