restfulness 0.2.2 → 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +1 -1
- data/README.md +173 -21
- data/lib/restfulness.rb +10 -0
- data/lib/restfulness/dispatchers/rack.rb +11 -5
- data/lib/restfulness/request.rb +16 -14
- data/lib/restfulness/response.rb +2 -2
- data/lib/restfulness/route.rb +1 -1
- data/lib/restfulness/sanitizer.rb +70 -0
- data/lib/restfulness/version.rb +1 -1
- data/spec/unit/dispatchers/rack_spec.rb +32 -4
- data/spec/unit/request_spec.rb +57 -1
- data/spec/unit/sanitizer_spec.rb +61 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fec45922d74ec00c62042cfa323627cb5e23d925
|
4
|
+
data.tar.gz: 87679a0dd96e313a113f24e85e35c4130285cb01
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a818c4417a58a9a84fe108b5a0c0a57e02f6342ccd47810e571e6d2f6d49ec0c736a111a1ac9b82ec1a67eeecc644908af1fbb869cf0f67f4a31335112e44fb6
|
7
|
+
data.tar.gz: 568d63dc34ffa86bd6e8f59c5d278ac54426ad7b95648c79252fd1475c93c80cd17611245f8d2e7d926dda22bdc5fc9bad929249cc4cef5aa2e62a9842a35057
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -144,6 +144,8 @@ bundle exec rackup
|
|
144
144
|
|
145
145
|
For a very simple example project, checkout the `/example` directory in the source code.
|
146
146
|
|
147
|
+
If you're using Restulfness in a Rails project, you'll want to checkout the Reloading section below.
|
148
|
+
|
147
149
|
|
148
150
|
### Routes
|
149
151
|
|
@@ -290,11 +292,27 @@ request.body # "{'key':'value'}" - string payload
|
|
290
292
|
request.params # {'key' => 'value'} - usually a JSON deserialized object
|
291
293
|
```
|
292
294
|
|
295
|
+
### Logging
|
296
|
+
|
297
|
+
By default, Restfulness uses `ActiveSupport::Logger.new(STDOUT)` as its logger.
|
298
|
+
|
299
|
+
To change the logger:
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
Restfulness.logger = Rack::NullLogger.new(My::Api)
|
303
|
+
```
|
304
|
+
|
305
|
+
By default, any parameter with key prefix `password` will be sanitized in the log. To change the sensitive parameters:
|
306
|
+
|
307
|
+
```ruby
|
308
|
+
Restfulness.sensitive_params = [:password, :secretkey]
|
309
|
+
```
|
310
|
+
|
293
311
|
## Error Handling
|
294
312
|
|
295
313
|
If you want your application to return anything other than a 200 (or 202) status, you have a couple of options that allow you to send codes back to the client.
|
296
314
|
|
297
|
-
|
315
|
+
One of the easiest approaches is to update the `response` code. Take the following example where we set a 403 response and the model's errors object in the payload:
|
298
316
|
|
299
317
|
```ruby
|
300
318
|
class ProjectResource < Restfulness::Resource
|
@@ -315,7 +333,7 @@ The favourite method in Restfulness however is to use the `HTTPException` class
|
|
315
333
|
class ProjectResource < Restfulness::Resource
|
316
334
|
def patch
|
317
335
|
unless project.update_attributes(request.params)
|
318
|
-
forbidden!
|
336
|
+
forbidden! project.errors
|
319
337
|
end
|
320
338
|
project
|
321
339
|
end
|
@@ -352,7 +370,7 @@ end
|
|
352
370
|
|
353
371
|
```
|
354
372
|
|
355
|
-
This can be a really nice way to mold your errors into a standard format. All HTTP exceptions generated inside resources will pass through `error!`, even those that a triggered by a callback. It gives a great way to provide your own
|
373
|
+
This can be a really nice way to mold your errors into a standard format. All HTTP exceptions generated inside resources will pass through `error!`, even those that a triggered by a callback. It gives a great way to provide your own JSON error payload, or even just resort to a simple string.
|
356
374
|
|
357
375
|
The currently built in error methods are:
|
358
376
|
|
@@ -367,7 +385,7 @@ The currently built in error methods are:
|
|
367
385
|
* `gone!`
|
368
386
|
* `unprocessable_entity!`
|
369
387
|
|
370
|
-
If you'd like to see me more, please send us a pull request
|
388
|
+
If you'd like to see me more, please send us a pull request. Failing that, you can create your own by writing something along the lines of:
|
371
389
|
|
372
390
|
```ruby
|
373
391
|
def im_a_teapot!(payload = "")
|
@@ -375,38 +393,147 @@ def im_a_teapot!(payload = "")
|
|
375
393
|
end
|
376
394
|
```
|
377
395
|
|
396
|
+
## Reloading
|
378
397
|
|
379
|
-
|
398
|
+
We're all used to the way Rails projects magically reload files so you don't have to restart the server after each change. Depending on the way you use Restfulness in your project, this can be supported.
|
380
399
|
|
381
|
-
|
400
|
+
### The Rails Way
|
382
401
|
|
383
|
-
|
384
|
-
* Support path methods for automatic URL generation.
|
385
|
-
* Support redirect exceptions.
|
386
|
-
* Reloading is a PITA (see note below).
|
387
|
-
* Needs more functional testing.
|
388
|
-
* Support for before and after filters in resources, although I'm slightly aprehensive about this.
|
402
|
+
Using Restfulness in Rails is the easiest way to take advantage support reloading.
|
389
403
|
|
390
|
-
|
404
|
+
The recomended approach is to create two directories in your Rails projects `/app` path:
|
391
405
|
|
392
|
-
|
406
|
+
* `/app/apis` can be used for defining your API route files, and
|
407
|
+
* `/app/resources` for defining a tree of resource definition files.
|
393
408
|
|
394
|
-
|
409
|
+
Add the two paths to your rails autoloading configuration in `/config/application.rb`, there will already be a sample in your config provided by Rails:
|
410
|
+
|
411
|
+
```ruby
|
412
|
+
# Custom directories with classes and modules you want to be autoloadable.
|
413
|
+
config.autoload_paths += %W( #{config.root}/app/resources #{config.root}/app/apis )
|
414
|
+
```
|
395
415
|
|
396
|
-
|
416
|
+
Your Resource and API files will now be autoloadable from your Rails project. The next step is to update our Rails router to be able to find our API. Modify the `/config/routes.rb` file so that it looks something like the following:
|
397
417
|
|
398
418
|
```ruby
|
399
|
-
|
419
|
+
YourRailsApp::Application.routes.draw do
|
420
|
+
|
421
|
+
# Autoreload the API in development
|
400
422
|
if Rails.env.development?
|
401
|
-
|
423
|
+
mount Api.new => '/api'
|
402
424
|
end
|
403
|
-
|
404
|
-
|
425
|
+
|
426
|
+
#.... rest of routes
|
427
|
+
end
|
428
|
+
|
429
|
+
```
|
430
|
+
|
431
|
+
You'll see in the code sample that we're only loading the Restfulness API during development. Our recommendation is to use Restfulness as close to Rack as possible and avoid any of the Rails overhead. To support request in production, you'll need to update your `/config.rb` so that it looks something like the following:
|
432
|
+
|
433
|
+
```ruby
|
434
|
+
# This file is used by Rack-based servers to start the application.
|
435
|
+
require ::File.expand_path('../config/environment', __FILE__)
|
436
|
+
|
437
|
+
map = {
|
438
|
+
"/" => YourRailsApp::Application
|
439
|
+
}
|
440
|
+
unless Rails.env.development?
|
441
|
+
map["/api"] = Api.new
|
442
|
+
end
|
443
|
+
|
444
|
+
run Rack::URLMap.new(map)
|
445
|
+
```
|
446
|
+
|
447
|
+
Thats all there is to it! You'll now have auto-reloading in Rails, and fast request handling in production. Just be sure to be careful in development that none of your other Rack middleware interfere with Restfulness. In a new Rails project this certainly won't be an issue.
|
448
|
+
|
449
|
+
### The Rack Way
|
450
|
+
|
451
|
+
If you're using Restfulness as a standalone project, we recommend using a rack extension like [Shotgun](https://github.com/rtomayko/shotgun).
|
452
|
+
|
453
|
+
|
454
|
+
## Writing Tests
|
455
|
+
|
456
|
+
Test your application by creating requests to your resources and making assertions about the responses.
|
457
|
+
|
458
|
+
### RSpec
|
459
|
+
|
460
|
+
Configure `rack-test` to be included in your resource specs. One way to does this would be to create a new file `/spec/support/example_groups/restfulness_resource_example_group.rb` with something similar to the following:
|
461
|
+
|
462
|
+
```ruby
|
463
|
+
module RestfulnessResourceExampleGroup
|
464
|
+
extend ActiveSupport::Concern
|
465
|
+
include Rack::Test::Methods
|
466
|
+
|
467
|
+
# Used by Rack::Test. This could be defined per spec if you have multiple Apps
|
468
|
+
def app
|
469
|
+
My::Api.new
|
470
|
+
end
|
471
|
+
protected :app
|
472
|
+
|
473
|
+
# Set the request content type for a JSON payload
|
474
|
+
def set_content_type_json
|
475
|
+
header('content-type', 'application/json; charset=utf-8')
|
476
|
+
end
|
477
|
+
|
478
|
+
# Helper method to POST a json payload
|
479
|
+
# post(uri, params = {}, env = {}, &block)
|
480
|
+
def post_json(uri, json_data = {}, env = {}, &block)
|
481
|
+
set_content_type_json
|
482
|
+
post(uri, json_data.to_json, &block)
|
483
|
+
end
|
484
|
+
|
485
|
+
included do
|
486
|
+
metadata[:type] = :restfulness_resource
|
487
|
+
end
|
488
|
+
|
489
|
+
# Setup RSpec to include RestfulnessResourceExampleGroup for all specs in given folder(s)
|
490
|
+
RSpec.configure do |config|
|
491
|
+
config.include self,
|
492
|
+
:type => :restfulness_resource,
|
493
|
+
:example_group => { :file_path => %r(spec/resources) }
|
494
|
+
end
|
495
|
+
|
496
|
+
# silence logger
|
497
|
+
Restfulness.logger = Rack::NullLogger.new(My::Api)
|
498
|
+
end
|
499
|
+
```
|
500
|
+
|
501
|
+
Make sure in your `spec_helper` all files in the support folder and sub-directories are being loaded. You should have something like the following:
|
502
|
+
|
503
|
+
```ruby
|
504
|
+
Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
|
505
|
+
```
|
506
|
+
|
507
|
+
Now you can add a resource spec in the `spec/resources` directory. Here's an example
|
508
|
+
|
509
|
+
```ruby
|
510
|
+
require 'spec_helper'
|
511
|
+
|
512
|
+
describe SessionResource do
|
513
|
+
|
514
|
+
let(:user) { create(:user) }
|
515
|
+
|
516
|
+
context 'GET' do
|
517
|
+
it 'returns 401 if not authorized' do
|
518
|
+
get 'api/session' do |response|
|
519
|
+
expect(response.status).to eq 401
|
520
|
+
end
|
521
|
+
end
|
522
|
+
end
|
523
|
+
|
524
|
+
context 'POST' do
|
525
|
+
it 'returns 200 when request with correct user info' do
|
526
|
+
post_json 'api/session', {:email => user.email, :password => user.password} do |response|
|
527
|
+
expect(response.status).to eq 200
|
528
|
+
end
|
529
|
+
end
|
405
530
|
end
|
406
531
|
end
|
407
532
|
```
|
408
533
|
|
409
|
-
|
534
|
+
See [Rack::Test](https://github.com/brynary/rack-test) for more information.
|
535
|
+
|
536
|
+
A useful gem for making assertions about json objects is [json_spec](https://github.com/collectiveidea/json_spec). This could be included in your `RestfulnessResourceExampleGroup`.
|
410
537
|
|
411
538
|
## Contributing
|
412
539
|
|
@@ -421,9 +548,34 @@ We're still working on ways to improve this. If you have any ideas, please send
|
|
421
548
|
|
422
549
|
Restfulness was created by Sam Lown <me@samlown.com> as a solution for building simple APIs at [Cabify](http://www.cabify.com).
|
423
550
|
|
551
|
+
The project is now awesome, thanks to contributions by:
|
552
|
+
|
553
|
+
* [Adam Williams](https://github.com/awilliams)
|
554
|
+
|
555
|
+
|
556
|
+
## Caveats and TODOs
|
557
|
+
|
558
|
+
Restfulness is still a work in progress but at Cabify we are using it in production. Here is a list of things that we'd like to improve or fix:
|
559
|
+
|
560
|
+
* Support for more serializers and content types, not just JSON.
|
561
|
+
* Support path methods for automatic URL generation.
|
562
|
+
* Support redirect exceptions.
|
563
|
+
* Needs more functional testing.
|
564
|
+
* Support for before and after filters in resources, although I'm slightly aprehensive about this.
|
424
565
|
|
425
566
|
## History
|
426
567
|
|
568
|
+
### 0.2.3 - pending
|
569
|
+
|
570
|
+
* Fixing issue where query parameters are set as Hash instead of HashWithIndifferentAccess.
|
571
|
+
* Rewinding the body, incase rails got there first.
|
572
|
+
* Updating the README to describe auto-reloading in Rails projects.
|
573
|
+
* Improved handling of Content-Type header that includes encoding. (@awilliams)
|
574
|
+
* Return 400 error when malformed JSON is provided in body (@awilliams)
|
575
|
+
* Updated documentation to describe resource testing (@awilliams)
|
576
|
+
* Now supports filtering of sensitive query and parameter request values (@awilliams)
|
577
|
+
* Adding support for X-HTTP-Method-Override header. (@samlown)
|
578
|
+
|
427
579
|
### 0.2.2 - October 31, 2013
|
428
580
|
|
429
581
|
* Refactoring logging support to not depend on Rack CommonLogger nor ShowExceptions.
|
data/lib/restfulness.rb
CHANGED
@@ -20,6 +20,7 @@ require "restfulness/resource"
|
|
20
20
|
require "restfulness/response"
|
21
21
|
require "restfulness/route"
|
22
22
|
require "restfulness/router"
|
23
|
+
require "restfulness/sanitizer"
|
23
24
|
require "restfulness/statuses"
|
24
25
|
require "restfulness/version"
|
25
26
|
|
@@ -29,6 +30,15 @@ module Restfulness
|
|
29
30
|
extend self
|
30
31
|
|
31
32
|
attr_accessor :logger
|
33
|
+
|
34
|
+
# Determine which parameters keys should be filtered in logs, etc
|
35
|
+
def sensitive_params=(params)
|
36
|
+
@sensitive_params = params
|
37
|
+
end
|
38
|
+
|
39
|
+
def sensitive_params
|
40
|
+
@sensitive_params ||= [:password]
|
41
|
+
end
|
32
42
|
end
|
33
43
|
|
34
44
|
Restfulness.logger = ActiveSupport::Logger.new(STDOUT)
|
@@ -19,14 +19,17 @@ module Restfulness
|
|
19
19
|
|
20
20
|
def prepare_request(env)
|
21
21
|
rack_req = ::Rack::Request.new(env)
|
22
|
+
|
22
23
|
request = Request.new(app)
|
23
24
|
|
24
25
|
request.uri = rack_req.url
|
25
|
-
request.action = parse_action(rack_req.request_method)
|
26
|
-
request.query = rack_req.GET
|
26
|
+
request.action = parse_action(env, rack_req.request_method)
|
27
27
|
request.body = rack_req.body
|
28
28
|
request.headers = prepare_headers(env)
|
29
29
|
|
30
|
+
# Just in case something else got to body first
|
31
|
+
request.body.rewind if request.body.is_a?(StringIO)
|
32
|
+
|
30
33
|
# Useful info
|
31
34
|
request.remote_ip = rack_req.ip
|
32
35
|
request.user_agent = rack_req.user_agent
|
@@ -37,10 +40,13 @@ module Restfulness
|
|
37
40
|
request
|
38
41
|
end
|
39
42
|
|
40
|
-
|
43
|
+
# Given that we need to deal with the action early on, we handle the
|
44
|
+
# HTTP method override header here.
|
45
|
+
def parse_action(env, action)
|
46
|
+
action = (env['HTTP_X_HTTP_METHOD_OVERRIDE'] || action).strip.downcase
|
41
47
|
case action
|
42
|
-
when '
|
43
|
-
action.
|
48
|
+
when 'delete', 'get', 'head', 'post', 'put', 'patch', 'options'
|
49
|
+
action.to_sym
|
44
50
|
else
|
45
51
|
raise HTTPException.new(501)
|
46
52
|
end
|
data/lib/restfulness/request.rb
CHANGED
@@ -18,15 +18,6 @@ module Restfulness
|
|
18
18
|
# Ruby URI object
|
19
19
|
attr_reader :uri
|
20
20
|
|
21
|
-
# Path object of the current URL being accessed
|
22
|
-
attr_accessor :path
|
23
|
-
|
24
|
-
# The route, determined from the path, if available!
|
25
|
-
attr_accessor :route
|
26
|
-
|
27
|
-
# Query parameters included in the URL
|
28
|
-
attr_accessor :query
|
29
|
-
|
30
21
|
# Raw HTTP body, for POST and PUT requests.
|
31
22
|
attr_accessor :body
|
32
23
|
|
@@ -56,21 +47,32 @@ module Restfulness
|
|
56
47
|
end
|
57
48
|
|
58
49
|
def query
|
59
|
-
@query ||=
|
60
|
-
|
61
|
-
|
50
|
+
@query ||= ::Rack::Utils.parse_nested_query(uri.query).with_indifferent_access
|
51
|
+
end
|
52
|
+
|
53
|
+
def sanitized_query_string
|
54
|
+
@sanitized_query ||= uri.query ? Sanitizer.sanitize_query_string(uri.query) : ''
|
62
55
|
end
|
63
56
|
|
64
57
|
def params
|
65
58
|
return @params if @params || body.nil?
|
66
59
|
case headers[:content_type]
|
67
|
-
|
68
|
-
|
60
|
+
when /application\/json/
|
61
|
+
begin
|
62
|
+
@params = MultiJson.decode(body)
|
63
|
+
rescue MultiJson::LoadError
|
64
|
+
raise HTTPException.new(400)
|
65
|
+
end
|
69
66
|
else
|
70
67
|
raise HTTPException.new(406)
|
71
68
|
end
|
72
69
|
end
|
73
70
|
|
71
|
+
def sanitized_params
|
72
|
+
# Note: this returns nil if #params has not been called
|
73
|
+
@sanitized_params ||= @params ? Sanitizer.sanitize_hash(@params) : nil
|
74
|
+
end
|
75
|
+
|
74
76
|
[:get, :post, :put, :patch, :delete, :head, :options].each do |m|
|
75
77
|
define_method("#{m}?") do
|
76
78
|
action == m
|
data/lib/restfulness/response.rb
CHANGED
@@ -93,13 +93,13 @@ module Restfulness
|
|
93
93
|
|
94
94
|
resource_name = resource ? resource.class.to_s : 'Error'
|
95
95
|
# We're only interested in parsed parameters.
|
96
|
-
params = request.
|
96
|
+
params = request.sanitized_params
|
97
97
|
|
98
98
|
msg = %{%s "%s %s%s" %s %d %s %s %0.4fs %s} % [
|
99
99
|
request.remote_ip,
|
100
100
|
request.action.to_s.upcase,
|
101
101
|
uri.path,
|
102
|
-
uri.query ? "?#{
|
102
|
+
uri.query ? "?#{request.sanitized_query_string}" : '',
|
103
103
|
resource_name,
|
104
104
|
status.to_s[0..3],
|
105
105
|
STATUSES[status],
|
data/lib/restfulness/route.rb
CHANGED
@@ -0,0 +1,70 @@
|
|
1
|
+
module Restfulness
|
2
|
+
module Sanitizer
|
3
|
+
SANITIZED = 'FILTERED'.freeze
|
4
|
+
|
5
|
+
def self.sanitize_hash(arg)
|
6
|
+
@hash_sanitizer ||= Hash.new(Restfulness.sensitive_params)
|
7
|
+
@hash_sanitizer.sanitize(arg)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.sanitize_query_string(arg)
|
11
|
+
@query_string_sanitizer ||= QueryString.new(Restfulness.sensitive_params)
|
12
|
+
@query_string_sanitizer.sanitize(arg)
|
13
|
+
end
|
14
|
+
|
15
|
+
class AbstractSanitizer
|
16
|
+
attr_reader :sensitive_params, :sensitive_param_matcher
|
17
|
+
|
18
|
+
def initialize(*sensitive_params)
|
19
|
+
@sensitive_params = [*sensitive_params].flatten.map(&:downcase)
|
20
|
+
@sensitive_param_matcher = Regexp.new("\\A#{@sensitive_params.join('|')}", Regexp::IGNORECASE)
|
21
|
+
end
|
22
|
+
|
23
|
+
def sensitive_param?(param)
|
24
|
+
sensitive_param_matcher === param.to_s
|
25
|
+
end
|
26
|
+
|
27
|
+
def sanitize(arg)
|
28
|
+
raise 'not implemented'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Clean a hash of sensitive data. Works on nested hashes
|
33
|
+
class Hash < AbstractSanitizer
|
34
|
+
def sanitize(h)
|
35
|
+
return h if sensitive_params.empty? || h.empty?
|
36
|
+
duplicate = h.dup
|
37
|
+
duplicate.each_pair do |k, v|
|
38
|
+
duplicate[k] = if sensitive_param?(k)
|
39
|
+
SANITIZED
|
40
|
+
elsif v.is_a?(::Hash)
|
41
|
+
sanitize(v)
|
42
|
+
else
|
43
|
+
v
|
44
|
+
end
|
45
|
+
end
|
46
|
+
duplicate
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Clean a query string of sensitive data
|
51
|
+
class QueryString < AbstractSanitizer
|
52
|
+
PARSER = /
|
53
|
+
([^&;=]+?) # param key
|
54
|
+
(\[.*?\])? # optionally a nested param, ie key[9]
|
55
|
+
= # divider
|
56
|
+
([^&;=]+) # param value
|
57
|
+
/x
|
58
|
+
def sanitize(qs)
|
59
|
+
return qs if sensitive_params.empty? || qs.length == 0
|
60
|
+
qs.gsub(PARSER) do |query_param|
|
61
|
+
if sensitive_param?($1)
|
62
|
+
"#{$1}#{$2}=#{SANITIZED}"
|
63
|
+
else
|
64
|
+
query_param
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/restfulness/version.rb
CHANGED
@@ -29,7 +29,7 @@ describe Restfulness::Dispatchers::Rack do
|
|
29
29
|
{
|
30
30
|
'REQUEST_METHOD' => 'GET',
|
31
31
|
'SCRIPT_NAME' => '',
|
32
|
-
'PATH_INFO' => '/projects',
|
32
|
+
'PATH_INFO' => '/projects?query=test',
|
33
33
|
'QUERY_STRING' => '',
|
34
34
|
'SERVER_NAME' => 'localhost',
|
35
35
|
'SERVER_PORT' => '3000',
|
@@ -59,17 +59,27 @@ describe Restfulness::Dispatchers::Rack do
|
|
59
59
|
it "should convert main actions to symbols" do
|
60
60
|
actions = ['DELETE', 'GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'OPTIONS']
|
61
61
|
actions.each do |action|
|
62
|
-
val = obj.send(:parse_action, action)
|
62
|
+
val = obj.send(:parse_action, env, action)
|
63
63
|
val.should eql(action.downcase.to_sym)
|
64
64
|
end
|
65
65
|
end
|
66
66
|
|
67
67
|
it "should raise error if action unrecognised" do
|
68
68
|
expect {
|
69
|
-
obj.send(:parse_action, 'FOOO')
|
69
|
+
obj.send(:parse_action, env, 'FOOO')
|
70
70
|
}.to raise_error(Restfulness::HTTPException)
|
71
71
|
end
|
72
72
|
|
73
|
+
it "should override the action if the override header is present" do
|
74
|
+
env['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'PATCH'
|
75
|
+
obj.send(:parse_action, env, 'POST').should eql(:patch)
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should handle junk in action override header" do
|
79
|
+
env['HTTP_X_HTTP_METHOD_OVERRIDE'] = ' PatCH '
|
80
|
+
obj.send(:parse_action, env, 'POST').should eql(:patch)
|
81
|
+
end
|
82
|
+
|
73
83
|
end
|
74
84
|
|
75
85
|
describe "#prepare_headers (protected)" do
|
@@ -88,15 +98,33 @@ describe Restfulness::Dispatchers::Rack do
|
|
88
98
|
|
89
99
|
req.uri.should be_a(URI)
|
90
100
|
req.action.should eql(:get)
|
91
|
-
req.query.should be_empty
|
92
101
|
req.body.should be_nil
|
93
102
|
req.headers.keys.should include(:x_auth_token)
|
94
103
|
req.remote_ip.should eql('192.168.1.23')
|
95
104
|
req.user_agent.should eql('Some Navigator')
|
96
105
|
|
106
|
+
req.query.should_not be_empty
|
107
|
+
req.query[:query].should eql('test')
|
108
|
+
|
97
109
|
req.headers[:content_type].should eql('application/json')
|
98
110
|
end
|
99
111
|
|
112
|
+
it "should handle the body stringio" do
|
113
|
+
env['rack.input'] = StringIO.new("Some String")
|
114
|
+
|
115
|
+
req = obj.send(:prepare_request, env)
|
116
|
+
req.body.read.should eql('Some String')
|
117
|
+
end
|
118
|
+
|
119
|
+
it "should rewind the body stringio" do
|
120
|
+
env['rack.input'] = StringIO.new("Some String")
|
121
|
+
env['rack.input'].read
|
122
|
+
|
123
|
+
req = obj.send(:prepare_request, env)
|
124
|
+
req.body.read.should eql('Some String')
|
125
|
+
end
|
126
|
+
|
127
|
+
|
100
128
|
end
|
101
129
|
|
102
130
|
end
|
data/spec/unit/request_spec.rb
CHANGED
@@ -40,6 +40,13 @@ describe Restfulness::Request do
|
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
|
+
describe "#action" do
|
44
|
+
it "should provide basic action" do
|
45
|
+
obj.action = :get
|
46
|
+
obj.action.should eql(:get)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
43
50
|
describe "#path" do
|
44
51
|
it "should be nil if there is no route" do
|
45
52
|
obj.stub(:route).and_return(nil)
|
@@ -94,18 +101,52 @@ describe Restfulness::Request do
|
|
94
101
|
end
|
95
102
|
end
|
96
103
|
|
104
|
+
describe "#sanitized_query_string" do
|
105
|
+
it "should be empty if no query" do
|
106
|
+
obj.uri = "https://example.com/project/12345"
|
107
|
+
obj.sanitized_query_string.should be_empty
|
108
|
+
end
|
109
|
+
it "should filter out bad keys" do # See sanitizer tests for more
|
110
|
+
obj.uri = "https://example.com/project/12345?foo=bar&password=safe"
|
111
|
+
obj.sanitized_query_string.should match(/foo=bar/)
|
112
|
+
obj.sanitized_query_string.should_not match(/password=safe/)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
97
116
|
describe "#params" do
|
98
117
|
it "should not return anything for empty body" do
|
99
118
|
obj.stub(:body).and_return(nil)
|
100
119
|
obj.params.should be_nil
|
101
120
|
end
|
121
|
+
|
122
|
+
it "should raise 400 bad request for invalid json body" do
|
123
|
+
obj.headers[:content_type] = "application/json; charset=utf-8"
|
124
|
+
obj.stub(:body).and_return("invalidjson!")
|
125
|
+
expect {
|
126
|
+
obj.params
|
127
|
+
}.to raise_error(Restfulness::HTTPException, "Bad Request"){ |exception|
|
128
|
+
expect(exception.status).to eq 400
|
129
|
+
}
|
130
|
+
end
|
131
|
+
|
102
132
|
it "should raise 406 error if no content type" do
|
103
133
|
obj.headers[:content_type] = nil
|
104
134
|
obj.body = "{\"foo\":\"bar\"}"
|
105
135
|
expect {
|
106
136
|
obj.params
|
107
|
-
}.to raise_error(Restfulness::HTTPException, "Not Acceptable")
|
137
|
+
}.to raise_error(Restfulness::HTTPException, "Not Acceptable"){ |exception|
|
138
|
+
expect(exception.status).to eq 406
|
139
|
+
}
|
108
140
|
end
|
141
|
+
|
142
|
+
it "should decode a JSON body with utf-8 encoding" do
|
143
|
+
obj.headers[:content_type] = "application/json; charset=utf-8"
|
144
|
+
obj.body = "{\"foo\":\"bar\"}"
|
145
|
+
expect {
|
146
|
+
obj.params
|
147
|
+
}.not_to raise_error
|
148
|
+
end
|
149
|
+
|
109
150
|
it "should decode a JSON body" do
|
110
151
|
obj.headers[:content_type] = "application/json"
|
111
152
|
obj.body = "{\"foo\":\"bar\"}"
|
@@ -113,6 +154,21 @@ describe Restfulness::Request do
|
|
113
154
|
end
|
114
155
|
end
|
115
156
|
|
157
|
+
describe "#sanitized_params" do
|
158
|
+
it "should provide nil if the params hash has not been used" do
|
159
|
+
obj.stub(:body).and_return(nil)
|
160
|
+
obj.sanitized_params.should be_nil
|
161
|
+
end
|
162
|
+
it "should provide santized params if params have been used" do
|
163
|
+
obj.headers[:content_type] = "application/json"
|
164
|
+
obj.body = "{\"foo\":\"bar\",\"password\":\"safe\"}"
|
165
|
+
obj.params['password'].should eql('safe')
|
166
|
+
obj.sanitized_params['foo'].should eql('bar')
|
167
|
+
obj.sanitized_params['password'].should_not be_blank
|
168
|
+
obj.sanitized_params['password'].should_not eql('safe')
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
116
172
|
describe "method helpers" do
|
117
173
|
it "should respond to method questions" do
|
118
174
|
[:get?, :post?, :put?, :delete?, :head?, :options?].each do |q|
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Restfulness::Sanitizer do
|
4
|
+
describe 'Hash' do
|
5
|
+
it 'does nothing when not given any sensitive params' do
|
6
|
+
subject = described_class::Hash.new()
|
7
|
+
input = {:password => 'ok', :nested => {:password => 'okay'}}
|
8
|
+
subject.sanitize(input).should eq input
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'filters sensitive param and not others' do
|
12
|
+
subject = described_class::Hash.new(:password)
|
13
|
+
input = {:PASSword => 'supersecret', :user => 'billy'}
|
14
|
+
subject.sanitize(input).should eq({:PASSword => described_class::SANITIZED, :user => 'billy'})
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'filters nested sensitive params and not others' do
|
18
|
+
subject = described_class::Hash.new(:password)
|
19
|
+
input = {:user => {:passWORD => 'supersecret', :user => 'billy'}}
|
20
|
+
subject.sanitize(input).should eq({:user => {:passWORD => described_class::SANITIZED, :user => 'billy'}})
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'filters any parameter beginning with sensitive params (prefix)' do
|
24
|
+
subject = described_class::Hash.new(:password)
|
25
|
+
input = {:user => {:passWORD_confirmation => 'supersecret', :user => 'billy'}}
|
26
|
+
subject.sanitize(input).should eq({:user => {:passWORD_confirmation => described_class::SANITIZED, :user => 'billy'}})
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe 'QueryString' do
|
31
|
+
it 'does nothing when not given any sensitive params' do
|
32
|
+
subject = described_class::QueryString.new()
|
33
|
+
input = 'password=ok&other=false'
|
34
|
+
subject.sanitize(input).should eq input
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'filters sensitive param and not others' do
|
38
|
+
subject = described_class::QueryString.new(:password)
|
39
|
+
input = 'PASSword=ok&other=false'
|
40
|
+
subject.sanitize(input).should eq "PASSword=#{described_class::SANITIZED}&other=false"
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'filters nested (with index) sensitive params and not others' do
|
44
|
+
subject = described_class::QueryString.new(:password)
|
45
|
+
input = 'password[0]=what&PASSword[1]=secret&other=false'
|
46
|
+
subject.sanitize(input).should eq "password[0]=#{described_class::SANITIZED}&PASSword[1]=#{described_class::SANITIZED}&other=false"
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'filters nested (no index) sensitive params and not others' do
|
50
|
+
subject = described_class::QueryString.new(:password)
|
51
|
+
input = 'password[]=what&password[]=secret&other=false'
|
52
|
+
subject.sanitize(input).should eq "password[]=#{described_class::SANITIZED}&password[]=#{described_class::SANITIZED}&other=false"
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'filters any parameter beginning with sensitive params (prefix)' do
|
56
|
+
subject = described_class::QueryString.new(:password)
|
57
|
+
input = 'password_confirmation[]=what&password[]=secret&password=false'
|
58
|
+
subject.sanitize(input).should eq "password_confirmation[]=#{described_class::SANITIZED}&password[]=#{described_class::SANITIZED}&password=#{described_class::SANITIZED}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: restfulness
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sam Lown
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2014-02-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -123,6 +123,7 @@ files:
|
|
123
123
|
- lib/restfulness/response.rb
|
124
124
|
- lib/restfulness/route.rb
|
125
125
|
- lib/restfulness/router.rb
|
126
|
+
- lib/restfulness/sanitizer.rb
|
126
127
|
- lib/restfulness/statuses.rb
|
127
128
|
- lib/restfulness/version.rb
|
128
129
|
- restfulness.gemspec
|
@@ -138,6 +139,7 @@ files:
|
|
138
139
|
- spec/unit/response_spec.rb
|
139
140
|
- spec/unit/route_spec.rb
|
140
141
|
- spec/unit/router_spec.rb
|
142
|
+
- spec/unit/sanitizer_spec.rb
|
141
143
|
homepage: https://github.com/samlown/restfulness
|
142
144
|
licenses:
|
143
145
|
- MIT
|
@@ -175,3 +177,4 @@ test_files:
|
|
175
177
|
- spec/unit/response_spec.rb
|
176
178
|
- spec/unit/route_spec.rb
|
177
179
|
- spec/unit/router_spec.rb
|
180
|
+
- spec/unit/sanitizer_spec.rb
|