prop 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,3 +1,3 @@
1
- source "http://rubygems.org"
1
+ source 'https://rubygems.org'
2
2
 
3
3
  gemspec
data/README.md CHANGED
@@ -3,73 +3,95 @@
3
3
 
4
4
  Prop is a simple gem for rate limiting requests of any kind. It allows you to configure hooks for registering certain actions, such that you can define thresholds, register usage and finally act on exceptions once thresholds get exceeded.
5
5
 
6
- Prop uses and interval to define a window of time using simple div arithmetic. This means that it's a worst case throttle that will allow up to 2 times the specified requests within the specified interval.
6
+ Prop uses an interval to define a window of time using simple div arithmetic. This means that it's a worst-case throttle that will allow up to two times the specified requests within the specified interval.
7
7
 
8
- To get going with Prop you first define the read and write operations. These define how you write a registered request and how to read the number of requests for a given action. For example do something like the below in a Rails initializer:
8
+ To get going with Prop, you first define the read and write operations. These define how you write a registered request and how to read the number of requests for a given action. For example, do something like the below in a Rails initializer:
9
9
 
10
- Prop.read do |key|
11
- Rails.cache.read(key)
12
- end
10
+ ```ruby
11
+ Prop.read do |key|
12
+ Rails.cache.read(key)
13
+ end
13
14
 
14
- Prop.write do |key, value|
15
- Rails.cache.write(key, value)
16
- end
15
+ Prop.write do |key, value|
16
+ Rails.cache.write(key, value)
17
+ end
18
+ ```
17
19
 
18
20
  You can choose to rely on whatever you'd like to use for transient storage. Prop does not do any sort of clean up of its key space, so you would have to implement that manually should you be using anything but an LRU cache like memcached.
19
21
 
22
+ ## Setting a Callback
23
+
24
+ You can define an optional callback that is invoked when a rate limit is reached. In a Rails application you could use such a handler to add notification support:
25
+
26
+ ```ruby
27
+ Prop.before_throttle do |handle, key, threshold, interval|
28
+ ActiveSupport::Notifications.instrument('throttle.prop', handle: handle, key: key, threshold: threshold, interval: interval)
29
+ end
30
+ ```
31
+
20
32
  ## Defining thresholds
21
33
 
22
- Once the read and write operations are defined, you can optionally define thresholds. If for example, you want to have a threshold on accepted emails per hour from a given user, you could define a threshold and interval (in seconds) for this like so:
34
+ Once the read and write operations are defined, you can optionally define thresholds. If, for example, you want to have a threshold on accepted emails per hour from a given user, you could define a threshold and interval (in seconds) for this like so:
23
35
 
24
- Prop.configure(:mails_per_hour, :threshold => 100, :interval => 1.hour, :description => "Mail rate limit exceeded")
36
+ ```ruby
37
+ Prop.configure(:mails_per_hour, :threshold => 100, :interval => 1.hour, :description => "Mail rate limit exceeded")
38
+ ```
25
39
 
26
- The `:mails_per_hour` in the above is called the "handle". You can now put the throttle to work with this values, by passing the handle to the respective methods in Prop:
40
+ The `:mails_per_hour` in the above is called the "handle". You can now put the throttle to work with these values, by passing the handle to the respective methods in Prop:
27
41
 
28
- # Throws Prop::RateLimitExceededError if the threshold/interval has been reached
29
- Prop.throttle!(:mails_per_hour)
42
+ ```ruby
43
+ # Throws Prop::RateLimitExceededError if the threshold/interval has been reached
44
+ Prop.throttle!(:mails_per_hour)
30
45
 
31
- # Prop can be used to guard a block of code
32
- Prop.throttle!(:expensive_request) { calculator.something_very_hard }
46
+ # Prop can be used to guard a block of code
47
+ Prop.throttle!(:expensive_request) { calculator.something_very_hard }
33
48
 
34
- # Returns true if the threshold/interval has been reached
35
- Prop.throttled?(:mails_per_hour)
49
+ # Returns true if the threshold/interval has been reached
50
+ Prop.throttled?(:mails_per_hour)
36
51
 
37
- # Sets the throttle "count" to 0
38
- Prop.reset(:mails_per_hour)
52
+ # Sets the throttle "count" to 0
53
+ Prop.reset(:mails_per_hour)
39
54
 
40
- # Returns the value of this throttle, usually a count, but see below for more
41
- Prop.count(:mails_per_hour)
55
+ # Returns the value of this throttle, usually a count, but see below for more
56
+ Prop.count(:mails_per_hour)
57
+ ```
42
58
 
43
- Prop will raise a RuntimeError if you attempt to operate on an undefined handle.
59
+ Prop will raise a `RuntimeError` if you attempt to operate on an undefined handle.
44
60
 
45
61
  ## Scoping a throttle
46
62
 
47
- In many cases you will want to tie a specific key to a defined throttle, for example you can scope the throttling to a specific sender rather than running a global "mails per hour" throttle:
63
+ In many cases you will want to tie a specific key to a defined throttle. For example, you can scope the throttling to a specific sender rather than running a global "mails per hour" throttle:
48
64
 
49
- Prop.throttle!(:mails_per_hour, mail.from)
50
- Prop.throttled?(:mails_per_hour, mail.from)
51
- Prop.reset(:mails_per_hour, mail.from)
52
- Prop.query(:mails_per_hour, mail.from)
65
+ ```ruby
66
+ Prop.throttle!(:mails_per_hour, mail.from)
67
+ Prop.throttled?(:mails_per_hour, mail.from)
68
+ Prop.reset(:mails_per_hour, mail.from)
69
+ Prop.query(:mails_per_hour, mail.from)
70
+ ```
53
71
 
