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.
- checksums.yaml +4 -4
- data/README.md +346 -125
- data/Rakefile +1 -0
- data/app/controllers/cathode/base_controller.rb +28 -45
- data/app/models/cathode/token.rb +21 -0
- data/config/routes.rb +16 -0
- data/db/migrate/20140425164100_create_cathode_tokens.rb +11 -0
- data/lib/cathode.rb +0 -13
- data/lib/cathode/_version.rb +2 -1
- data/lib/cathode/action.rb +197 -39
- data/lib/cathode/action_dsl.rb +60 -0
- data/lib/cathode/base.rb +81 -12
- data/lib/cathode/create_request.rb +21 -0
- data/lib/cathode/custom_request.rb +5 -0
- data/lib/cathode/debug.rb +25 -0
- data/lib/cathode/destroy_request.rb +13 -0
- data/lib/cathode/engine.rb +9 -0
- data/lib/cathode/exceptions.rb +20 -0
- data/lib/cathode/index_request.rb +40 -0
- data/lib/cathode/object_collection.rb +49 -0
- data/lib/cathode/query.rb +24 -0
- data/lib/cathode/railtie.rb +21 -0
- data/lib/cathode/request.rb +139 -7
- data/lib/cathode/resource.rb +50 -19
- data/lib/cathode/resource_dsl.rb +46 -0
- data/lib/cathode/show_request.rb +13 -0
- data/lib/cathode/update_request.rb +26 -0
- data/lib/cathode/version.rb +112 -23
- data/lib/tasks/cathode_tasks.rake +5 -4
- data/spec/dummy/app/api/api.rb +0 -0
- data/spec/dummy/app/models/payment.rb +3 -0
- data/spec/dummy/app/models/product.rb +1 -0
- data/spec/dummy/app/models/sale.rb +5 -0
- data/spec/dummy/app/models/salesperson.rb +3 -0
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/migrate/20140409183635_create_sales.rb +11 -0
- data/spec/dummy/db/migrate/20140423172419_create_salespeople.rb +11 -0
- data/spec/dummy/db/migrate/20140424181343_create_payments.rb +10 -0
- data/spec/dummy/db/schema.rb +31 -1
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +1167 -0
- data/spec/dummy/log/test.log +180602 -0
- data/spec/dummy/spec/factories/payments.rb +8 -0
- data/spec/dummy/spec/factories/products.rb +1 -1
- data/spec/dummy/spec/factories/sales.rb +9 -0
- data/spec/dummy/spec/factories/salespeople.rb +7 -0
- data/spec/dummy/spec/requests/requests_spec.rb +434 -0
- data/spec/lib/cathode/action_spec.rb +136 -0
- data/spec/lib/cathode/base_spec.rb +34 -0
- data/spec/lib/cathode/create_request_spec.rb +40 -0
- data/spec/lib/cathode/custom_request_spec.rb +31 -0
- data/spec/lib/cathode/debug_spec.rb +25 -0
- data/spec/lib/cathode/destroy_request_spec.rb +28 -0
- data/spec/lib/cathode/index_request_spec.rb +62 -0
- data/spec/lib/cathode/object_collection_spec.rb +66 -0
- data/spec/lib/cathode/query_spec.rb +28 -0
- data/spec/lib/cathode/request_spec.rb +58 -0
- data/spec/lib/cathode/resource_spec.rb +482 -0
- data/spec/lib/cathode/show_request_spec.rb +23 -0
- data/spec/lib/cathode/update_request_spec.rb +41 -0
- data/spec/lib/cathode/version_spec.rb +416 -0
- data/spec/models/cathode/token_spec.rb +62 -0
- data/spec/spec_helper.rb +8 -2
- data/spec/support/factories/payments.rb +3 -0
- data/spec/support/factories/sale.rb +3 -0
- data/spec/support/factories/salespeople.rb +3 -0
- data/spec/support/factories/token.rb +3 -0
- data/spec/support/helpers.rb +13 -2
- metadata +192 -47
- data/app/helpers/cathode/application_helper.rb +0 -4
- data/spec/dummy/app/api/dummy_api.rb +0 -4
- data/spec/integration/api_spec.rb +0 -88
- data/spec/lib/action_spec.rb +0 -140
- data/spec/lib/base_spec.rb +0 -28
- data/spec/lib/request_spec.rb +0 -5
- data/spec/lib/resources_spec.rb +0 -78
- data/spec/lib/versioning_spec.rb +0 -104
@@ -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
|