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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5c7504fc661a80914cd4f3e3ad81918799de7b0b
4
- data.tar.gz: a6b3d22e6fc9c30ba6010043550d37e3c651be0a
3
+ metadata.gz: fec45922d74ec00c62042cfa323627cb5e23d925
4
+ data.tar.gz: 87679a0dd96e313a113f24e85e35c4130285cb01
5
5
  SHA512:
6
- metadata.gz: d21da3dc7f1db2dd8b1259c36e710aa31698f31f5b39b37af0ecca2abc433fa15833941359efabcd9d74681b0570e63a1f4c651d0fb309b1b62c2fc656eb031f
7
- data.tar.gz: e7fe0be99de3fcf1947e5fc75bfe2fce76bbd629e9b5cdf3b0370db2ae9e6a1f08a4353130392c3aa83770f37f0c00c0d67e67c4c024046eac142b6897b0ab68
6
+ metadata.gz: a818c4417a58a9a84fe108b5a0c0a57e02f6342ccd47810e571e6d2f6d49ec0c736a111a1ac9b82ec1a67eeecc644908af1fbb869cf0f67f4a31335112e44fb6
7
+ data.tar.gz: 568d63dc34ffa86bd6e8f59c5d278ac54426ad7b95648c79252fd1475c93c80cd17611245f8d2e7d926dda22bdc5fc9bad929249cc4cef5aa2e62a9842a35057
data/.gitignore CHANGED
@@ -1,6 +1,7 @@
1
1
  *.gem
2
2
  *.rbc
3
3
  .bundle
4
+ .idea
4
5
  .config
5
6
  .yardoc
6
7
  Gemfile.lock
data/.travis.yml CHANGED
@@ -3,4 +3,4 @@ rvm:
3
3
  - 2.0.0
4
4
  - 1.9.3
5
5
  - jruby-19mode # JRuby in 1.9 mode
6
- - rbx-19mode
6
+ - rbx
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
- The easiest method is probably just to update the `response` code. Take the following example where we set a 403 response and the model's errors object in the payload:
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!(project.errors)
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 more complete result, or even just resort to a simple string.
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! Failing that, you can create your own by writing something along the lines of:
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
- ## Caveats and TODOs
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
- Restfulness is still very much a work in progress. Here is a list of things that we'd like to improve or fix:
400
+ ### The Rails Way
382
401
 
383
- * Support for more serializers and content types, not just JSON.
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
- ## Reloading
404
+ The recomended approach is to create two directories in your Rails projects `/app` path:
391
405
 
392
- Reloading is complicated. Unfortunately we're all used to the way Rails projects magically reload changed files so you don't have to restart the server after each change.
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
- If you're using Restfulness as a standalone project, we recommend using a rack extension like [Shotgun](https://github.com/rtomayko/shotgun).
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
- If you're adding Restfulness to a Rails project, you can take advantage of the `ActionDispatch::Reloader` rack middleware. Simply include it in the application definition:
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
- class MyAPI < Restfulness::Application
419
+ YourRailsApp::Application.routes.draw do
420
+
421
+ # Autoreload the API in development
400
422
  if Rails.env.development?
401
- middlewares << ActionDispatch::Reloader
423
+ mount Api.new => '/api'
402
424
  end
403
- routes do
404
- # etc. etc.
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
- We're still working on ways to improve this. If you have any ideas, please send me a pull request!
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
- def parse_action(action)
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 'DELETE', 'GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'OPTIONS'
43
- action.downcase.to_sym
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
@@ -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 ||= HashWithIndifferentAccess.new(
60
- ::Rack::Utils.parse_nested_query(uri.query)
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
- when 'application/json'
68
- @params = MultiJson.decode(body)
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
@@ -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.instance_variable_get(:@params)
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 ? "?#{uri.query}" : '',
102
+ uri.query ? "?#{request.sanitized_query_string}" : '',
103
103
  resource_name,
104
104
  status.to_s[0..3],
105
105
  STATUSES[status],
@@ -2,7 +2,7 @@ module Restfulness
2
2
 
3
3
  class Route
4
4
 
5
- # The path array of eliments, :id always on end!
5
+ # The path array of elements, :id always on end!
6
6
  attr_accessor :path
7
7
 
8
8
  # Reference to the class that will handle requests for this route
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Restfulness
2
- VERSION = "0.2.2"
2
+ VERSION = "0.2.3"
3
3
  end
@@ -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
@@ -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.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: 2013-10-31 00:00:00.000000000 Z
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