jduff-api-throttling 0.1.1 → 0.2.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.
data/Rakefile CHANGED
@@ -10,6 +10,7 @@ begin
10
10
  gemspec.homepage = "http://github.com/jduff/api-throttling/tree"
11
11
  gemspec.description = "TODO"
12
12
  gemspec.authors = ["Luc Castera", "John Duff"]
13
+ gemspec.add_development_dependency('context')
13
14
  end
14
15
  rescue LoadError
15
16
  puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  :major: 0
3
- :minor: 1
4
- :patch: 1
3
+ :minor: 2
4
+ :patch: 0
@@ -5,34 +5,46 @@ require File.expand_path(File.dirname(__FILE__) + '/handlers/handlers')
5
5
  class ApiThrottling
6
6
  def initialize(app, options={})
7
7
  @app = app
8
- @options = {:requests_per_hour => 60, :cache=>:redis}.merge(options)
8
+ @options = {:requests_per_hour => 60, :cache=>:redis, :auth=>true}.merge(options)
9
9
  @handler = Handlers.cache_handler_for(@options[:cache])
10
10
  raise "Sorry, we couldn't find a handler for the cache you specified: #{@options[:cache]}" unless @handler
11
11
  end
12
12
 
13
13
  def call(env, options={})
14
- auth = Rack::Auth::Basic::Request.new(env)
15
- if auth.provided?
14
+ if @options[:auth]
15
+ auth = Rack::Auth::Basic::Request.new(env)
16
+ return auth_required unless auth.provided?
16
17
  return bad_request unless auth.basic?
17
- begin
18
- cache = @handler.new(@options[:cache])
19
- key = "#{auth.username}_#{Time.now.strftime("%Y-%m-%d-%H")}"
20
- cache.increment(key)
21
- return over_rate_limit if cache.get(key).to_i > @options[:requests_per_hour]
22
- rescue Errno::ECONNREFUSED
23
- # If Redis-server is not running, instead of throwing an error, we simply do not throttle the API
24
- # It's better if your service is up and running but not throttling API, then to have it throw errors for all users
25
- # Make sure you monitor your redis-server so that it's never down. monit is a great tool for that.
26
- end
27
18
  end
19
+
20
+ begin
21
+ cache = @handler.new(@options[:cache])
22
+ key = generate_key(env, auth)
23
+ cache.increment(key)
24
+ return over_rate_limit if cache.get(key).to_i > @options[:requests_per_hour]
25
+ rescue Errno::ECONNREFUSED
26
+ # If Redis-server is not running, instead of throwing an error, we simply do not throttle the API
27
+ # It's better if your service is up and running but not throttling API, then to have it throw errors for all users
28
+ # Make sure you monitor your redis-server so that it's never down. monit is a great tool for that.
29
+ end
28
30
  @app.call(env)
29
31
  end
30
32
 
33
+ def generate_key(env, auth)
34
+ return @options[:key].call(env, auth) if @options[:key]
35
+ auth ? "#{auth.username}_#{Time.now.strftime("%Y-%m-%d-%H")}" : "#{Time.now.strftime("%Y-%m-%d-%H")}"
36
+ end
37
+
31
38
  def bad_request
32
39
  body_text = "Bad Request"
33
40
  [ 400, { 'Content-Type' => 'text/plain', 'Content-Length' => body_text.size.to_s }, [body_text] ]
34
41
  end
35
42
 
43
+ def auth_required
44
+ body_text = "Authorization Required"
45
+ [ 401, { 'Content-Type' => 'text/plain', 'Content-Length' => body_text.size.to_s }, [body_text] ]
46
+ end
47
+
36
48
  def over_rate_limit
37
49
  body_text = "Over Rate Limit"
38
50
  retry_after_in_seconds = (60 - Time.now.min) * 60
@@ -7,33 +7,89 @@ require 'redis'
7
7
 
8
8
  class ApiThrottlingTest < Test::Unit::TestCase
9
9
  include Rack::Test::Methods
