graphiti 2.0.0.beta.7 → 2.0.0.beta.8

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 (238) hide show
  1. checksums.yaml +4 -4
  2. data/.git-blame-ignore-revs +2 -0
  3. data/.github/workflows/ci.yml +55 -3
  4. data/.github/workflows/docs.yml +1 -1
  5. data/.github/workflows/notify-example-app.yml +18 -0
  6. data/.gitignore +2 -0
  7. data/.standard.yml +6 -0
  8. data/CHANGELOG.md +45 -0
  9. data/CONTRIBUTING.md +3 -1
  10. data/README.md +13 -206
  11. data/Rakefile +6 -3
  12. data/UPGRADING.md +1 -1
  13. data/graphiti.gemspec +8 -4
  14. data/lib/generators/graphiti/generator_mixin.rb +1 -1
  15. data/lib/generators/graphiti/install_generator.rb +1 -1
  16. data/lib/generators/graphiti/resource_generator.rb +6 -3
  17. data/lib/generators/graphiti/templates/application_resource.rb.erb +19 -0
  18. data/lib/generators/graphiti/templates/controller.rb.erb +26 -8
  19. data/lib/graphiti/audit/report.rb +6 -6
  20. data/lib/graphiti/audit.rb +3 -4
  21. data/lib/graphiti/delegates/pagination.rb +2 -2
  22. data/lib/graphiti/deserializer.rb +3 -3
  23. data/lib/graphiti/errors.rb +6 -6
  24. data/lib/graphiti/extensions/temp_id.rb +1 -1
  25. data/lib/graphiti/query.rb +3 -3
  26. data/lib/graphiti/rails/context.rb +2 -1
  27. data/lib/graphiti/rails/controller.rb +3 -0
  28. data/lib/graphiti/rails/debugging.rb +1 -1
  29. data/lib/graphiti/rails/responders.rb +2 -1
  30. data/lib/graphiti/resource/configuration.rb +1 -0
  31. data/lib/graphiti/resource/dsl.rb +2 -2
  32. data/lib/graphiti/resource/links.rb +1 -1
  33. data/lib/graphiti/resource/persistence.rb +1 -1
  34. data/lib/graphiti/resource/polymorphism.rb +1 -1
  35. data/lib/graphiti/resource.rb +2 -2
  36. data/lib/graphiti/resource_proxy.rb +0 -1
  37. data/lib/graphiti/scoping/filter.rb +5 -3
  38. data/lib/graphiti/scoping/paginate.rb +1 -1
  39. data/lib/graphiti/scoping/sort.rb +1 -1
  40. data/lib/graphiti/sideload/has_many.rb +1 -1
  41. data/lib/graphiti/spec_helpers/errors_proxy.rb +1 -1
  42. data/lib/graphiti/spec_helpers/matchers.rb +1 -1
  43. data/lib/graphiti/types.rb +5 -1
  44. data/lib/graphiti/util/cache_debug.rb +3 -3
  45. data/lib/graphiti/util/hash.rb +1 -1
  46. data/lib/graphiti/util/persistence.rb +6 -6
  47. data/lib/graphiti/util/serializer_relationships.rb +1 -1
  48. data/lib/graphiti/version.rb +1 -1
  49. data/lib/tasks/graphiti.rake +1 -1
  50. metadata +31 -197
  51. data/docs/concepts/backends-and-models.md +0 -122
  52. data/docs/concepts/endpoints.md +0 -183
  53. data/docs/concepts/links.md +0 -212
  54. data/docs/concepts/overview.md +0 -80
  55. data/docs/concepts/persisting.md +0 -376
  56. data/docs/concepts/relationships.md +0 -627
  57. data/docs/concepts/resources.md +0 -677
  58. data/docs/getting-started/first-api.md +0 -289
  59. data/docs/getting-started/installation.md +0 -185
  60. data/docs/intro.md +0 -307
  61. data/docs/js/authentication.md +0 -63
  62. data/docs/js/ddau.md +0 -20
  63. data/docs/js/extra-params.md +0 -41
  64. data/docs/js/index.md +0 -112
  65. data/docs/js/installation.md +0 -120
  66. data/docs/js/middleware.md +0 -72
  67. data/docs/js/models.md +0 -202
  68. data/docs/js/reads.md +0 -494
  69. data/docs/js/state-syncing.md +0 -100
  70. data/docs/js/writes.md +0 -373
  71. data/docs/reference/vandal.md +0 -63
  72. data/docs/reference/why.md +0 -13
  73. data/docs/topics/authorization.md +0 -155
  74. data/docs/topics/caching.md +0 -55
  75. data/docs/topics/customizing-sideloads.md +0 -156
  76. data/docs/topics/debugging.md +0 -242
  77. data/docs/topics/error-handling.md +0 -210
  78. data/docs/topics/etags.md +0 -46
  79. data/docs/topics/hopping-relationships.md +0 -149
  80. data/docs/topics/json-attributes.md +0 -77
  81. data/docs/topics/openstruct-models.md +0 -50
  82. data/docs/topics/remote-resources.md +0 -291
  83. data/docs/topics/testing.md +0 -916
  84. data/docs/topics/without-activerecord.md +0 -324
  85. data/docs/tutorial/index.md +0 -58
  86. data/docs/tutorial/step_0.md +0 -107
  87. data/docs/tutorial/step_1.md +0 -199
  88. data/docs/tutorial/step_2.md +0 -312
  89. data/docs/tutorial/step_3.md +0 -142
  90. data/docs/tutorial/step_4.md +0 -135
  91. data/docs/tutorial/step_5.md +0 -69
  92. data/docs/tutorial/step_6.md +0 -82
  93. data/docs/tutorial/step_7.md +0 -205
  94. data/docs/tutorial/step_8.md +0 -128
  95. data/docs/tutorial/step_9.md +0 -171
  96. data/docs/upgrading.md +0 -267
  97. data/website/.gitignore +0 -20
  98. data/website/README.md +0 -43
  99. data/website/docusaurus.config.js +0 -197
  100. data/website/package-lock.json +0 -19474
  101. data/website/package.json +0 -46
  102. data/website/sidebars.js +0 -82
  103. data/website/src/css/custom.css +0 -85
  104. data/website/src/pages/markdown-page.mdx +0 -7
  105. data/website/static/.nojekyll +0 -0
  106. data/website/static/1.13/2019/03/31/graphiti-1-0.html +0 -205
  107. data/website/static/1.13/2019/05/08/graphiti-1-1.html +0 -212
  108. data/website/static/1.13/2019/05/20/graphiti-1-2.html +0 -214
  109. data/website/static/1.13/2019/10/14/tutorial.html +0 -198
  110. data/website/static/1.13/CNAME +0 -1
  111. data/website/static/1.13/README.md +0 -16
  112. data/website/static/1.13/assets/css/syntax.css +0 -60
  113. data/website/static/1.13/assets/favicons/android-chrome-192x192.png +0 -0
  114. data/website/static/1.13/assets/favicons/android-chrome-256x256.png +0 -0
  115. data/website/static/1.13/assets/favicons/apple-touch-icon.png +0 -0
  116. data/website/static/1.13/assets/favicons/browserconfig.xml +0 -9
  117. data/website/static/1.13/assets/favicons/favicon-16x16.png +0 -0
  118. data/website/static/1.13/assets/favicons/favicon-32x32.png +0 -0
  119. data/website/static/1.13/assets/favicons/favicon.ico +0 -0
  120. data/website/static/1.13/assets/favicons/mstile-150x150.png +0 -0
  121. data/website/static/1.13/assets/favicons/safari-pinned-tab.svg +0 -1
  122. data/website/static/1.13/assets/favicons/site.webmanifest +0 -19
  123. data/website/static/1.13/assets/img/backend.gif +0 -0
  124. data/website/static/1.13/assets/img/conformity.png +0 -0
  125. data/website/static/1.13/assets/img/error_payload.png +0 -0
  126. data/website/static/1.13/assets/img/fancy-cushion.png +0 -0
  127. data/website/static/1.13/assets/img/gh.png +0 -0
  128. data/website/static/1.13/assets/img/home-bg.jpg +0 -0
  129. data/website/static/1.13/assets/img/lifecycle.gif +0 -0
  130. data/website/static/1.13/assets/img/logo-500.png +0 -0
  131. data/website/static/1.13/assets/img/logo.png +0 -0
  132. data/website/static/1.13/assets/img/love-graffiti.jpg +0 -0
  133. data/website/static/1.13/assets/img/meta_total_count.png +0 -0
  134. data/website/static/1.13/assets/img/persist.jpg +0 -0
  135. data/website/static/1.13/assets/img/resource.gif +0 -0
  136. data/website/static/1.13/assets/img/rest-graffiti.jpg +0 -0
  137. data/website/static/1.13/assets/img/rest1.gif +0 -0
  138. data/website/static/1.13/assets/img/rest2.gif +0 -0
  139. data/website/static/1.13/assets/img/rest3.gif +0 -0
  140. data/website/static/1.13/assets/img/rethink-rest-graffiti.jpg +0 -0
  141. data/website/static/1.13/assets/img/sunrise.png +0 -0
  142. data/website/static/1.13/assets/img/why.png +0 -0
  143. data/website/static/1.13/assets/js/highlight.pack.js +0 -2
  144. data/website/static/1.13/assets/main.css +0 -15518
  145. data/website/static/1.13/assets/main.css.map +0 -1
  146. data/website/static/1.13/bin/bundle +0 -109
  147. data/website/static/1.13/bin/jekyll +0 -27
  148. data/website/static/1.13/bin/kramdown +0 -27
  149. data/website/static/1.13/bin/listen +0 -27
  150. data/website/static/1.13/bin/rake +0 -27
  151. data/website/static/1.13/bin/rougify +0 -27
  152. data/website/static/1.13/bin/safe_yaml +0 -27
  153. data/website/static/1.13/bin/sass +0 -27
  154. data/website/static/1.13/bin/sass-convert +0 -27
  155. data/website/static/1.13/bin/scss +0 -27
  156. data/website/static/1.13/blog.html +0 -259
  157. data/website/static/1.13/cheatsheet.html +0 -316
  158. data/website/static/1.13/cookbooks/authorization.md +0 -0
  159. data/website/static/1.13/cookbooks/caching.md +0 -0
  160. data/website/static/1.13/cookbooks/customizing-sideloads.html +0 -325
  161. data/website/static/1.13/cookbooks/etags.md +0 -0
  162. data/website/static/1.13/cookbooks/hopping-relationships.html +0 -324
  163. data/website/static/1.13/cookbooks/json_attributes.md +0 -0
  164. data/website/static/1.13/cookbooks/openstruct-models.md +0 -0
  165. data/website/static/1.13/cookbooks/remote-resources.md +0 -0
  166. data/website/static/1.13/cookbooks/without-activerecord.html +0 -510
  167. data/website/static/1.13/features.html +0 -249
  168. data/website/static/1.13/feed.xml +0 -106
  169. data/website/static/1.13/guides/concepts/backends-and-models.html +0 -467
  170. data/website/static/1.13/guides/concepts/debugging.html +0 -440
  171. data/website/static/1.13/guides/concepts/endpoints.html +0 -432
  172. data/website/static/1.13/guides/concepts/error-handling.html +0 -396
  173. data/website/static/1.13/guides/concepts/links.html +0 -501
  174. data/website/static/1.13/guides/concepts/remote-resources.html +0 -536
  175. data/website/static/1.13/guides/concepts/resources.html +0 -2176
  176. data/website/static/1.13/guides/concepts/testing.html +0 -1469
  177. data/website/static/1.13/guides/getting-started/installation.html +0 -420
  178. data/website/static/1.13/guides/graphiti-rails-migration.html +0 -242
  179. data/website/static/1.13/guides/index.html +0 -269
  180. data/website/static/1.13/guides/overview.html +0 -325
  181. data/website/static/1.13/guides/upgrading-2-0.html +0 -193
  182. data/website/static/1.13/guides/upgrading.html +0 -314
  183. data/website/static/1.13/guides/vandal.html +0 -282
  184. data/website/static/1.13/guides/why.html +0 -1121
  185. data/website/static/1.13/index.html +0 -72
  186. data/website/static/1.13/js/authentication.html +0 -295
  187. data/website/static/1.13/js/ddau.html +0 -238
  188. data/website/static/1.13/js/extra-params.html +0 -270
  189. data/website/static/1.13/js/index.html +0 -321
  190. data/website/static/1.13/js/installation.html +0 -637
  191. data/website/static/1.13/js/introduction.html +0 -257
  192. data/website/static/1.13/js/middleware.html +0 -318
  193. data/website/static/1.13/js/reads/fieldsets.html +0 -271
  194. data/website/static/1.13/js/reads/filtering.html +0 -289
  195. data/website/static/1.13/js/reads/includes.html +0 -260
  196. data/website/static/1.13/js/reads/index.html +0 -497
  197. data/website/static/1.13/js/reads/nested-queries.html +0 -353
  198. data/website/static/1.13/js/reads/pagination.html +0 -260
  199. data/website/static/1.13/js/reads/sorting.html +0 -265
  200. data/website/static/1.13/js/reads/statistics.html +0 -289
  201. data/website/static/1.13/js/state-syncing.html +0 -340
  202. data/website/static/1.13/js/writes/deferred.html +0 -296
  203. data/website/static/1.13/js/writes/dirty-tracking.html +0 -399
  204. data/website/static/1.13/js/writes/index.html +0 -391
  205. data/website/static/1.13/js/writes/nested.html +0 -330
  206. data/website/static/1.13/js/writes/validations.html +0 -272
  207. data/website/static/1.13/quickstart.html +0 -660
  208. data/website/static/1.13/template +0 -161
  209. data/website/static/1.13/tutorial/index.html +0 -250
  210. data/website/static/1.13/tutorial/step_0.html +0 -292
  211. data/website/static/1.13/tutorial/step_1.html +0 -517
  212. data/website/static/1.13/tutorial/step_2.html +0 -481
  213. data/website/static/1.13/tutorial/step_3.html +0 -323
  214. data/website/static/1.13/tutorial/step_4.html +0 -318
  215. data/website/static/1.13/tutorial/step_5.html +0 -265
  216. data/website/static/1.13/tutorial/step_6.html +0 -276
  217. data/website/static/1.13/tutorial/step_7.html +0 -390
  218. data/website/static/1.13/tutorial/step_8.html +0 -316
  219. data/website/static/1.13/tutorial/step_9.html +0 -365
  220. data/website/static/CNAME +0 -1
  221. data/website/static/assets/img/error_payload.png +0 -0
  222. data/website/static/assets/img/legacy/legacy-0378a3bb39.png +0 -0
  223. data/website/static/assets/img/legacy/legacy-05bbd3e5fd.png +0 -0
  224. data/website/static/assets/img/legacy/legacy-07aa104495.png +0 -0
  225. data/website/static/assets/img/legacy/legacy-0c75a16b3a.gif +0 -0
  226. data/website/static/assets/img/legacy/legacy-3076df6209.png +0 -0
  227. data/website/static/assets/img/legacy/legacy-7f6889bc89.png +0 -0
  228. data/website/static/assets/img/legacy/legacy-a2cc4363c3.png +0 -0
  229. data/website/static/assets/img/legacy/legacy-f67cfa89ab.png +0 -0
  230. data/website/static/assets/img/meta_total_count.png +0 -0
  231. data/website/static/img/docusaurus-social-card.jpg +0 -0
  232. data/website/static/img/docusaurus.png +0 -0
  233. data/website/static/img/favicon.ico +0 -0
  234. data/website/static/img/logo.png +0 -0
  235. data/website/static/img/logo.svg +0 -1
  236. data/website/static/img/undraw_docusaurus_mountain.svg +0 -171
  237. data/website/static/img/undraw_docusaurus_react.svg +0 -170
  238. data/website/static/img/undraw_docusaurus_tree.svg +0 -40
