cathode 0.0.1 → 0.1.0

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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +346 -125
  3. data/Rakefile +1 -0
  4. data/app/controllers/cathode/base_controller.rb +28 -45
  5. data/app/models/cathode/token.rb +21 -0
  6. data/config/routes.rb +16 -0
  7. data/db/migrate/20140425164100_create_cathode_tokens.rb +11 -0
  8. data/lib/cathode.rb +0 -13
  9. data/lib/cathode/_version.rb +2 -1
  10. data/lib/cathode/action.rb +197 -39
  11. data/lib/cathode/action_dsl.rb +60 -0
  12. data/lib/cathode/base.rb +81 -12
  13. data/lib/cathode/create_request.rb +21 -0
  14. data/lib/cathode/custom_request.rb +5 -0
  15. data/lib/cathode/debug.rb +25 -0
  16. data/lib/cathode/destroy_request.rb +13 -0
  17. data/lib/cathode/engine.rb +9 -0
  18. data/lib/cathode/exceptions.rb +20 -0
  19. data/lib/cathode/index_request.rb +40 -0
  20. data/lib/cathode/object_collection.rb +49 -0
  21. data/lib/cathode/query.rb +24 -0
  22. data/lib/cathode/railtie.rb +21 -0
  23. data/lib/cathode/request.rb +139 -7
  24. data/lib/cathode/resource.rb +50 -19
  25. data/lib/cathode/resource_dsl.rb +46 -0
  26. data/lib/cathode/show_request.rb +13 -0
  27. data/lib/cathode/update_request.rb +26 -0
  28. data/lib/cathode/version.rb +112 -23
  29. data/lib/tasks/cathode_tasks.rake +5 -4
  30. data/spec/dummy/app/api/api.rb +0 -0
  31. data/spec/dummy/app/models/payment.rb +3 -0
  32. data/spec/dummy/app/models/product.rb +1 -0
  33. data/spec/dummy/app/models/sale.rb +5 -0
  34. data/spec/dummy/app/models/salesperson.rb +3 -0
  35. data/spec/dummy/db/development.sqlite3 +0 -0
  36. data/spec/dummy/db/migrate/20140409183635_create_sales.rb +11 -0
  37. data/spec/dummy/db/migrate/20140423172419_create_salespeople.rb +11 -0
  38. data/spec/dummy/db/migrate/20140424181343_create_payments.rb +10 -0
  39. data/spec/dummy/db/schema.rb +31 -1
  40. data/spec/dummy/db/test.sqlite3 +0 -0
  41. data/spec/dummy/log/development.log +1167 -0
  42. data/spec/dummy/log/test.log +180602 -0
  43. data/spec/dummy/spec/factories/payments.rb +8 -0
  44. data/spec/dummy/spec/factories/products.rb +1 -1
  45. data/spec/dummy/spec/factories/sales.rb +9 -0
  46. data/spec/dummy/spec/factories/salespeople.rb +7 -0
  47. data/spec/dummy/spec/requests/requests_spec.rb +434 -0
  48. data/spec/lib/cathode/action_spec.rb +136 -0
  49. data/spec/lib/cathode/base_spec.rb +34 -0
  50. data/spec/lib/cathode/create_request_spec.rb +40 -0
  51. data/spec/lib/cathode/custom_request_spec.rb +31 -0
  52. data/spec/lib/cathode/debug_spec.rb +25 -0
  53. data/spec/lib/cathode/destroy_request_spec.rb +28 -0
  54. data/spec/lib/cathode/index_request_spec.rb +62 -0
  55. data/spec/lib/cathode/object_collection_spec.rb +66 -0
  56. data/spec/lib/cathode/query_spec.rb +28 -0
  57. data/spec/lib/cathode/request_spec.rb +58 -0
  58. data/spec/lib/cathode/resource_spec.rb +482 -0
  59. data/spec/lib/cathode/show_request_spec.rb +23 -0
  60. data/spec/lib/cathode/update_request_spec.rb +41 -0
  61. data/spec/lib/cathode/version_spec.rb +416 -0
  62. data/spec/models/cathode/token_spec.rb +62 -0
  63. data/spec/spec_helper.rb +8 -2
  64. data/spec/support/factories/payments.rb +3 -0
  65. data/spec/support/factories/sale.rb +3 -0
  66. data/spec/support/factories/salespeople.rb +3 -0
  67. data/spec/support/factories/token.rb +3 -0
  68. data/spec/support/helpers.rb +13 -2
  69. metadata +192 -47
  70. data/app/helpers/cathode/application_helper.rb +0 -4
  71. data/spec/dummy/app/api/dummy_api.rb +0 -4
  72. data/spec/integration/api_spec.rb +0 -88
  73. data/spec/lib/action_spec.rb +0 -140
  74. data/spec/lib/base_spec.rb +0 -28
  75. data/spec/lib/request_spec.rb +0 -5
  76. data/spec/lib/resources_spec.rb +0 -78
  77. data/spec/lib/versioning_spec.rb +0 -104