10
- include BasicTests
11
-
12
- def app
13
- app = Rack::Builder.new {
14
- use ApiThrottling, :requests_per_hour => 3
15
- run lambda {|env| [200, {'Content-Type' => 'text/plain', 'Content-Length' => '12'}, ["Hello World!"] ] }
16
- }
17
- end
18
10
 
19
- def setup
20
- # Delete all the keys for 'joe' in Redis so that every test starts fresh
21
- # Having this here also helps as a reminder to start redis-server
22
- begin
23
- r = Redis.new
24
- r.keys("joe*").each do |key|
25
- r.delete key
26
- end
27
- r.keys("luc*").each do |key|
28
- r.delete key
29
- end
30
- rescue Errno::ECONNREFUSED
31
- assert false, "You need to start redis-server"
11
+ context "using redis" do
12
+ before do
13
+ # Delete all the keys for 'joe' in Redis so that every test starts fresh
14
+ # Having this here also helps as a reminder to start redis-server
15
+ begin
16
+ r = Redis.new
17
+ r.keys("*").each do |key|
18
+ r.delete key
19
+ end
20
+
21
+ rescue Errno::ECONNREFUSED
22
+ assert false, "You need to start redis-server"
23
+ end
24
+ end
25
+
26
+ context "with authentication required" do
27
+ include BasicTests
28
+
29
+ def app
30
+ app = Rack::Builder.new {
31
+ use ApiThrottling, :requests_per_hour => 3
32
+ run lambda {|env| [200, {'Content-Type' => 'text/plain', 'Content-Length' => '12'}, ["Hello World!"] ] }
33
+ }
34
+ end
35
+
36
+ def test_cache_handler_should_be_redis
37
+ assert_equal "Handlers::RedisHandler", app.to_app.instance_variable_get(:@handler).to_s
38
+ end
39
+
40
+ end
41
+
42
+ context "without authentication required" do
43
+ def app
44
+ app = Rack::Builder.new {
45
+ use ApiThrottling, :requests_per_hour => 3, :auth=>false
46
+ run lambda {|env| [200, {'Content-Type' => 'text/plain', 'Content-Length' => '12'}, ["Hello World!"] ] }
47
+ }
48
+ end
49
+
50
+ def test_should_not_require_authorization
51
+ 3.times do
52
+ get '/'
53
+ assert_equal 200, last_response.status
54
+ end
55
+ get '/'
56
+ assert_equal 503, last_response.status
57
+ end
58
+ end
59
+
60
+ context "with rate limit key based on url" do
61
+ def app
62
+ app = Rack::Builder.new {
63
+ use ApiThrottling, :requests_per_hour => 3,
64
+ :key=>Proc.new{ |env,auth| "#{auth.username}_#{env['PATH_INFO']}_#{Time.now.strftime("%Y-%m-%d-%H")}" }
65
+ run lambda {|env| [200, {'Content-Type' => 'text/plain', 'Content-Length' => '12'}, ["Hello World!"] ] }
66
+ }
67
+ end
68
+
69
+ test "should throttle requests based on the user and url called" do
70
+ authorize "joe", "secret"
71
+ 3.times do
72
+ get '/'
73
+ assert_equal 200, last_response.status
74
+ end
75
+ get '/'
76
+ assert_equal 503, last_response.status
77
+
78
+ 3.times do
79
+ get '/awesome'
80
+ assert_equal 200, last_response.status
81
+ end
82
+ get '/awesome'
83
+ assert_equal 503, last_response.status
84
+
85
+ authorize "luc", "secret"
86
+ get '/awesome'
87
+ assert_equal 200, last_response.status
88
+
89
+ get '/'
90
+ assert_equal 200, last_response.status
91
+ end
32
92
  end
33
- end
34
-
35
- def test_cache_handler_should_be_redis
36
- assert_equal "Handlers::RedisHandler", app.to_app.instance_variable_get(:@handler).to_s
37
93
  end
38
94
 
39
95
  end