54
72
  The throttle scope can also be an array of values, e.g.:
55
73
 
56
- Prop.throttle!(:mails_per_hour, [ account.id, mail.from ])
74
+ ```ruby
75
+ Prop.throttle!(:mails_per_hour, [ account.id, mail.from ])
76
+ ```
57
77
 
58
78
  ## Error handling
59
79
 
60
- If the throttle! method gets called more than "threshold" times within "interval in seconds" for a given handle and key combination, Prop throws a Prop::RateLimited error which is a subclass of StandardError. This exception contains a "handle" reference and a "description" if specified during the configuration. The handle allows you to rescue Prop::RateLimited and differentiate action depending on the handle. For example, in Rails you can use this in e.g. ApplicationController:
80
+ If the throttle! method gets called more than "threshold" times within "interval in seconds" for a given handle and key combination, Prop throws a `Prop::RateLimited` error which is a subclass of `StandardError`. This exception contains a "handle" reference and a "description" if specified during the configuration. The handle allows you to rescue `Prop::RateLimited` and differentiate action depending on the handle. For example, in Rails you can use this in e.g. `ApplicationController`:
61
81
 
62
- rescue_from Prop::RateLimited do |e|
63
- if e.handle == :authorization_attempt
64
- render :status => :forbidden, :message => I18n.t(e.description)
65
- elsif ...
82
+ ```ruby
83
+ rescue_from Prop::RateLimited do |e|
84
+ if e.handle == :authorization_attempt
85
+ render :status => :forbidden, :message => I18n.t(e.description)
86
+ elsif ...
66
87
 
67
- end
68
- end
88
+ end
89
+ end
90
+ ```
69
91
 
70
92
  ### Using the Middleware
71
93
 