@@ -0,0 +1,8 @@
1
+ # Read about factories at https://github.com/thoughtbot/factory_girl
2
+
3
+ FactoryGirl.define do
4
+ factory :payment do
5
+ amount 1
6
+ sale_id 1
7
+ end
8
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  FactoryGirl.define do
4
4
  factory :product do
5
- title "MyString"
5
+ title 'MyString'
6
6
  cost 1
7
7
  end
8
8
  end
@@ -0,0 +1,9 @@
1
+ # Read about factories at https://github.com/thoughtbot/factory_girl
2
+
3
+ FactoryGirl.define do
4
+ factory :sale, class: 'Sale' do
5
+ product_id 1
6
+ subtotal 1
7
+ taxes 1
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ # Read about factories at https://github.com/thoughtbot/factory_girl
2
+
3
+ FactoryGirl.define do
4
+ factory :salesperson do
5
+ name "MyString"
6
+ end
7
+ end
@@ -0,0 +1,434 @@
1
+ require 'spec_helper'
2
+
3
+ def make_request(method, path, params = nil, version = '1.0.0')
4
+ send(method, path, params, 'Accept-Version' => version)
5
+ end
6
+
7
+ def request_spec(method, path, params = nil, &block)
8
+ context 'without version header' do
9
+ subject { send(method, path, params) }
10
+
11
+ it 'responds with bad request' do
12
+ expect(subject).to eq(400)
13
+ expect(response.body).to eq('A version number must be passed in the Accept-Version header')
14
+ end
15
+ end
16
+
17
+ context 'with invalid version header' do
18
+ subject { make_request method, path, params, '2.0.0' }
19
+
20
+ it 'responds with bad request' do
21
+ expect(subject).to eq(400)
22
+ expect(response.body).to eq('Unknown API version: 2.0.0')
23
+ end
24
+ end
25
+
26
+ context 'with valid version header' do
27
+ subject { make_request method, path, params, '1.5.0' }
28
+
29
+ instance_eval(&block)
30
+ end
31
+ end
32
+
33
+ describe 'API' do
34
+ context 'with no explicit version' do
35
+ before(:all) do
36
+ use_api do
37
+ resources :products, actions: [:index]
38
+ end
39
+ end
40
+
41
+ let!(:products) { create_list(:product, 5) }
42
+
43
+ it 'makes a request' do
44
+ make_request :get, 'api/products'
45
+ expect(response.body).to eq(products.to_json)
46
+ end
47
+ end
48
+
49
+ context 'with explicit version' do
50
+ before do
51
+ use_api do
52
+ version 1.5 do
53
+ resources :products, actions: :all do
54
+ attributes do
55
+ params.require(:product).permit(:title, :cost)
56
+ end
57
+ end
58
+ resources :sales, actions: [:index, :show]
59
+ end
60
+ end
61
+ end
62
+
63
+ let!(:products) { create_list(:product, 5) }
64
+
65
+ describe 'resources with all actions' do
66
+ describe 'index' do
67
+ request_spec :get, 'api/products', nil do
68
+ it 'responds with all records' do
69
+ subject
70
+ expect(response.body).to eq(products.to_json)
71
+ end
72
+ end
73
+ end
74
+
75
+ describe 'show' do
76
+ request_spec :get, 'api/products/1' do
77
+ it 'responds with all records' do
78
+ subject
79
+ expect(response.body).to eq(products.first.to_json)
80
+ end
81
+ end
82
+ end
83
+
84
+ describe 'create' do
85
+ request_spec :post, 'api/products', product: { title: 'hello', cost: 1900 } do
86
+ it 'responds with the new record' do
87
+ subject
88
+ parsed_response = JSON.parse(response.body)
89
+ expect(parsed_response['title']).to eq('hello')
90
+ expect(parsed_response['cost']).to eq(1900)
91
+ end
92
+ end
93
+ end
94
+
95
+ describe 'update' do
96
+ request_spec :put, 'api/products/1', product: { title: 'goodbye' } do
97
+ it 'responds with the updated record' do
98
+ subject
99
+ expect(JSON.parse(response.body)['title']).to eq('goodbye')
100
+ end
101
+ end
102
+ end
103
+
104
+ describe 'destroy' do
105
+ request_spec :delete, 'api/products/5' do
106
+ it 'responds with success' do
107
+ expect(subject).to eq(200)
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ describe 'to a nonexistent endpoint' do
114
+ subject { make_request :get, 'api/boxes', nil, '1.5.0' }
115
+
116
+ it 'responds with 404' do
117
+ subject
118
+ expect(response.status).to eq(404)
119
+ end
120
+ end
121
+ end
122
+
123
+ context 'with cascading versions' do
124
+ before(:each) do
125
+ use_api do
126
+ resources :products, actions: [:index, :show]
127
+ version '1.0.1' do
128
+ resources :sales, actions: [:index, :show]
129
+ end
130
+ version 1.1 do
131
+ remove_resources :sales
132
+ resources :products, actions: [:index]
133
+ end
134
+ end
135
+ end
136
+
137
+ let!(:product) { create :product }
138
+
139
+ it 'inherits from previous versions' do
140
+ make_request :get, 'api/products', nil, '1.0'
141
+ expect(response.status).to eq(200)
142
+
143
+ make_request :get, 'api/products/1', nil, '1.0'
144
+ expect(response.status).to eq(200)
145
+
146
+ make_request :get, 'api/sales', nil, '1.0'
147
+ expect(response.status).to eq(404)
148
+
149
+ make_request :get, 'api/sales', nil, '1.0.1'
150
+ expect(response.status).to eq(200)
151
+
152
+ make_request :get, 'api/sales', nil, '1.1.0'
153
+ expect(response.status).to eq(404)
154
+
155
+ make_request :get, 'api/products/1', nil, '1.1.0'
156
+ expect(response.status).to eq(200)
157
+
158
+ make_request :get, 'api/products', nil, '1.1.0'
159
+ expect(response.status).to eq(200)
160
+ end
161
+ end
162
+
163
+ context 'with action replacing' do
164
+ before do
165
+ use_api do
166
+ resources :products do
167
+ action :show do
168
+ replace do
169
+ body Product.last
170
+ end
171
+ end
172
+ replace_action :index do
173
+ body Product.all.reverse
174
+ end
175
+ end
176
+ end
177
+ end
178
+
179
+ let!(:products) { create_list(:product, 3) }
180
+
181
+ describe 'with replace defined inside action' do
182
+ subject { make_request(:get, 'api/products/1', nil, '1.0') }
183
+
184
+ it 'uses the replace logic instead of the default behavior' do
185
+ subject
186
+ expect(response.body).to eq(Product.last.to_json)
187
+ end
188
+ end
189
+
190
+ describe 'with replace defined as the action' do
191
+ subject { make_request(:get, 'api/products', nil, '1.0') }
192
+
193
+ it 'uses the replace logic instead of the default behavior' do
194
+ subject
195
+ expect(JSON.parse(response.body).map { |p| p['id'] }).to eq(Product.all.reverse.map(&:id))
196
+ end
197
+ end
198
+ end
199
+
200
+ context 'with action overriding' do
201
+ before do
202
+ use_api do
203
+ resources :products do
204
+ action :show do
205
+ override do
206
+ render json: Product.last
207
+ end
208
+ end
209
+ override_action :index do
210
+ render json: Product.all.reverse
211
+ end
212
+ end
213
+ end
214
+ end
215
+
216
+ let!(:products) { create_list(:product, 3) }
217
+
218
+ describe 'with override defined inside action' do
219
+ subject { make_request(:get, 'api/products/1', nil, '1.0') }
220
+
221
+ it 'uses the custom logic instead of the default behavior' do
222
+ subject
223
+ expect(response.body).to eq(Product.last.to_json)
224
+ end
225
+ end
226
+
227
+ describe 'with override defined as the action' do
228
+ subject { make_request(:get, 'api/products', nil, '1.0') }
229
+
230
+ it 'uses the custom logic instead of the default behavior' do
231
+ subject
232
+ expect(JSON.parse(response.body).map { |p| p['id'] }).to eq(Product.all.reverse.map(&:id))
233
+ end
234
+ end
235
+ end
236
+
237
+ context 'with nested resources' do
238
+ before do
239
+ use_api do
240
+ resources :products do
241
+ resources :sales, actions: [:index]
242
+ end
243
+ resources :sales do
244
+ resource :payment, actions: [:show, :create, :update, :destroy] do
245
+ attributes do
246
+ params.require(:payment).permit(:amount)
247
+ end
248
+ end
249
+ end
250
+ resources :payments do
251
+ resource :sale, actions: [:show]
252
+ end
253
+ end
254
+ end
255
+ let!(:product) { create(:product) }
256
+ let!(:sale) { create(:sale, product: product) }
257
+
258
+ context 'with has_many association' do
259
+ it 'uses the associations to get the records' do
260
+ make_request :get, 'api/products/1/sales'
261
+ expect(response.status).to eq(200)
262
+ expect(response.body).to eq(Sale.all.to_json)
263
+ end
264
+ end
265
+
266
+ context 'with has_one association' do
267
+ context ':show' do
268
+ let!(:payment) { create(:payment, sale: sale) }
269
+
270
+ it 'gets the association record' do
271
+ make_request :get, 'api/sales/1/payment'
272
+ expect(response.status).to eq(200)
273
+ expect(response.body).to eq(payment.to_json)
274
+ end
275
+ end
276
+
277
+ context ':create' do
278
+ subject { make_request :post, 'api/sales/1/payment', { payment: { amount: 500 } } }
279
+
280
+ it 'adds a new record associated with the parent' do
281
+ expect { subject }.to change(Payment, :count).by(1)
282
+ expect(Payment.last.sale).to eq(sale)
283
+ expect(response.status).to eq(200)
284
+ expect(response.body).to eq(Payment.last.to_json)
285
+ end
286
+ end
287
+
288
+ context ':update' do
289
+ let!(:payment) { create(:payment, amount: 200, sale: sale) }
290
+ subject { make_request :put, 'api/sales/1/payment', { payment: { amount: 500 } } }
291
+
292
+ it 'updates the associated record' do
293
+ expect { subject }.to_not change(Payment, :count).by(1)
294
+ expect(response.status).to eq(200)
295
+ expect(JSON.parse(response.body)['amount']).to eq(500)
296
+ end
297
+ end
298
+
299
+ context ':destroy' do
300
+ let!(:payment) { create(:payment, amount: 200, sale: sale) }
301
+ subject { make_request :delete, 'api/sales/1/payment' }
302
+
303
+ it 'deletes the associated record' do
304
+ expect { subject }.to change(Payment, :count).by(-1)
305
+ expect(response.status).to eq(200)
306
+ expect(sale.payment).to be_nil
307
+ end
308
+ end
309
+ end
310
+
311
+ context 'with belongs_to association' do
312
+ context ':show' do
313
+ let!(:payment) { create(:payment, sale: sale) }
314
+
315
+ it 'gets the association record' do
316
+ make_request :get, 'api/payments/1/sale'
317
+ expect(response.status).to eq(200)
318
+ expect(response.body).to eq(sale.to_json)
319
+ end
320
+ end
321
+ end
322
+ end
323
+
324
+ context 'with a custom action' do
325
+ before do
326
+ use_api do
327
+ resources :products do
328
+ override_action :custom_override, method: :get do
329
+ render json: Product.last
330
+ end
331
+ get :custom_replace do
332
+ attributes do
333
+ params.require(:flag)
334
+ end
335
+ body Product.all.reverse
336
+ end
337
+ end
338
+ end
339
+ end
340
+
341
+ let!(:products) { create_list(:product, 3) }
342
+
343
+ context 'with replace (default)' do
344
+ subject { make_request(:get, 'api/products/custom_replace', { flag: true }, '1.0') }
345
+
346
+ it 'sets the status' do
347
+ subject
348
+ expect(response.status).to eq(200)
349
+ end
350
+
351
+ it 'sets the body with the replace logic' do
352
+ subject
353
+ expect(JSON.parse(response.body).map { |p| p['id'] }).to eq(Product.all.reverse.map(&:id))
354
+ end
355
+ end
356
+
357
+ context 'with override' do
358
+ subject { make_request(:get, 'api/products/custom_override', nil, '1.0') }
359
+
360
+ it 'sets the status' do
361
+ subject
362
+ expect(response.status).to eq(200)
363
+ end
364
+
365
+ it 'uses the override logic' do
366
+ subject
367
+ expect(response.body).to eq(Product.last.to_json)
368
+ end
369
+ end
370
+
371
+ context 'with attributes block' do
372
+ subject { make_request(:get, 'api/products/custom_replace', nil, '1.0') }
373
+
374
+ it 'sets the status' do
375
+ subject
376
+ expect(response.status).to eq(400)
377
+ end
378
+
379
+ it 'sets the body' do
380
+ subject
381
+ expect(response.body).to eq('param is missing or the value is empty: flag')
382
+ end
383
+ end
384
+ end
385
+
386
+ describe 'token auth' do
387
+ subject { get 'api/products', nil, headers }
388
+ let(:headers) { { 'HTTP_ACCEPT_VERSION' => '1.0.0' } }
389
+ before do
390
+ api = proc do |required|
391
+ proc do
392
+ require_tokens if required
393
+ resources :products, actions: [:index]
394
+ end
395
+ end
396
+ use_api(&api.call(required))
397
+ end
398
+
399
+ context 'when tokens are required' do
400
+ let(:required) { true }
401
+
402
+ context 'with a valid token' do
403
+ let(:token) { create(:token) }
404
+ before { headers['Authorization'] = "Token token=#{token.token}" }
405
+
406
+ it 'responds with ok' do
407
+ expect(subject).to eq(200)
408
+ end
409
+ end
410
+
411
+ context 'with an invalid token' do
412
+ before { headers['Authorization'] = 'Token token=invalid' }
413
+
414
+ it 'responds with unauthorized' do
415
+ expect(subject).to eq(401)
416
+ end
417
+ end
418
+
419
+ context 'with no token' do
420
+ it 'responds with unauthorized' do
421
+ expect(subject).to eq(401)
422
+ end
423
+ end
424
+ end
425
+
426
+ context 'when tokens are not required' do
427
+ let(:required) { false }
428
+
429
+ it 'responds with ok' do
430
+ expect(subject).to eq(200)
431
+ end
432
+ end
433
+ end
434
+ end