@@ -7,7 +7,7 @@ class TestApiThrottlingHash < Test::Unit::TestCase
7
7
 
8
8
  def app
9
9
  app = Rack::Builder.new {
10
- use ApiThrottling, :requests_per_hour => 3, :cache => HASH, :read_method=>"get", :write_method=>"add"
10
+ use ApiThrottling, :requests_per_hour => 3, :cache => HASH
11
11
  run lambda {|env| [200, {'Content-Type' => 'text/plain', 'Content-Length' => '12'}, ["Hello World!"] ] }
12
12
  }
13
13
  end
@@ -8,7 +8,7 @@ class TestApiThrottlingMemcache < Test::Unit::TestCase
8
8
 
9
9
  def app
10
10
  app = Rack::Builder.new {
11
- use ApiThrottling, :requests_per_hour => 3, :cache => CACHE, :read_method=>"get", :write_method=>"add"
11
+ use ApiThrottling, :requests_per_hour => 3, :cache => CACHE
12
12
  run lambda {|env| [200, {'Content-Type' => 'text/plain', 'Content-Length' => '12'}, ["Hello World!"] ] }
13
13
  }
14
14
  end
data/test/test_helper.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'rubygems'
2
2
  require 'rack/test'
3
3
  require 'test/unit'
4
+ require 'context'
4
5
  require File.expand_path(File.dirname(__FILE__) + '/../lib/api_throttling')
5
6
 
6
7
  # this way we can include the module for any of the handler tests
@@ -14,25 +15,17 @@ module BasicTests
14
15
 
15
16
  def test_fourth_request_should_be_blocked
16
17
  authorize "joe", "secret"
17
- get '/'
18
- assert_equal 200, last_response.status
19
- get '/'
20
- assert_equal 200, last_response.status
21
- get '/'
22
- assert_equal 200, last_response.status
23
- get '/'
24
- assert_equal 503, last_response.status
18
+ 3.times do
19
+ get '/'
20
+ assert_equal 200, last_response.status
21
+ end
25
22
  get '/'
26
23
  assert_equal 503, last_response.status
27
24
  end
28
25
 
29
26
  def test_over_rate_limit_should_only_apply_to_user_that_went_over_the_limit
30
27
  authorize "joe", "secret"
31
- get '/'
32
- get '/'
33
- get '/'
34
- get '/'
35
- get '/'
28
+ 5.times { get '/' }
36
29
  assert_equal 503, last_response.status
37
30
  authorize "luc", "secret"
38
31
  get '/'
@@ -41,21 +34,21 @@ module BasicTests
41
34
 
42
35
  def test_over_rate_limit_should_return_a_retry_after_header
43
36
  authorize "joe", "secret"
44
- get '/'
45
- get '/'
46
- get '/'
47
- get '/'
37
+ 4.times { get '/' }
48
38
  assert_equal 503, last_response.status
49
39
  assert_not_nil last_response.headers['Retry-After']
50
40
  end
51
41
 
52
42
  def test_retry_after_should_be_less_than_60_minutes
53
43
  authorize "joe", "secret"
54
- get '/'
55
- get '/'
56
- get '/'
57
- get '/'
44
+ 4.times { get '/' }
58
45
  assert_equal 503, last_response.status
59
46
  assert last_response.headers['Retry-After'].to_i <= (60 * 60)
60
47
  end
48
+
49
+ def test_should_require_authorization
50
+ get '/'
51
+ assert_equal 401, last_response.status
52
+ end
53
+
61
54
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jduff-api-throttling
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Luc Castera
@@ -10,10 +10,19 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2009-07-03 00:00:00 -07:00
13
+ date: 2009-07-06 00:00:00 -07:00
14
14
  default_executable:
15
- dependencies: []
16
-
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: context
18
+ type: :development
19
+ version_requirement:
20
+ version_requirements: !ruby/object:Gem::Requirement
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: "0"
25
+ version:
17
26
  description: TODO
18
27
  email: duff.john@gmail.com
19
28
  executables: []