@@ -1,916 +0,0 @@
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::Rails::TestHelpers, type: :request
138
-
139
- # Clean your DB between test runs
140
- config.before(:suite) do
141
- DatabaseCleaner.strategy = :transaction
142
- DatabaseCleaner.clean_with(:truncation)
143
- end
144
-
145
- config.around(:each) do |example|
146
- begin
147
- DatabaseCleaner.cleaning do
148
- example.run
149
- end
150
- ensure
151
- DatabaseCleaner.clean
152
- end
153
- end
154
- end
155
- ```
156
-
157
- ## Test Helpers {#test-helpers}
158
-
159
- 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.
160
-
161
- These helpers ship with Graphiti, under `Graphiti::SpecHelpers`.
162
-
163
- ### #jsonapi_data {#jsonapi-data}
164
-
165
- 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:
166
-
167
- ```ruby
168
- data = jsonapi_data[0]
169
- expect(data.id).to eq(employee.id)
170
- expect(data.jsonapi_type).to eq('employees')
171
- expect(data.first_name).to eq('Jane')
172
- ```
173
-
174
- * `id` will automatically case to an integer. If you would like to avoid this, use `rawid` instead.
175
- * `jsonapi_type` is a convenience method for `data/type`, to avoid conflicting with an attribute of the same name.
176
- * If the `first_name` key was not present in the response, an error will be raised.
177
-
178
- #### Accessing Sideloads {#accessing-sideloads}
179
-
180
- To grab a relationship:
181
-
182
- ```ruby
183
- sideload = jsonapi_data[0].sideload(:comments)
184
- expect(sideload.id).to eq(123)
185
- expect(sideload.jsonapi_type).to eq('comments')
186
- expect(sideload.body).to eq('body')
187
- ```
188
-
189
- The `sideload` method accepts the *name of the relationship*. It returns a normal `jsonapi_data` `Graphiti::SpecHelpers::Node` containing the `include`-ed data.
190
-
191
- #### Accessing Links {#accessing-links}
192
-
193
- To grab a Link:
194
-
195
- ```ruby
196
- jsonapi_data[0].link(:comments, :related)
197
- ```
198
-
199
- This accepts the relationship name and the link type. It will return the link URL.
200
-
201
- ### #json {#json}
202
-
203
- To see the raw JSON response, use `json`.
204
-
205
- ### #json_date and #json_datetime {#date-and-datetime}
206
-
207
- 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:
208
-
209
- ```ruby
210
- # WRONG
211
- expect(jsonapi_data[0].created_at).to eq(post.created_at)
212
- ```
213
-
214
- Instead, use the `json_datetime` helper to convert to ISO 8601 and compare apples to apples:
215
-
216
- ```ruby
217
- # RIGHT
218
- expect(jsonapi_data[0].created_at).to eq(json_datetime(post.created_at))
219
- ```
220
-
221
- Similarly, there's a `json_date` helper as well.
222
-
223
- ### #jsonapi_errors {#jsonapi-errors}
224
-
225
- To parse an [Errors Payload](http://jsonapi.org/format/#errors):
226
-
227
- ```ruby
228
- errors = jsonapi_errors
229
-
230
- # Direct access
231
- expect(errors.length).to eq(1)
232
- expect(errors[0].attribute).to eq(:name)
233
- expect(errors[0].status).to eq('422')
234
- expect(errors[0].title).to eq('Validation Error')
235
- expect(errors[0].detail).to eq("Name can't be blank")
236
- expect(errors[0].code).to eq(:blank)
237
- expect(errors[0].message).to eq("can't be blank")
238
-
239
- # By attribute
240
- expect(errors.name.message).to eq("can't be blank")
241
- expect(errors.name.code).to eq(:blank)
242
- # ... etc ...
243
-
244
- # As a hash
245
- expect(errors.to_h).to eq({
246
- name: "can't be blank"
247
- })
248
- ```
249
-
250
- ### Resource Test Helpers {#resource-test-helpers}
251
-
252
- Resource tests have two helpers, both different ways to execute a query.
253
-
254
- `render` will fire the query and return a JSON response that can be accessed as normal:
255
-
256
- ```ruby
257
- it 'works' do
258
- render
259
- expect(jsonapi_data[0].first_name).to eq('Jane')
260
- json # => { data: { type: 'employees', ... } }
261
- end
262
- ```
263
-
264
- `records` will return model instances:
265
-
266
- ```ruby
267
- it 'works' do
268
- render
269
- expect(records.map(&:id)).to eq([1, 2, 3])
270
- end
271
- ```
272
-
273
- ### Resource Matchers {#resource-matchers}
274
-
275
- For one-line assertions about a Resource's shape, use the built-in matchers. They're included automatically in `type: :resource` specs and expect a Resource instance as the subject:
276
-
277
- ```ruby
278
- RSpec.describe PostResource, type: :resource do
279
- subject { described_class.new }
280
-
281
- it { is_expected.to belong_to_resource(:author) }
282
- it { is_expected.to have_many_resources(:comments) }
283
- it { is_expected.to have_one_resource(:detail) }
284
- it { is_expected.to expose_attribute(:title, :string) }
285
- it { is_expected.to filter_attribute(:title, :string) }
286
- end
287
- ```
288
-
289
- Each matcher accepts `with_options` to assert configuration:
290
-
291
- ```ruby
292
- it do
293
- is_expected.to belong_to_resource(:author)
294
- .with_options(foreign_key: :author_id, resource: AuthorResource)
295
- end
296
-
297
- it { is_expected.to expose_attribute(:title, :string).with_options(writable: false) }
298
- ```
299
-
300
- ### API Test Helpers {#api-test-helpers}
301
-
302
- When executing an API test request, always use the `jsonapi_` doppelgänger:
303
-
304
- * `jsonapi_get(url, params:)` instead of `get`
305
- * `jsonapi_post(url, payload)` instead of `post`
306
- * `jsonapi_put(url, payload)` instead of `put`
307
- * `jsonapi_patch(url, payload)` instead of `patch`
308
- * `jsonapi_delete(url)` instead of `delete`
309
-
310
- This will set the `CONTENT_TYPE` header to `application/vnd.api+json` and call `to_json` on the payload (when applicable).
311
-
312
- It also allows overriding `jsonapi_headers`. Use this to manipulate headers for a given request:
313
-
314
- ```ruby
315
- def jsonapi_headers
316
- super.tap do |headers|
317
- headers['CUSTOM'] = 'foo'
318
- end
319
- end
320
- ```
321
-
322
- ### Guard Helpers {#guard-helpers}
323
-
324
- 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).
325
-
326
- ## Resource Tests {#resource-tests}
327
-
328
- There are two test files for each Resource:
329
-
330
- * `spec/resources/post/reads_spec.rb`
331
- * `spec/resources/post/writes_spec.rb`
332
-
333
- ### Reads {#reads}
334
-
335
- The basic setup for read operations:
336
-
337
- ```ruby
338
- # spec/resources/employee/reads_spec.rb
339
- require 'rails_helper'
340
-
341
- RSpec.describe EmployeeResource, type: :resource do
342
- describe 'serialization' do
343
- # ... code ...
344
- end
345
-
346
- describe 'filtering' do
347
- # ... code ...
348
- end
349
-
350
- describe 'sorting' do
351
- # ... code ...
352
- end
353
-
354
- describe 'sideloading' do
355
- # ... code ...
356
- end
357
- end
358
- ```
359
-
360
- #### Serialization {#serialization}
361
-
362
- ```ruby
363
- describe 'serialization' do
364
- let!(:employee) { create(:employee, first_name: 'Jane') }
365
-
366
- it 'works' do
367
- render
368
- data = jsonapi_data[0]
369
- expect(data.id).to eq(employee.id)
370
- expect(data.jsonapi_type).to eq('employees')
371
- expect(data.first_name).to eq('Jane')
372
- end
373
- end
374
- ```
375
-
376
- Best practices:
377
-
378
- * Assert on all attributes, even if there is no logic. This way adding logic will cause a test failure.
379
- * When seeding data, manually assign values. This way you can be assured you aren't accidentally testing `nil == nil`
380
-
381
- If you decide you have a high level of confidence in your factories, you can instead save some keystrokes and assert on randomized data:
382
-
383
- ```ruby
384
- expect(data.first_name).to eq(employee.first_name)
385
- ```
386
-
387
- > Note: Our schema validation test will ensure no attributes get removed or change types.
388
-
389
- #### Filtering {#filtering}
390
-
391
- ```ruby
392
- describe 'filtering' do
393
- let!(:employee1) { create(:employee) }
394
- let!(:employee2) { create(:employee) }
395
-
396
- context 'by id' do
397
- before do
398
- params[:filter] = { id: { eq: employee2.id } }
399
- end
400
-
401
- it 'works' do
402
- render
403
- expect(jsonapi_data.map(&:id)).to eq([employee2.id])
404
- end
405
- end
406
- end
407
- ```
408
-
409
- 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.
410
-
411
- #### Sorting {#sorting}
412
-
413
- ```ruby
414
- describe 'sorting' do
415
- describe 'by id' do
416
- let!(:employee1) { create(:employee) }
417
- let!(:employee2) { create(:employee) }
418
-
419
- context 'when ascending' do
420
- before do
421
- params[:sort] = 'id'
422
- end
423
-
424
- it 'works' do
425
- render
426
- expect(jsonapi_data.map(&:id)).to eq([
427
- employee1.id,
428
- employee2.id
429
- ])
430
- end
431
- end
432
-
433
- context 'when descending' do
434
- before do
435
- params[:sort] = '-id'
436
- end
437
-
438
- it 'works' do
439
- render
440
- expect(jsonapi_data.map(&:id)).to eq([
441
- employee2.id,
442
- employee1.id
443
- ])
444
- end
445
- end
446
- end
447
- end
448
- ```
449
-
450
- 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.
451
-
452
- #### Sideloading {#sideloading}
453
-
454
- ```ruby
455
- describe 'sideloading' do
456
- let!(:employee) { create(:employee) }
457
-
458
- describe 'current_position' do
459
- let!(:pos1) do
460
- create(:position, employee: employee, historical_index: 2)
461
- end
462
- let!(:pos2) do
463
- create(:position, employee: employee, historical_index: 1)
464
- end
465
-
466
- before do
467
- params[:include] = 'current_position'
468
- end
469
-
470
- it 'returns position with historical index == 1' do
471
- render
472
- sl = jsonapi_data[0].sideload(:current_position)
473
- expect(sl.jsonapi_type).to eq('positions')
474
- expect(sl.id).to eq(pos2.id)
475
- end
476
- end
477
- end
478
- ```
479
-
480
- 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.
481
-
482
- 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.
483
-
484
- ### Writes {#writes}
485
-
486
- The basic setup for write operations:
487
-
488
- ```ruby
489
- # spec/resources/employee/writes_spec.rb
490
- require 'rails_helper'
491
-
492
- RSpec.describe EmployeeResource, type: :resource do
493
- describe 'creating' do
494
- let(:payload) { ... }
495
- # ... code ...
496
- end
497
-
498
- describe 'creating' do
499
- let(:payload) { ... }
500
- # ... code ...
501
- end
502
-
503
- describe 'destroying' do
504
- # ... code ...
505
- end
506
- end
507
- ```
508
-
509
- Here `payload` is a [JSONAPI Resource Object](http://jsonapi.org/format/#crud).
510
-
511
- #### Create {#create}
512
-
513
- ```ruby
514
- describe 'creating' do
515
- let(:payload) do
516
- {
517
- data: {
518
- type: 'employees',
519
- attributes: { }
520
- }
521
- }
522
- end)
523
-
524
- let(:instance) do
525
- EmployeeResource.build(payload)
526
- end
527
-
528
- it 'works' do
529
- expect {
530
- expect(instance.save).to eq(true)
531
- }.to change { Employee.count }.by(1)
532
- end
533
- end
534
- ```
535
-
536
- `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:
537
-
538
- ```ruby
539
- let(:payload) do
540
- {
541
- data: {
542
- type: 'employees',
543
- attributes: { first_name: 'Jane', age: 30 }
544
- }
545
- }
546
- end
547
-
548
- # ... code ...
549
-
550
- it 'works' do
551
- expect {
552
- expect(instance.save).to eq(true)
553
- }.to change { Employee.count }.by(1)
554
- employee = Employee.last
555
- expect(employee.first_name).to eq('Jane')
556
- expect(employee.age).to eq(30)
557
- end
558
- ```
559
-
560
- ##### Required Belongs To {#required-belongs-to}
561
-
562
- 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).
563
-
564
- You have 3 options here:
565
-
566
- * Turn off this validation in test mode. Add `config.active_record.belongs_to_required_by_default = false` to `config/environments/test.rb`.
567
- * Turn off the validation for this specific relationship: `belongs_to :department, optional: true`.
568
- * Associate as part of the request.
569
-
570
- We recommend the third option to preserve real-world end-to-end behavior:
571
-
572
- ```ruby
573
- describe 'creating' do
574
- let!(:department) { create(:department) }
575
-
576
- let(:payload) do
577
- {
578
- type: 'employees',
579
- attributes: { ... },
580
- relationships: {
581
- department: {
582
- data: {
583
- type: 'departments',
584
- id: department.id.to_s
585
- }
586
- }
587
- }
588
- }
589
- end
590
-
591
- # ... code ...
592
- end
593
- ```
594
-
595
- This ensures the Employee is created and associated to the given department.
596
-
597
- #### Update {#update}
598
-
599
- 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:
600
-
601
- ```ruby
602
- describe 'updating' do
603
- let!(:employee) { create(:employee) }
604
-
605
- let(:payload) do
606
- {
607
- data: {
608
- id: employee.id.to_s,
609
- type: 'employees',
610
- attributes: { first_name: 'changed!' }
611
- }
612
- }
613
- end
614
-
615
- let(:instance) do
616
- EmployeeResource.find(payload)
617
- end
618
-
619
- it 'works' do
620
- expect {
621
- expect(instance.update_attributes).to eq(true)
622
- }.to change { employee.reload.updated_at }
623
- .and change { employee.first_name }.to('changed!')
624
- end
625
- end
626
- ```
627
-
628
- > Note that this test will be pending by default when using the generator, as we require the attributes to be explicitly defined.
629
-
630
- #### Destroy {#destroy}
631
-
632
- Destroy specs drop the payload/instance-building entirely and just find and destroy the record, asserting the count decreases:
633
-
634
- ```ruby
635
- describe 'destroying' do
636
- let!(:employee) { create(:employee) }
637
-
638
- let(:instance) do
639
- EmployeeResource.find(id: employee.id)
640
- end
641
-
642
- it 'works' do
643
- expect {
644
- expect(instance.destroy).to eq(true)
645
- }.to change { Employee.count }.by(-1)
646
- end
647
- end
648
- ```
649
-
650
- #### Side Effects {#side-effects}
651
-
652
- ```ruby
653
- it 'works' do
654
- # some assertion
655
- email = ActionMailer::Base.deliveries.last
656
- expect(email.subject).to eq('Welcome!')
657
- end
658
- ```
659
-
660
- 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.
661
-
662
- ## API Tests {#api-tests}
663
-
664
- There are five test files for each Resource:
665
-
666
- * `spec/api/v1/employees/index_spec.rb`
667
- * `spec/api/v1/employees/show_spec.rb`
668
- * `spec/api/v1/employees/create_spec.rb`
669
- * `spec/api/v1/employees/update_spec.rb`
670
- * `spec/api/v1/employees/destroy_spec.rb`
671
-
672
- ### Reads {#api-reads}
673
-
674
- #### #index {#index}
675
-
676
- ```ruby
677
- require 'rails_helper'
678
-
679
- RSpec.describe "employees#index", type: :request do
680
- let(:params) { {} }
681
-
682
- subject(:make_request) do
683
- jsonapi_get "/api/v1/employees", params: params
684
- end
685
-
686
- describe 'basic fetch' do
687
- let!(:employee1) { create(:employee) }
688
- let!(:employee2) { create(:employee) }
689
-
690
- it 'works' do
691
- expect(EmployeeResource).to receive(:all).and_call_original
692
- make_request
693
- expect(response.status).to eq(200)
694
- expect(jsonapi_data.map(&:jsonapi_type).uniq)
695
- .to match_array(['employees'])
696
- expect(jsonapi_data.map(&:id))
697
- .to match_array([employee1.id, employee2.id])
698
- end
699
- end
700
- end
701
- ```
702
-
703
- #### #show {#show}
704
-
705
- Same shape as `#index`, but requests a single Employee by id and asserts against the singular `d` node instead of an array:
706
-
707
- ```ruby
708
- subject(:make_request) do
709
- jsonapi_get "/api/v1/employees/#{employee.id}", params: params
710
- end
711
-
712
- describe 'basic fetch' do
713
- let!(:employee) { create(:employee) }
714
-
715
- it 'works' do
716
- expect(EmployeeResource).to receive(:find).and_call_original
717
- make_request
718
- expect(response.status).to eq(200)
719
- expect(jsonapi_data.jsonapi_type).to eq('employees')
720
- expect(jsonapi_data.id).to eq(employee.id)
721
- end
722
- end
723
- ```
724
-
725
- ### Writes {#api-writes}
726
-
727
- #### #create {#api-create}
728
-
729
- ```ruby
730
- require 'rails_helper'
731
-
732
- RSpec.describe "employees#create", type: :request do
733
- subject(:make_request) do
734
- jsonapi_post "/api/v1/employees", payload
735
- end
736
-
737
- describe 'basic create' do
738
- let(:payload) do
739
- {
740
- data: {
741
- type: 'employees',
742
- attributes: {
743
- first_name: 'Jane'
744
- }
745
- }
746
- }
747
- end
748
-
749
- it 'works' do
750
- expect(EmployeeResource).to receive(:build).and_call_original
751
- expect {
752
- make_request
753
- }.to change { Employee.count }.by(1)
754
- expect(response.status).to eq(201)
755
- end
756
- end
757
- end
758
- ```
759
-
760
- 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:
761
-
762
- ```ruby
763
- let(:payload) do
764
- {
765
- data: {
766
- type: 'employees',
767
- attributes: attributes_for(:employee)
768
- }
769
- }
770
- end
771
- ```
772
-
773
- See also: [Dealing with required belongs_to relationships](#required-belongs-to).
774
-
775
- #### #update {#api-update}
776
-
777
- 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:
778
-
779
- ```ruby
780
- subject(:make_request) do
781
- jsonapi_put "/api/v1/employees/#{employee.id}", payload
782
- end
783
-
784
- describe 'basic update' do
785
- let!(:employee) { create(:employee) }
786
-
787
- let(:payload) do
788
- {
789
- data: {
790
- id: employee.id.to_s,
791
- type: 'employees',
792
- attributes: {
793
- first_name: 'changed!'
794
- }
795
- }
796
- }
797
- end
798
-
799
- it 'updates the resource' do
800
- expect(EmployeeResource).to receive(:find).and_call_original
801
- expect {
802
- make_request
803
- }.to change { employee.reload.attributes }
804
- expect(response.status).to eq(200)
805
- end
806
- end
807
- ```
808
-
809
- 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:
810
-
811
- ```ruby
812
- let(:payload) do
813
- {
814
- data: {
815
- id: employee.id.to_s,
816
- type: 'employees',
817
- attributes: attributes_for(:employee)
818
- }
819
- }
820
- end
821
- ```
822
-
823
- #### #destroy {#api-destroy}
824
-
825
- ```ruby
826
- subject(:make_request) do
827
- jsonapi_delete "/api/v1/employees/#{employee.id}"
828
- end
829
-
830
- describe 'basic destroy' do
831
- let!(:employee) { create(:employee) }
832
-
833
- it 'updates the resource' do
834
- expect(EmployeeResource).to receive(:find).and_call_original
835
- expect { make_request }.to change { Employee.count }.by(-1)
836
- expect { employee.reload }
837
- .to raise_error(ActiveRecord::RecordNotFound)
838
- expect(response.status).to eq(200)
839
- expect(json).to eq('meta' => {})
840
- end
841
- end
842
- ```
843
-
844
- 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.
845
-
846
- ## Context {#context}
847
-
848
- Occasionally you'll need to set context for tests. The most common scenario is authorization:
849
-
850
- ```ruby
851
- attribute :salary, :integer, readable: :admin?
852
-
853
- def admin?
854
- context.current_user.admin?
855
- end
856
- ```
857
-
858
- When using Rails, `context` is the controller associated to the request. We can manually set context in tests:
859
-
860
- ```ruby
861
- let(:user) { double(admin?: true) }
862
- let(:ctx) { double(current_user: user) }
863
-
864
- it 'works' do
865
- Graphiti.with_context ctx do
866
- render
867
- end
868
- expect(jsonapi_data[0].salary).to eq(100_000)
869
- end
870
- ```
871
-
872
- ## Schema Validation {#schema-validation}
873
-
874
- Graphiti comes with built-in backwards-compatibility tests. We do this by comparing the current version of the schema with one previously checked-in.
875
-
876
- These tests are added at the bottom of `spec/rails_helper.rb`:
877
-
878
- ```ruby
879
- Graphiti::SpecHelpers::RSpec.schema!
880
- ```
881
-
882
- 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.
883
-
884
- When the schema test succeeds, it will overwrite the existing schema file with the new schema. It will not do this on failure.
885
-
886
- There are times when you want to accept an incompatibility and move on anyway. In this case, use `FORCE_SCHEMA`:
887
-
888
- ```bash
889
- $ FORCE_SCHEMA=true bin/rspec
890
- ```
891
-
892
- ## Generators {#generators}
893
-
894
- The [Resource generator](/concepts/resources#generators) will create both Resource and API tests for you. Use these as templates to implement your tests.
895
-
896
- You can also run
897
-
898
- ```bash
899
- $ rails generate graphiti:api_test RESOURCE [options]
900
- ```
901
-
902
- For example
903
-
904
- ```bash
905
- $ rails generate graphiti:api_test EmployeeResource -a index show
906
- ```
907
-
908
- 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.
909
-
910
- ## Testing Spectrum {#testing-spectrum}
911
-
912
- 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.
913
-
914
- ## Double-Testing Units {#double-testing-units}
915
-
916
- 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.