72
- Prop ships with a built in Rack middleware that you can use to do all the exception handling. When a `Prop::RateLimited` error is caught, it will build an HTTP [429 Too Many Requests](http://tools.ietf.org/html/draft-nottingham-http-new-status-02#section-4) response and set the following headers:
94
+ Prop ships with a built-in Rack middleware that you can use to do all the exception handling. When a `Prop::RateLimited` error is caught, it will build an HTTP [429 Too Many Requests](http://tools.ietf.org/html/draft-nottingham-http-new-status-02#section-4) response and set the following headers:
73
95
 
74
96
  Retry-After: 32
75
97
  Content-Type: text/plain
@@ -79,14 +101,16 @@ Where `Retry-After` is the number of seconds the client has to wait before retry
79
101
 
80
102
  If you wish to do manual error messaging in these cases, you can define an error handler in your Prop configuration. Here's how the default error handler looks - you use anything that responds to `.call` and takes the environment and a `RateLimited` instance as argument:
81
103
 
82
- error_handler = Proc.new do |env, error|
83
- body = error.description || "This action has been rate limited"
84
- headers = { "Content-Type" => "text/plain", "Content-Length" => body.size, "Retry-After" => error.retry_after }
104
+ ```ruby
105
+ error_handler = Proc.new do |env, error|
106
+ body = error.description || "This action has been rate limited"
107
+ headers = { "Content-Type" => "text/plain", "Content-Length" => body.size, "Retry-After" => error.retry_after }
85
108
 
86
- [ 429, headers, [ body ]]
87
- end
109
+ [ 429, headers, [ body ]]
110
+ end
88
111
 
89
- ActionController::Dispatcher.middleware.insert_before(ActionController::ParamsParser, :error_handler => error_handler)
112
+ ActionController::Dispatcher.middleware.insert_before(ActionController::ParamsParser, :error_handler => error_handler)
113
+ ```
90
114
 
91
115
  An alternative to this, is to extend `Prop::Middleware` and override the `render_response(env, error)` method.
92
116
 
@@ -94,25 +118,33 @@ An alternative to this, is to extend `Prop::Middleware` and override the `render
94
118
 
95
119
  In case you need to perform e.g. a manual bulk operation:
96
120
 
97
- Prop.disabled do
98
- # No throttles will be tested here
99
- end
121
+ ```ruby
122
+ Prop.disabled do
123
+ # No throttles will be tested here
124
+ end
125
+ ```
100
126
 
101
127
  ## Threshold settings
102
128
 
103
129
  You can chose to override the threshold for a given key:
104
130
 
105
- Prop.throttle!(:mails_per_hour, mail.from, :threshold => current_account.mail_throttle_threshold)
131
+ ```ruby
132
+ Prop.throttle!(:mails_per_hour, mail.from, :threshold => current_account.mail_throttle_threshold)
133
+ ```
106
134
 
107
135
  When the threshold are invoked without argument, the key is nil and as such a scope of its own, i.e. these are equivalent:
108
136
 
109
- Prop.throttle!(:mails_per_hour)
110
- Prop.throttle!(:mails_per_hour, nil)
137
+ ```ruby
138
+ Prop.throttle!(:mails_per_hour)
139
+ Prop.throttle!(:mails_per_hour, nil)
140
+ ```
111
141
 
112
142
  The default (and smallest possible) increment is 1, you can set that to any integer value using :increment which is handy for building time based throttles:
113
143
 
114
- Prop.setup(:execute_time, :threshold => 10, :interval => 1.minute)
115
- Prop.throttle!(:execute_time, account.id, :increment => (Benchmark.realtime { execute }).to_i)
144
+ ```ruby
145
+ Prop.setup(:execute_time, :threshold => 10, :interval => 1.minute)
146
+ Prop.throttle!(:execute_time, account.id, :increment => (Benchmark.realtime { execute }).to_i)
147
+ ```
116
148
 
117
149
  ## License
118
150
 
data/lib/prop.rb CHANGED
@@ -7,7 +7,7 @@ module Prop
7
7
  # Short hand for accessing Prop::Limiter methods
8
8
  class << self
9
9
  extend Forwardable
10
- def_delegators :"Prop::Limiter", :read, :write, :configure, :disabled
10
+ def_delegators :"Prop::Limiter", :read, :write, :configure, :disabled, :before_throttle
11
11
  def_delegators :"Prop::Limiter", :throttle!, :throttled?, :count, :query, :reset
12
12
  end
13
13
  end
data/lib/prop/limiter.rb CHANGED
@@ -6,7 +6,7 @@ module Prop
6
6
  class Limiter
7
7
 
8
8
  class << self
9
- attr_accessor :handles, :reader, :writer
9
+ attr_accessor :handles, :reader, :writer, :before_throttle_callback
10
10
 
11
11
  def read(&blk)
12
12
  self.reader = blk
@@ -16,6 +16,10 @@ module Prop
16
16
  self.writer = blk
17
17
  end
18
18
 
19
+ def before_throttle(&blk)
20
+ self.before_throttle_callback = blk
21
+ end
22
+
19
23
  # Public: Registers a handle for rate limiting
20
24
  #
21
25
  # handle - the name of the handle you wish to use in your code, e.g. :login_attempt
@@ -55,6 +59,10 @@ module Prop
55
59
 
56
60
  unless disabled?
57
61
  if at_threshold?(counter, options[:threshold])
62
+ unless before_throttle_callback.nil?
63
+ before_throttle_callback.call(handle, key, options[:threshold], options[:interval])
64
+ end
65
+
58
66
  raise Prop::RateLimited.new(options.merge(:cache_key => cache_key, :handle => handle))
59
67
  else
60
68
  increment = options.key?(:increment) ? options[:increment].to_i : 1
data/prop.gemspec CHANGED
@@ -1,6 +1,6 @@
1
- Gem::Specification.new "prop", "1.0.0" do |s|
1
+ Gem::Specification.new "prop", "1.0.1" do |s|
2
2
  s.name = 'prop'
3
- s.version = '1.0.0'
3
+ s.version = '1.0.1'
4
4
  s.date = '2012-04-24'
5
5
  s.rubyforge_project = 'prop'
6
6
  s.license = "Apache License Version 2.0"
@@ -18,7 +18,7 @@ Gem::Specification.new "prop", "1.0.0" do |s|
18
18
 
19
19
  s.add_development_dependency('rake')
20
20
  s.add_development_dependency('bundler')
21
- s.add_development_dependency('shoulda')
21
+ s.add_development_dependency('minitest')
22
22
  s.add_development_dependency('mocha')
23
23
 
24
24
  s.files = `git ls-files`.split("\n")
data/test/helper.rb CHANGED
@@ -1,12 +1,13 @@
1
1
  require 'rubygems'
2
- require 'test/unit'
3
- require 'shoulda'
4
- require 'mocha'
2
+
3
+ require "minitest/spec"
4
+ require "minitest/mock"
5
+ require "minitest/autorun"
6
+ require 'mocha/setup'
7
+
5
8
  require 'time'
6
9
 
7
10
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
8
11
  $LOAD_PATH.unshift(File.dirname(__FILE__))
9
- require 'prop'
10
12
 
11
- class Test::Unit::TestCase
12
- end
13
+ require 'prop'
data/test/test_key.rb CHANGED
@@ -1,26 +1,24 @@
1
1
  require 'helper'
2
2
 
3
- class TestKey < Test::Unit::TestCase
4
-
5
- context Prop::Key do
6
- context "#build" do
7
- should "return a hexdigested key" do
8
- assert_match /prop\/[a-f0-9]+/, Prop::Key.build(:handle => :hello, :key => [ "foo", 2, :bar ], :interval => 60)
9
- end
3
+ describe Prop::Key do
4
+ describe "#build" do
5
+ it "return a hexdigested key" do
6
+ assert_match /prop\/[a-f0-9]+/, Prop::Key.build(:handle => :hello, :key => [ "foo", 2, :bar ], :interval => 60)
10
7
  end
8
+ end
11
9
 
12
- context "#normalize" do
13
- should "turn a Fixnum into a String" do
14
- assert_equal "3", Prop::Key.normalize(3)
15
- end
10
+ describe "#normalize" do
11
+ it "turn a Fixnum into a String" do
12
+ assert_equal "3", Prop::Key.normalize(3)
13
+ end
16
14
 
17
- should "return a String" do
18
- assert_equal "S", Prop::Key.normalize("S")
19
- end
15
+ it "return a String" do
16
+ assert_equal "S", Prop::Key.normalize("S")
17
+ end
20
18
 
21
- should "flatten and join an Array" do
22
- assert_equal "1/B/3", Prop::Key.normalize([ 1, "B", "3" ])
23
- end
19
+ it "flatten and join an Array" do
20
+ assert_equal "1/B/3", Prop::Key.normalize([ 1, "B", "3" ])
24
21
  end
25
22
  end
26
- end
23
+ end
24
+
data/test/test_limiter.rb CHANGED
@@ -1,90 +1,107 @@
1
1
  require 'helper'
2
2
 
3
- class TestLimiter < Test::Unit::TestCase
4
3
 
5
- context Prop::Limiter do
6
- setup do
7
- @store = {}
4
+ describe Prop::Limiter do
5
+ before do
6
+ @store = {}
8
7
 
9
- Prop::Limiter.read { |key| @store[key] }
10
- Prop::Limiter.write { |key, value| @store[key] = value }
11
- Prop::Limiter.configure(:something, :threshold => 10, :interval => 10)
8
+ Prop::Limiter.read { |key| @store[key] }
9
+ Prop::Limiter.write { |key, value| @store[key] = value }
10
+ Prop::Limiter.configure(:something, :threshold => 10, :interval => 10)
12
11
 
13
- @start = Time.now
14
- Time.stubs(:now).returns(@start)
15
- end
12
+ @start = Time.now
13
+ Time.stubs(:now).returns(@start)
14
+ end
16
15
 
17
- context "#throttle!" do
18
- setup do
19
- Prop.reset(:something)
20
- end
16
+ describe "#throttle!" do
17
+ before do
18
+ Prop.reset(:something)
19
+ end
21
20
 
22
- context "when disabled" do
23
- setup { Prop::Limiter.expects(:disabled?).returns(true) }
21
+ describe "when disabled" do
22
+ before { Prop::Limiter.expects(:disabled?).returns(true) }
24
23
 
25
- [ true, false ].each do |threshold_reached|
26
- context "and threshold has #{"not " unless threshold_reached}been reached" do
27
- setup { Prop::Limiter.stubs(:at_threshold?).returns(threshold_reached) }
24
+ [ true, false ].each do |threshold_reached|
25
+ describe "and threshold has #{"not " unless threshold_reached}been reached" do
26
+ before { Prop::Limiter.stubs(:at_threshold?).returns(threshold_reached) }
28
27
 
29
- context "given a block" do
30
- should "execute that block" do
31
- assert_equal "wibble", Prop.throttle!(:something) { "wibble" }
32
- end
28
+ describe "given a block" do
29
+ it "execute that block" do
30
+ assert_equal "wibble", Prop.throttle!(:something) { "wibble" }
33
31
  end
32
+ end
34
33
 
35
- context "not given a block" do
36
- should "return the current throttle count" do
37
- assert_equal Prop.count(:something), Prop.throttle!(:something)
38
- end
34
+ describe "not given a block" do
35
+ it "return the current throttle count" do
36
+ assert_equal Prop.count(:something), Prop.throttle!(:something)
39
37
  end
40
38
  end
41
39
  end
42
40
  end
41
+ end
43
42
 
44
- context "when not disabled" do
45
- setup { Prop::Limiter.expects(:disabled?).returns(false) }
43
+ describe "when not disabled" do
44
+ before { Prop::Limiter.expects(:disabled?).returns(false) }
46
45
 
47
- context "and threshold has been reached" do
48
- setup { Prop::Limiter.expects(:at_threshold?).returns(true) }
46
+ describe "and threshold has been reached" do
47
+ before { Prop::Limiter.expects(:at_threshold?).returns(true) }
49
48
 
50
- context "given a block" do
51
- should "raise Prop::RateLimited" do
52
- assert_raises(Prop::RateLimited) { Prop.throttle!(:something) { "wibble" }}
53
- end
49
+ describe "given a block" do
50
+ it "raise Prop::RateLimited" do
51
+ assert_raises(Prop::RateLimited) { Prop.throttle!(:something) { "wibble" }}
52
+ end
54
53
 
55
- should "raise even if given :increment => 0" do
56
- value = Prop.count(:something)
57
- assert_raises(Prop::RateLimited) { Prop.throttle!(:something, nil, :increment => 0) { "wibble" }}
58
- assert_equal value, Prop.count(:something)
59
- end
54
+ it "raise even if given :increment => 0" do
55
+ value = Prop.count(:something)
56
+ assert_raises(Prop::RateLimited) { Prop.throttle!(:something, nil, :increment => 0) { "wibble" }}
57
+ assert_equal value, Prop.count(:something)
60
58
  end
61
59
 
62
- context "not given a block" do
63
- should "raise Prop::RateLimited" do
64
- assert_raises(Prop::RateLimited) { Prop.throttle!(:something) }
60
+ describe "and given a before_throttle callback" do
61
+ before do
62
+ Prop.before_throttle do |handle, key, threshold, interval|
63
+ @handle = handle
64
+ @key = key
65
+ @threshold = threshold
66
+ @interval = interval
67
+ end
68
+ end
69
+
70
+ it "invoke callback with expected parameters" do
71
+ assert_raises(Prop::RateLimited) { Prop.throttle!(:something, [:extra]) }
72
+ assert_equal @handle, :something
73
+ assert_equal @key, [:extra]
74
+ assert_equal @threshold, 10
75
+ assert_equal @interval, 10
65
76
  end
66
77
  end
67
78
  end
68
79
 
69
- context "and threshold has not been reached" do
70
- setup do
71
- Prop::Limiter.expects(:at_threshold?).returns(false)
80
+ describe "not given a block" do
81
+ it "raise Prop::RateLimited" do
82
+ assert_raises(Prop::RateLimited) { Prop.throttle!(:something) }
72
83
  end
84
+ end
85
+ end
73
86
 
74
- context "given a block" do
75
- should "execute that block" do
76
- assert_equal "wibble", Prop.throttle!(:something) { "wibble" }
77
- end
87
+ describe "and threshold has not been reached" do
88
+ before do
89
+ Prop::Limiter.expects(:at_threshold?).returns(false)
90
+ end
91
+
92
+ describe "given a block" do
93
+ it "execute that block" do
94
+ assert_equal "wibble", Prop.throttle!(:something) { "wibble" }
78
95
  end
96
+ end
79
97
 
80
- context "not given a block" do
81
- should "return the updated throttle count" do
82
- assert_equal Prop.count(:something) + 1, Prop.throttle!(:something)
83
- end
98
+ describe "not given a block" do
99
+ it "return the updated throttle count" do
100
+ assert_equal Prop.count(:something) + 1, Prop.throttle!(:something)
101
+ end
84
102
 
85
- should "not update count if passed an increment of 0" do
86
- assert_equal Prop.count(:something), Prop.throttle!(:something, nil, :increment => 0)
87
- end
103
+ it "not update count if passed an increment of 0" do
104
+ assert_equal Prop.count(:something), Prop.throttle!(:something, nil, :increment => 0)
88
105
  end
89
106
  end
90
107
  end
@@ -1,47 +1,46 @@
1
1
  require 'helper'
2
+
2
3
  require 'prop/middleware'
3
4
  require 'prop/rate_limited'
4
5
 
5
- class TestMiddleware < Test::Unit::TestCase
6
6
 
7
- context Prop::Middleware do
8
- setup do
9
- @app = stub()
10
- @env = {}
11
- @middleware = Prop::Middleware.new(@app)
7
+ describe Prop::Middleware do
8
+ before do
9
+ @app = stub()
10
+ @env = {}
11
+ @middleware = Prop::Middleware.new(@app)
12
+ end
13
+
14
+ describe "when the app call completes" do
15
+ before do
16
+ @app.expects(:call).with(@env).returns("response")
12
17
  end
13
18
 
14
- context "when the app call completes" do
15
- setup do
16
- @app.expects(:call).with(@env).returns("response")
17
- end
19
+ it "return the response" do
20
+ assert_equal "response", @middleware.call(@env)
21
+ end
22
+ end
18
23
 
19
- should "return the response" do
20
- assert_equal "response", @middleware.call(@env)
21
- end
24
+ describe "when the app call results in a raised throttle" do
25
+ before do
26
+ @app.expects(:call).with(@env).raises(Prop::RateLimited.new(:handle => "foo", :threshold => 10, :interval => 60, :cache_key => "wibble", :description => "Boom!"))
22
27
  end
23
28
 
24
- context "when the app call results in a raised throttle" do
25
- setup do
26
- @app.expects(:call).with(@env).raises(Prop::RateLimited.new(:handle => "foo", :threshold => 10, :interval => 60, :cache_key => "wibble", :description => "Boom!"))
27
- end
29
+ it "return the rate limited message" do
30
+ response = @middleware.call(@env)
28
31
 
29
- should "return the rate limited message" do
30
- response = @middleware.call(@env)
32
+ assert_equal 429, response[0]
33
+ assert_equal ["Boom!"], response[2]
34
+ end
31
35
 
32
- assert_equal 429, response[0]
33
- assert_equal ["Boom!"], response[2]
36
+ describe "with a custom error handler" do
37
+ before do
38
+ @middleware = Prop::Middleware.new(@app, :error_handler => Proc.new { |env, error| "Oops" })
34
39
  end
35
40
 
36
- context "with a custom error handler" do
37
- setup do
38
- @middleware = Prop::Middleware.new(@app, :error_handler => Proc.new { |env, error| "Oops" })
39
- end
40
-
41
- should "allow setting a custom error handler" do
42
- assert_equal "Oops", @middleware.call(@env)
43
- end
41
+ it "allow setting a custom error handler" do
42
+ assert_equal "Oops", @middleware.call(@env)
44
43
  end
45
44
  end
46
45
  end
47
- end
46
+ end
data/test/test_options.rb CHANGED
@@ -1,48 +1,44 @@
1
1
  require 'helper'
2
2
 
3
- class TestOptions < Test::Unit::TestCase
3
+ describe Prop::Options do
4
+ describe "#build" do
5
+ before do
6
+ @args = { :key => "hello", :params => { :foo => "bif" }, :defaults => { :foo => "bar", :baz => "moo", :threshold => 10, :interval => 5 }}
7
+ end
4
8
 
5
- context Prop::Options do
6
- context "#build" do
7
- setup do
8
- @args = { :key => "hello", :params => { :foo => "bif" }, :defaults => { :foo => "bar", :baz => "moo", :threshold => 10, :interval => 5 }}
9
+ describe "when given valid input" do
10
+ before do
11
+ @options = Prop::Options.build(@args)
9
12
  end
10
13
 
11
- context "when given valid input" do
12
- setup do
13
- @options = Prop::Options.build(@args)
14
- end
15
-
16
- should "support defaults" do
17
- assert_equal "moo", @options[:baz]
18
- end
14
+ it "support defaults" do
15
+ assert_equal "moo", @options[:baz]
16
+ end
19
17
 
20
- should "override defaults" do
21
- assert_equal "bif", @options[:foo]
22
- end
18
+ it "override defaults" do
19
+ assert_equal "bif", @options[:foo]
23
20
  end
21
+ end
24
22
 
25
- context "when given invalid input" do
26
- should "raise when not given an interval" do
27
- @args[:defaults].delete(:interval)
28
- assert_raises(RuntimeError) { Prop::Options.build(@args) }
29
- end
23
+ describe "when given invalid input" do
24
+ it "raise when not given an interval" do
25
+ @args[:defaults].delete(:interval)
26
+ assert_raises(RuntimeError) { Prop::Options.build(@args) }
27
+ end
30
28
 
31
- should "raise when not given a threshold" do
32
- @args[:defaults].delete(:threshold)
33
- assert_raises(RuntimeError) { Prop::Options.build(@args) }
34
- end
29
+ it "raise when not given a threshold" do
30
+ @args[:defaults].delete(:threshold)
31
+ assert_raises(RuntimeError) { Prop::Options.build(@args) }
32
+ end
35
33
 
36
- should "raise when not given a key" do
37
- @args.delete(:key)
38
- begin
39
- Prop::Options.build(@args)
40
- fail "Should puke when not given a valid key"
41
- rescue
42
- end
34
+ it "raise when not given a key" do
35
+ @args.delete(:key)
36
+ begin
37
+ Prop::Options.build(@args)
38
+ fail "it puke when not given a valid key"
39
+ rescue
43
40
  end
44
41
  end
45
42
  end
46
-
47
43
  end
48
- end
44
+ end
data/test/test_prop.rb CHANGED
@@ -1,198 +1,195 @@
1
1
  require 'helper'
2
2
 
3
3
  # Integration level tests
4
- class TestProp < Test::Unit::TestCase
4
+ describe Prop do
5
+ before do
6
+ store = {}
7
+ Prop.read { |key| store[key] }
8
+ Prop.write { |key, value| store[key] = value }
9
+
10
+ @start = Time.now
11
+ Time.stubs(:now).returns(@start)
12
+ end
5
13
 
6
- context "Prop" do
7
- setup do
8
- store = {}
9
- Prop.read { |key| store[key] }
10
- Prop.write { |key, value| store[key] = value }
14
+ describe "#defaults" do
15
+ it "raise errors on invalid configuation" do
16
+ assert_raises(RuntimeError) do
17
+ Prop.configure :hello_there, :threshold => 20, :interval => 'hello'
18
+ end
11
19
 
12
- @start = Time.now
13
- Time.stubs(:now).returns(@start)
20
+ assert_raises(RuntimeError) do
21
+ Prop.configure :hello_there, :threshold => 'wibble', :interval => 100
22
+ end
14
23
  end
15
24
 
16
- context "#defaults" do
17
- should "raise errors on invalid configuation" do
18
- assert_raises(RuntimeError) do
19
- Prop.configure :hello_there, :threshold => 20, :interval => 'hello'
20
- end
21
-
22
- assert_raises(RuntimeError) do
23
- Prop.configure :hello_there, :threshold => 'wibble', :interval => 100
24
- end
25
+ it "result in a default handle" do
26
+ Prop.configure :hello_there, :threshold => 4, :interval => 10
27
+ 4.times do |i|
28
+ assert_equal (i + 1), Prop.throttle!(:hello_there, 'some key')
25
29
  end
26
30
 
27
- should "result in a default handle" do
28
- Prop.configure :hello_there, :threshold => 4, :interval => 10
29
- 4.times do |i|
30
- assert_equal (i + 1), Prop.throttle!(:hello_there, 'some key')
31
- end
32
-
33
- assert_raises(Prop::RateLimited) { Prop.throttle!(:hello_there, 'some key') }
34
- assert_equal 5, Prop.throttle!(:hello_there, 'some key', :threshold => 20)
35
- end
31
+ assert_raises(Prop::RateLimited) { Prop.throttle!(:hello_there, 'some key') }
32
+ assert_equal 5, Prop.throttle!(:hello_there, 'some key', :threshold => 20)
33
+ end
36
34
 
37
- should "create a handle accepts various cache key types" do
38
- Prop.configure :hello_there, :threshold => 4, :interval => 10
39
- assert_equal 1, Prop.throttle!(:hello_there, 5)
40
- assert_equal 2, Prop.throttle!(:hello_there, 5)
41
- assert_equal 1, Prop.throttle!(:hello_there, '6')
42
- assert_equal 2, Prop.throttle!(:hello_there, '6')
43
- assert_equal 1, Prop.throttle!(:hello_there, [ 5, '6' ])
44
- assert_equal 2, Prop.throttle!(:hello_there, [ 5, '6' ])
45
- end
35
+ it "create a handle accepts various cache key types" do
36
+ Prop.configure :hello_there, :threshold => 4, :interval => 10
37
+ assert_equal 1, Prop.throttle!(:hello_there, 5)
38
+ assert_equal 2, Prop.throttle!(:hello_there, 5)
39
+ assert_equal 1, Prop.throttle!(:hello_there, '6')
40
+ assert_equal 2, Prop.throttle!(:hello_there, '6')
41
+ assert_equal 1, Prop.throttle!(:hello_there, [ 5, '6' ])
42
+ assert_equal 2, Prop.throttle!(:hello_there, [ 5, '6' ])
46
43
  end
44
+ end
47
45
 
48
- context "#disable" do
49
- setup do
50
- Prop.configure :hello, :threshold => 10, :interval => 10
51
- end
46
+ describe "#disable" do
47
+ before do
48
+ Prop.configure :hello, :threshold => 10, :interval => 10
49
+ end
52
50
 
53
- should "not increase the throttle" do
54
- assert_equal 1, Prop.throttle!(:hello)
51
+ it "not increase the throttle" do
52
+ assert_equal 1, Prop.throttle!(:hello)
53
+ assert_equal 2, Prop.throttle!(:hello)
54
+ Prop.disabled do
55
+ assert_equal 2, Prop.throttle!(:hello)
55
56
  assert_equal 2, Prop.throttle!(:hello)
56
- Prop.disabled do
57
- assert_equal 2, Prop.throttle!(:hello)
58
- assert_equal 2, Prop.throttle!(:hello)
59
- assert Prop::Limiter.send(:disabled?)
60
- end
61
- assert !Prop::Limiter.send(:disabled?)
62
- assert_equal 3, Prop.throttle!(:hello)
57
+ assert Prop::Limiter.send(:disabled?)
63
58
  end
59
+ assert !Prop::Limiter.send(:disabled?)
60
+ assert_equal 3, Prop.throttle!(:hello)
64
61
  end
62
+ end
65
63
 
66
- context "#reset" do
67
- setup do
68
- Prop.configure :hello, :threshold => 10, :interval => 10
64
+ describe "#reset" do
65
+ before do
66
+ Prop.configure :hello, :threshold => 10, :interval => 10
69
67
 
70
- 5.times do |i|
71
- assert_equal (i + 1), Prop.throttle!(:hello)
72
- end
68
+ 5.times do |i|
69
+ assert_equal (i + 1), Prop.throttle!(:hello)
73
70
  end
71
+ end
74
72
 
75
- should "set the correct counter to 0" do
76
- Prop.throttle!(:hello, 'wibble')
77
- Prop.throttle!(:hello, 'wibble')
73
+ it "set the correct counter to 0" do
74
+ Prop.throttle!(:hello, 'wibble')
75
+ Prop.throttle!(:hello, 'wibble')
78
76
 
79
- Prop.reset(:hello)
80
- assert_equal 1, Prop.throttle!(:hello)
77
+ Prop.reset(:hello)
78
+ assert_equal 1, Prop.throttle!(:hello)
81
79
 
82
- assert_equal 3, Prop.throttle!(:hello, 'wibble')
83
- Prop.reset(:hello, 'wibble')
84
- assert_equal 1, Prop.throttle!(:hello, 'wibble')
85
- end
80
+ assert_equal 3, Prop.throttle!(:hello, 'wibble')
81
+ Prop.reset(:hello, 'wibble')
82
+ assert_equal 1, Prop.throttle!(:hello, 'wibble')
86
83
  end
84
+ end
87
85
 
88
- context "#throttled?" do
89
- should "return true once the threshold has been reached" do
90
- Prop.configure(:hello, :threshold => 2, :interval => 10)
91
- Prop.throttle!(:hello)
92
- assert !Prop.throttled?(:hello)
93
- Prop.throttle!(:hello)
94
- assert Prop.throttled?(:hello)
95
- end
86
+ describe "#throttled?" do
87
+ it "return true once the threshold has been reached" do
88
+ Prop.configure(:hello, :threshold => 2, :interval => 10)
89
+ Prop.throttle!(:hello)
90
+ assert !Prop.throttled?(:hello)
91
+ Prop.throttle!(:hello)
92
+ assert Prop.throttled?(:hello)
96
93
  end
94
+ end
97
95
 
98
- context "#count" do
99
- setup do
100
- Prop.configure(:hello, :threshold => 20, :interval => 20)
101
- Prop.throttle!(:hello)
102
- Prop.throttle!(:hello)
103
- end
96
+ describe "#count" do
97
+ before do
98
+ Prop.configure(:hello, :threshold => 20, :interval => 20)
99
+ Prop.throttle!(:hello)
100
+ Prop.throttle!(:hello)
101
+ end
104
102
 
105
- should "be aliased by #count" do
106
- assert_equal Prop.count(:hello), 2
107
- end
103
+ it "be aliased by #count" do
104
+ assert_equal Prop.count(:hello), 2
105
+ end
108
106
 
109
- should "return the number of hits on a throttle" do
110
- assert_equal Prop.query(:hello), 2
107
+ it "return the number of hits on a throttle" do
108
+ assert_equal Prop.query(:hello), 2
109
+ end
110
+ end
111
+
112
+ describe "#throttle!" do
113
+ it "increment counter correctly" do
114
+ 3.times do |i|
115
+ assert_equal (i + 1), Prop.throttle!(:hello, nil, :threshold => 10, :interval => 10)
111
116
  end
112
117
  end
113
118
 
114
- context "#throttle!" do
115
- should "increment counter correctly" do
116
- 3.times do |i|
117
- assert_equal (i + 1), Prop.throttle!(:hello, nil, :threshold => 10, :interval => 10)
118
- end
119
+ it "reset counter when time window is passed" do
120
+ 3.times do |i|
121
+ assert_equal (i + 1), Prop.throttle!(:hello, nil, :threshold => 10, :interval => 10)
119
122
  end
120
123
 
121
- should "reset counter when time window is passed" do
122
- 3.times do |i|
123
- assert_equal (i + 1), Prop.throttle!(:hello, nil, :threshold => 10, :interval => 10)
124
- end
124
+ Time.stubs(:now).returns(@start + 20)
125
125
 
126
- Time.stubs(:now).returns(@start + 20)
126
+ 3.times do |i|
127
+ assert_equal (i + 1), Prop.throttle!(:hello, nil, :threshold => 10, :interval => 10)
128
+ end
129
+ end
127
130
 
128
- 3.times do |i|
129
- assert_equal (i + 1), Prop.throttle!(:hello, nil, :threshold => 10, :interval => 10)
130
- end
131
+ it "not increment the counter beyond the threshold" do
132
+ Prop.configure(:hello, :threshold => 5, :interval => 1)
133
+ 10.times do |i|
134
+ Prop.throttle!(:hello) rescue nil
131
135
  end
132
136
 
133
- should "not increment the counter beyond the threshold" do
134
- Prop.configure(:hello, :threshold => 5, :interval => 1)
135
- 10.times do |i|
136
- Prop.throttle!(:hello) rescue nil
137
- end
137
+ assert_equal 5, Prop.query(:hello)
138
+ end
139
+
140
+ it "support custom increments" do
141
+ Prop.configure(:hello, :threshold => 100, :interval => 10)
138
142
 
139
- assert_equal 5, Prop.query(:hello)
140
- end
143
+ Prop.throttle!(:hello)
144
+ Prop.throttle!(:hello)
141
145
 
142
- should "support custom increments" do
143
- Prop.configure(:hello, :threshold => 100, :interval => 10)
146
+ assert_equal 2, Prop.query(:hello)
144
147
 
145
- Prop.throttle!(:hello)
146
- Prop.throttle!(:hello)
148
+ Prop.throttle!(:hello, nil, :increment => 48)
147
149
 
148
- assert_equal 2, Prop.query(:hello)
150
+ assert_equal 50, Prop.query(:hello)
151
+ end
149
152
 
150
- Prop.throttle!(:hello, nil, :increment => 48)
153
+ it "raise Prop::RateLimited when the threshold is exceeded" do
154
+ Prop.configure(:hello, :threshold => 5, :interval => 10, :description => "Boom!")
151
155
 
152
- assert_equal 50, Prop.query(:hello)
156
+ 5.times do |i|
157
+ Prop.throttle!(:hello, nil)
153
158
  end
154
-
155
- should "raise Prop::RateLimited when the threshold is exceeded" do
156
- Prop.configure(:hello, :threshold => 5, :interval => 10, :description => "Boom!")
157
-
158
- 5.times do |i|
159
- Prop.throttle!(:hello, nil)
160
- end
161
- assert_raises(Prop::RateLimited) do
162
- Prop.throttle!(:hello, nil)
163
- end
164
-
165
- begin
166
- Prop.throttle!(:hello, nil)
167
- fail
168
- rescue Prop::RateLimited => e
169
- assert_equal :hello, e.handle
170
- assert_match "5 tries per 10s exceeded for key", e.message
171
- assert_equal "Boom!", e.description
172
- assert e.retry_after
173
- end
159
+ assert_raises(Prop::RateLimited) do
160
+ Prop.throttle!(:hello, nil)
174
161
  end
175
162
 
176
- should "raise a RuntimeError when a handle has not been configured" do
177
- assert_raises(RuntimeError) do
178
- Prop.throttle!(:no_such_handle, nil, :threshold => 5, :interval => 10)
179
- end
163
+ begin
164
+ Prop.throttle!(:hello, nil)
165
+ fail
166
+ rescue Prop::RateLimited => e
167
+ assert_equal :hello, e.handle
168
+ assert_match "5 tries per 10s exceeded for key", e.message
169
+ assert_equal "Boom!", e.description
170
+ assert e.retry_after
180
171
  end
181
172
  end
182
173
 
183
- context 'different handles with the same interval' do
184
- setup do
185
- Prop.configure(:api_requests, :threshold => 100, :interval => 30)
186
- Prop.configure(:login_attempts, :threshold => 10, :interval => 30)
174
+ it "raise a RuntimeError when a handle has not been configured" do
175
+ assert_raises(RuntimeError) do
176
+ Prop.throttle!(:no_such_handle, nil, :threshold => 5, :interval => 10)
187
177
  end
178
+ end
179
+ end
188
180
 
189
- should 'be counted separately' do
190
- user_id = 42
191
- Prop.throttle!(:api_requests, user_id)
192
- assert_equal(1, Prop.count(:api_requests, user_id))
193
- assert_equal(0, Prop.count(:login_attempts, user_id))
194
- end
181
+ describe 'different handles with the same interval' do
182
+ before do
183
+ Prop.configure(:api_requests, :threshold => 100, :interval => 30)
184
+ Prop.configure(:login_attempts, :threshold => 10, :interval => 30)
195
185
  end
196
186
 
187
+ it 'be counted separately' do
188
+ user_id = 42
189
+ Prop.throttle!(:api_requests, user_id)
190
+ assert_equal(1, Prop.count(:api_requests, user_id))
191
+ assert_equal(0, Prop.count(:login_attempts, user_id))
192
+ end
197
193
  end
194
+
198
195
  end
@@ -1,27 +1,24 @@
1
1
  require 'helper'
2
2
 
3
- class TestRateLimited < Test::Unit::TestCase
3
+ describe Prop::RateLimited do
4
+ describe "#initialize" do
5
+ before do
6
+ time = Time.at(1333685680)
7
+ Time.stubs(:now).returns(time)
4
8
 
5
- context Prop::RateLimited do
6
- context "#initialize" do
7
- setup do
8
- time = Time.at(1333685680)
9
- Time.stubs(:now).returns(time)
10
-
11
- @error = Prop::RateLimited.new(:handle => "foo", :threshold => 10, :interval => 60, :cache_key => "wibble", :description => "Boom!")
12
- end
9
+ @error = Prop::RateLimited.new(:handle => "foo", :threshold => 10, :interval => 60, :cache_key => "wibble", :description => "Boom!")
10
+ end
13
11
 
14
- should "return an error instance" do
15
- assert @error.is_a?(StandardError)
16
- assert @error.is_a?(Prop::RateLimited)
12
+ it "return an error instance" do
13
+ assert @error.is_a?(StandardError)
14
+ assert @error.is_a?(Prop::RateLimited)
17
15
 
18
- assert_equal "foo", @error.handle
19
- assert_equal "wibble", @error.cache_key
20
- assert_equal "Boom!", @error.description
21
- assert_equal "foo threshold of 10 tries per 60s exceeded for key 'nil', hash wibble", @error.message
22
- assert_equal 20, @error.retry_after
23
- end
16
+ assert_equal "foo", @error.handle
17
+ assert_equal "wibble", @error.cache_key
18
+ assert_equal "Boom!", @error.description
19
+ assert_equal "foo threshold of 10 tries per 60s exceeded for key 'nil', hash wibble", @error.message
20
+ assert_equal 20, @error.retry_after
24
21
  end
25
-
26
22
  end
27
- end
23
+
24
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prop
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -44,7 +44,7 @@ dependencies:
44
44
  - !ruby/object:Gem::Version
45
45
  version: '0'
46
46
  - !ruby/object:Gem::Dependency
47
- name: shoulda
47
+ name: minitest
48
48
  requirement: !ruby/object:Gem::Requirement
49
49
  none: false
50
50
  requirements: