cathode 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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