merb_threshold 0.1.4

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.
@@ -0,0 +1,165 @@
1
+ module Merb
2
+ module Threshold
3
+
4
+ ##
5
+ #
6
+ # @note
7
+ # * time is treated as relative from 'now'
8
+ # * nowhere are events sorted within this class so they should
9
+ # be added in their proper order
10
+ # * Why? its easy to do this from the controller naturally since time is linear (right?)
11
+ # and its faster not having to worry about sorting whenever an occurrence is added
12
+ #
13
+ class Frequency
14
+ attr_reader :interval, :occurrence, :units, :period, :events
15
+
16
+ # Frequency.new(5, 30, :seconds) => "5 times per 30 seconds"
17
+ # Frequency.new(1, 3.minutes) => "1 time per 180 seconds"
18
+ def initialize(occ,int,unts = nil)
19
+ @occurrence = occ
20
+ @interval = int
21
+
22
+ # All test are done with the period (reduced to seconds)
23
+ # 50.minutes #=> 50.send :minutes => 3000 seconds
24
+ if unts
25
+ @period = interval.send unts
26
+ @units = unts
27
+ else #no units default :seconds
28
+ @period = interval
29
+ cast_units
30
+ end
31
+
32
+ #If the period is zero, never permit?
33
+ if period == 0
34
+ @occurrence = 0
35
+ end
36
+ end
37
+
38
+ ##
39
+ # tests if the frequency would permit the additional occurence or if it
40
+ # would exceed the frequency. Histories can be loaded with frequency#load
41
+ #
42
+ # @see #load
43
+ #
44
+ # @return [Boolean]
45
+ #
46
+ def permit?
47
+ (current_events.length < occurrence)
48
+ end
49
+
50
+ ##
51
+ # How long until the resource is freely available
52
+ #
53
+ # @return [~Numeric]
54
+ def wait
55
+ num_evts = current_events.length
56
+ if num_evts == 0 || num_evts < occurrence
57
+ return 0
58
+ else #How long until the oldest falls off?
59
+ # originally had now - period > @mm.first but +1 all over the place was
60
+ # retareded:
61
+ # Want: now - period >= @mm.first
62
+ # now - period + x == @mm.first
63
+ # => x == @mm.first + period - now
64
+ return (current_events.first + period - Time.now.to_i)
65
+ end
66
+ end
67
+
68
+ ##
69
+ # Loads a history of events
70
+ #
71
+ # @param evts [~Array[Fixnum]] list of timestamps
72
+ #
73
+ # @note
74
+ # Should be loaded presorted (threshold should always stored sorted min=>high)
75
+ # an array is used rather than a set because duplicates may be needed
76
+ # (concurrent access times)
77
+ #
78
+ def load(evts)
79
+ @events ||= []
80
+ @events += evts
81
+ end
82
+
83
+ ##
84
+ # clears current events and sets
85
+ #
86
+ # @param evts [~Array[Fixnum]] list of timestamps
87
+ #
88
+ # @see #load
89
+ #
90
+ #
91
+ def load!(evts)
92
+ @events = evts
93
+ end
94
+
95
+ ##
96
+ # flushes the events array
97
+ #
98
+ def flush
99
+ @events = []
100
+ end
101
+
102
+ ##
103
+ # adds a single event, this always adds and does not perform a permit? first
104
+ # @param evt [Fixnum] Timestamp
105
+ #
106
+ # @return [Array[Fixnum]]
107
+ def add(evt)
108
+ @events ||= []
109
+ @events << evt
110
+ @events
111
+ end
112
+
113
+ ##
114
+ # The rate of occurences in seconds
115
+ # returns the frequency for events that happened over period
116
+ #
117
+ # @see #events
118
+ # @see #load
119
+ # @see #period
120
+ #
121
+ # @return [~Numeric]
122
+ #
123
+ def rate
124
+ current_events.length / period.to_f
125
+ end
126
+
127
+ ##
128
+ # Casts frequency units when not specified
129
+ #
130
+ def cast_units
131
+ case @interval
132
+ when 0..120 # :seconds
133
+ @units = (@interval != 1 ? :seconds : :second)
134
+ when 121..3600 # :minutes
135
+ @interval = @interval / 60.0
136
+ @units = (@interval != 1 ? :minutes : :minute)
137
+ else # :hours
138
+ @interval = @interval / 3600.0
139
+ @units = (@interval != 1 ? :hours : :hour)
140
+ end
141
+ end
142
+
143
+ ##
144
+ # Describe the frequency
145
+ #
146
+ # @return [String]
147
+ def to_s
148
+ @to_s ||= "#{occurrence} time#{'s' if occurrence != 1} per #{interval} #{units}"
149
+ end
150
+
151
+ ##
152
+ # Get list of events for current period
153
+ #
154
+ # @return [Array[Fixnum]]
155
+ #
156
+ def current_events
157
+ if @events
158
+ @events.find_all{ |evt| evt >= (Time.now.to_i - period) }
159
+ else
160
+ @events ||= []
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,70 @@
1
+ module Merb
2
+ module Threshold
3
+ module Helpers
4
+ ##
5
+ # Display a captcha if the threshold has been exceeded
6
+ #
7
+ # @param threshold_name [~Symbol] key to look up
8
+ #
9
+ # @params opts [Hash] passed to partial as 'threshold_options'
10
+ # Pass any params intended for RecaptchaOptions
11
+ # Additionally may pass:
12
+ # :
13
+ # :partial => "./path/to/alternate/partial"
14
+ # :ssl => TRue|False
15
+ # :partial_opts => {} #options to pass to partial()
16
+ # #Theses keys are deleted before being passed to RecapthaOptions
17
+ #
18
+ # @return [String]
19
+ def captcha(threshold_name = nil, opts={})
20
+ if threshold_name.is_a?(Hash)
21
+ opts = threshold_name
22
+ threshold_name = action_name
23
+ end
24
+
25
+ curr_threshold_key = threshold_key(threshold_name)
26
+
27
+ # Has the thresholded resource been accessed during this request
28
+ # if so
29
+ # if it was relaxed
30
+ # dont show partial
31
+ # else
32
+ # show partial
33
+ # else #resource wasn't access
34
+ # if permit_another?
35
+ # dont show partial
36
+ # else
37
+ # show partial
38
+ #
39
+ @show_captcha = if @relaxed_thresholds && @relaxed_thresholds.key?(curr_threshold_key)
40
+ if @relaxed_thresholds[curr_threshold_key]
41
+ false #dont show partial, it was relaxed
42
+ else
43
+ true #show partial, threshold exceeded
44
+ end
45
+ else
46
+ !will_permit_another?(threshold_name)
47
+ end
48
+
49
+ # if it won't permit another, show the captcha
50
+ if @show_captcha
51
+ _src_uri = opts.delete(:ssl) ? RecaptchaClient::API_SSL_SERVER : RecaptchaClient::API_SERVER
52
+
53
+ _encoded_key = escape_html(RecaptchaClient.public_key)
54
+
55
+ _recaptcha_partial = (opts.delete(:partial) || Merb::Plugins.config[:merb_threshold][:captcha_partial])
56
+ _partial_opts = opts.delete(:partial_opts) || {}
57
+
58
+ _partial_opts.merge!({
59
+ :src_uri => _src_uri,
60
+ :encoded_key => _encoded_key,
61
+ :threshold_options => opts,
62
+ :captcha_error => escape_html(@captcha_error.to_s)
63
+ })
64
+
65
+ partial(_recaptcha_partial, _partial_opts)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,56 @@
1
+ module Merb
2
+ module Threshold
3
+ module Helpers
4
+
5
+ ##
6
+ # Display a wait message if the threshold has been exceeded
7
+ #
8
+ # @param threshold_name [~Symbol] key to look up
9
+ #
10
+ # @params opts [Hash] passed to partial as 'threshold_options'
11
+ # :partial_opts => {} #options to pass to partial()
12
+ # @return [String]
13
+ def wait(threshold_name = nil,opts={})
14
+ if threshold_name.is_a?(Hash)
15
+ opts = threshold_name
16
+ threshold_name = action_name
17
+ end
18
+
19
+ curr_threshold_key = threshold_key(threshold_name)
20
+
21
+ # Has the thresholded resource been accessed during this request
22
+ # if so
23
+ # if it was relaxed
24
+ # dont show partial
25
+ # else
26
+ # show partial
27
+ # else #resource wasn't access
28
+ # if permit_another?
29
+ # dont show partial
30
+ # else
31
+ # show partial
32
+ #
33
+ @show_wait = if @relaxed_thresholds && @relaxed_thresholds.key?(curr_threshold_key)
34
+ if @relaxed_thresholds[curr_threshold_key]
35
+ false #dont show partial, it was relaxed
36
+ else
37
+ true #show partial, threshold exceeded
38
+ end
39
+ else #wasn't accessed, will it permit another?
40
+ !will_permit_another?(threshold_name)
41
+ end
42
+
43
+ # if it wont permit another show wait
44
+ if @show_wait
45
+ _wait_partial = opts.delete(:partial) || Merb::Plugins.config[:merb_threshold][:wait_partial]
46
+ _partial_opts = opts.delete(:partial_opts) || {}
47
+ _partial_opts.merge!({
48
+ :seconds_to_wait => (waiting_period[threshold_key(threshold_name)] || 0)
49
+ })
50
+ partial(_wait_partial,_partial_opts)
51
+ end
52
+ end
53
+
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,40 @@
1
+ require 'fileutils'
2
+ namespace :merb_threshold do
3
+ desc "Generate captcha partial OUT=./path/to/partials"
4
+ task :generate_captcha do
5
+ from = File.dirname(__FILE__)
6
+ to = File.join(ENV['OUT'],'_recaptcha_partial.html.erb')
7
+ FileUtils.cp File.join(from,'templates','_recaptcha_partial.html.erb'), to
8
+
9
+ if File.exist? to
10
+ puts "Captcha partial created: #{to}"
11
+ else
12
+ puts "Could not create partial: #{to}"
13
+ end
14
+ end
15
+
16
+ desc "Generate wait partial OUT=./path/to/partials"
17
+ task :generate_wait do
18
+ from = File.dirname(__FILE__)
19
+ to = File.join(ENV['OUT'],'_wait_partial.html.erb')
20
+ FileUtils.cp File.join(from,'templates','_wait_partial.html.erb'), to
21
+
22
+ if File.exist? to
23
+ puts "Wait partial created: #{to}"
24
+ else
25
+ puts "Could not create partial: #{to}"
26
+ end
27
+ end
28
+ end
29
+
30
+ namespace :audit do
31
+ desc "Print out all thresholds"
32
+ task :thresholds => :merb_env do
33
+ puts "Thresholds:"
34
+ Merb::Controller.send(:class_variable_get, "@@_threshold_map").each do |name,opts|
35
+ limit = opts[:limit].is_a?(Array) ?
36
+ Merb::Threshold::Frequency.new(*opts[:limit]) : opts[:limit]
37
+ puts " ~ #{name}: #{limit.to_s}"
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,13 @@
1
+
2
+ module Merb
3
+ module Threshold
4
+ # Add support for doing
5
+ # :limit => 1.per(30.seconds)
6
+ # :limit => 1.per(50, :minutes)
7
+ module Per
8
+ def per(period, units = nil)
9
+ Frequency.new(self,period,units)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,47 @@
1
+ require 'net/http'
2
+
3
+ module Merb
4
+ module Threshold
5
+ class RecaptchaClient
6
+ API_SERVER = "http://api.recaptcha.net"
7
+ API_SSL_SERVER = "https://api-secure.recaptcha.net"
8
+ API_VERIFY_SERVER = "http://api-verify.recaptcha.net"
9
+
10
+ def self.public_key
11
+ @@public_key
12
+ end
13
+ def self.public_key=(key)
14
+ @@public_key = key
15
+ end
16
+ def self.private_key
17
+ @@private_key
18
+ end
19
+ def self.private_key=(key)
20
+ @@private_key = key
21
+ end
22
+
23
+ ##
24
+ # Attempt to solve the captcha
25
+ #
26
+ # @param ip [String] remote ip address
27
+ # @param challenge [String] captcha challenge
28
+ # @param response [String] captcha response
29
+ #
30
+ # @return [Array[Boolean,String]]
31
+ #
32
+ def self.solve(ip,challenge,response)
33
+ response = Net::HTTP.post_form(URI.parse(API_VERIFY_SERVER + '/verify'),{
34
+ :privatekey => @@private_key,
35
+ :remoteip => ip, #request.remote_ip,
36
+ :challenge => challenge, #params[:recaptcha_challenge_field],
37
+ :response => response #params[:recaptcha_response_field]
38
+ })
39
+
40
+ answer, error = response.body.split.map { |s| s.chomp }
41
+
42
+ [ !!(answer=='true'), error ]
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,16 @@
1
+ <script type="text/javascript">
2
+ var RecaptchaOptions = <%= threshold_options.to_json %>
3
+ </script>
4
+
5
+ <script type="text/javascript" src="<%=src_uri-%>/challenge?k=<%=encoded_key-%>&error=<%=captcha_error-%>">
6
+
7
+ </script>
8
+
9
+ <noscript>
10
+ <iframe src="<%=src_uri-%>/noscript?k=<%=encoded_key-%>&error=<%=captcha_error-%>" height="300" width="500" frameborder="0">
11
+ </iframe>
12
+ <br>
13
+ <textarea name="recaptcha_challenge_field" rows="3" cols="40">
14
+ </textarea>
15
+ <input type="hidden" name="recaptcha_response_field" value="manual_challenge">
16
+ </noscript>
@@ -0,0 +1,3 @@
1
+ <span>
2
+ This resource will be available in <%= seconds_to_wait -%> seconds.
3
+ </span>
@@ -0,0 +1,271 @@
1
+ describe Merb::Controller do
2
+ before(:all) do
3
+ class OtherController < Merb::Controller
4
+ def index; "Index"; end
5
+ end
6
+
7
+ class TestController < Merb::Controller
8
+ def index; "Index"; end
9
+ def create; "Create"; end
10
+ def destroy; "Destroy"; end
11
+ def blog; "Blah blah"; end
12
+
13
+ GhettoSessionStore = {}
14
+ #Why is this ghetto session hack here? Because I spent like an hour trying to figure
15
+ # out how to get access to cookie based sessions in rspecs without starting a damn server
16
+ # up and couldn't figure it out. Feel free to 'fix' this.
17
+ def session
18
+ GhettoSessionStore[params[:session_id]] ||={}
19
+ GhettoSessionStore[params[:session_id]]
20
+ end
21
+
22
+ end
23
+ end
24
+ after(:each) do
25
+ TestController._before_filters.clear
26
+ TestController.send :class_variable_set, "@@_threshold_map", Mash.new
27
+ end
28
+
29
+ it 'should respond to #waiting_period' do
30
+ TestController.new('').should respond_to(:waiting_period)
31
+ end
32
+
33
+ it 'should respond to TestController.register_threshold' do
34
+ TestController.should respond_to(:register_threshold)
35
+ end
36
+
37
+ it 'should register the threshold with Merb::Controller so any threshold is accessible by all controllers' do
38
+ TestController.register_threshold(:cross_controller, :limit => 1.per(30.seconds))
39
+ @thresholds = OtherController.send :class_variable_get, "@@_threshold_map"
40
+ @thresholds.key?(:cross_controller).should be(true)
41
+ end
42
+
43
+ it 'should respond to TestController.threshold_actions' do
44
+ TestController.should respond_to(:threshold_actions)
45
+ end
46
+
47
+ it 'should respond to #permit_access?' do
48
+ TestController.new('').should respond_to(:permit_access?)
49
+ end
50
+
51
+ it 'should respond to #is_currently_exceeded?' do
52
+ TestController.new('').should respond_to(:is_currently_exceeded?)
53
+ end
54
+
55
+ it 'should respond to #access_history' do
56
+ TestController.new('').should respond_to(:access_history)
57
+ end
58
+
59
+ it 'should define THRESHOLD_OPTIONS' do
60
+ defined?(Merb::Controller::THRESHOLD_OPTIONS).should == "constant"
61
+ Merb::Controller::THRESHOLD_OPTIONS.should be_instance_of(Array)
62
+ end
63
+
64
+ it 'should raise an exception if a threshold is not named' do
65
+ lambda{
66
+ TestController.register_threshold :limit => 1.per(30.seconds)
67
+ }.should raise_error(ArgumentError)
68
+ end
69
+
70
+ it 'should raise an exception if an invalid option is passed' do
71
+ lambda{
72
+ TestController.register_threshold :create, :band => "stixx"
73
+ }.should raise_error(ArgumentError)
74
+ end
75
+
76
+ it 'should define THRESHOLD_DEFAULTS' do
77
+ defined?(Merb::Controller::THRESHOLD_DEFAULTS).should == "constant"
78
+ Merb::Controller::THRESHOLD_DEFAULTS.should be_instance_of(Hash)
79
+ end
80
+
81
+ it 'should wrap calls to before filter with threshold' do
82
+ class TestController
83
+ threshold_actions :index
84
+ end
85
+ TestController._before_filters.first.last[:only].member?("index")
86
+ TestController._before_filters.first.first.should be_instance_of(Proc)
87
+ end
88
+
89
+ it 'should consider a captch invalid if the challenge was not submitted' do
90
+ pending
91
+ end
92
+
93
+ it 'should be able to determine if a captcha is valid or not' do
94
+ TestController.threshold_actions :index, :limit => 1.per(1.week)
95
+ @response = dispatch_to(TestController, :index,{:session_id=>"submitting_captcha"})
96
+
97
+ # Logic here is that recaptcha, guarantees when a captcha is solved, just need to confirm
98
+ # that the api can be contacted
99
+ @response = dispatch_to(TestController, :index,{
100
+ :session_id => "submitting_captcha",
101
+ :recaptcha_challenge_field => "bad challenge",
102
+ :recaptcha_response_field => "bad response"
103
+ })
104
+
105
+ @response.is_currently_exceeded?(:"test_controller/index").should be(true)
106
+ @response.instance_variable_get("@captcha_error").should_not be(nil)
107
+ end
108
+
109
+ it 'should be able to relax a threshold by waiting' do
110
+ unless ENV['SKIP_WAIT']
111
+ TestController.threshold_actions :index, :limit => 1.per(2.seconds)
112
+ @response = dispatch_to(TestController, :index,{:session_id=>"allow_wait_timeout"})
113
+
114
+ @response.is_currently_exceeded?(:"test_controller/index").should be(false)
115
+ @response = dispatch_to(TestController, :index,{:session_id=>"allow_wait_timeout"})
116
+ @response.is_currently_exceeded?(:"test_controller/index").should be(true)
117
+
118
+ @response.waiting_period[:"test_controller/index"].should be(2)
119
+
120
+ puts "Waiting a few seconds for timeout test..."
121
+ sleep(4)
122
+
123
+ @response = dispatch_to(TestController, :index,{
124
+ :session_id=>"allow_wait_timeout",
125
+ :debug => "true"
126
+ })
127
+ @response.is_currently_exceeded?(:"test_controller/index").should be(false)
128
+ end
129
+ end
130
+
131
+ it 'should be able to relax a threshold that halts by waiting' do
132
+ unless ENV['SKIP_WAIT']
133
+ TestController.threshold_actions :index, :limit => [1,2.seconds], :halt_with => "Too many requests!"
134
+ @response = dispatch_to(TestController, :index,{:session_id=>"allow_wait_timeout_w_halt"})
135
+
136
+ @response.is_currently_exceeded?(:"test_controller/index").should be(false)
137
+ @response = dispatch_to(TestController, :index,{:session_id=>"allow_wait_timeout_w_halt"})
138
+ @response.is_currently_exceeded?(:"test_controller/index").should be(true)
139
+
140
+ @response.waiting_period[:"test_controller/index"].should be(2)
141
+
142
+ puts "Waiting a few seconds for timeout test..."
143
+ sleep(4)
144
+
145
+ @response = dispatch_to(TestController, :index,{:session_id=>"allow_wait_timeout_w_halt"})
146
+ @response.is_currently_exceeded?(:"test_controller/index").should be(false)
147
+ end
148
+ end
149
+
150
+ it 'should be able to throw(:halt) when a threshold is exceeded' do
151
+ TestController.threshold_actions :create,
152
+ :halt_with => "Access Denied",
153
+ :limit => [1,30.minutes]
154
+
155
+ dispatch_to(TestController, :create,{:session_id=>"throw_halt_test"})
156
+
157
+ @response = dispatch_to(TestController, :create,{:session_id=>"throw_halt_test"})
158
+ @response.body.should == "Access Denied"
159
+ end
160
+
161
+ it 'should set the wait time when the threshold has been exceeded and in wait mode' do
162
+ TestController.threshold_actions :create, :limit => [1,30.minutes]
163
+ @response = dispatch_to(TestController, :create, {:session_id => "wait_bitch"})
164
+ @response = dispatch_to(TestController, :create, {:session_id => "wait_bitch"})
165
+
166
+ @response.waiting_period[:"test_controller/create"].should be(30.minutes)
167
+ end
168
+
169
+ it 'should be able to determine if a threshold has been exceeded' do
170
+ TestController.threshold_actions :create, :limit => 1.per(30.minutes)
171
+
172
+ dispatch_to(TestController, :create, {:session_id => "threshold_exceed?"})
173
+ @response = dispatch_to(TestController, :create, {:session_id => "threshold_exceed?"})
174
+
175
+ @response.is_currently_exceeded?(:"test_controller/create").should be(true)
176
+ end
177
+
178
+ it 'should be able to specify params as portions of the key in Controller.threshold' do
179
+ TestController.threshold_actions :blog,
180
+ :params => [:blog_id],
181
+ :limit => [1,30.minutes]
182
+
183
+ @response = dispatch_to(TestController, :blog, {
184
+ :session_id => "params_in_key",
185
+ :blog_id => 35,
186
+ :username => "awesome_user"
187
+ })
188
+
189
+ @response.access_history(:"test_controller/blog/35").should have(1).history
190
+ end
191
+
192
+ it 'should add an access time when the threshold has not been exceeded' do
193
+ TestController.threshold_actions :create, :limit => [30,1.minute]
194
+ 30.times do |access_counter|
195
+ @response = dispatch_to(TestController, :create,{:session_id=>"record_access_to_resource"})
196
+ @response.access_history(:"test_controller/create").should have(access_counter + 1).accesses
197
+ end
198
+ end
199
+
200
+ it 'should not add an access time when the threshold has been exceeded' do
201
+ TestController.threshold_actions :create, :limit => [30,1.minute]
202
+ 30.times do |access_counter|
203
+ @response = dispatch_to(TestController, :create,{:session_id=>"record_access_whilst_not_exceeded"})
204
+ @response.access_history(:"test_controller/create").should have(access_counter + 1).accesses
205
+ end
206
+
207
+ @response = dispatch_to(TestController, :create,{:session_id=>"record_access_whilst_not_exceeded"})
208
+ @response.permit_access?(:"test_controller/create").should be(false)
209
+ @response.access_history(:"test_controller/create").should have(30).accesses
210
+ end
211
+
212
+ it 'should be able to determine if it can permit another access' do
213
+ TestController.threshold_actions :index, :limit => 2.per(30.seconds)
214
+ @response = dispatch_to(TestController, :index,{:session_id=>"default_to_action_name"})
215
+ @response.will_permit_another?(:"test_controller/index").should be(true)
216
+ @response = dispatch_to(TestController, :index,{:session_id=>"default_to_action_name"})
217
+ @response.will_permit_another?(:"test_controller/index").should be(false)
218
+ end
219
+
220
+ it 'should check the threshold with the action name if not provided' do
221
+ pending
222
+ #TestController.threshold_actions :index, :limit => 1.per(30.minutes)
223
+ #@response = dispatch_to(TestController, :index,{:session_id=>"default_to_action_name"})
224
+ #need an action to call permit_access?
225
+ end
226
+
227
+ it 'should captcha everytime if the :limit is not set' do
228
+ pending
229
+ end
230
+
231
+ it 'should never wait if the :limit is not set' do
232
+ pending
233
+ end
234
+
235
+ it 'should apply the threshold to the controller when not specified' do
236
+ TestController.threshold_actions :limit => [10,30,:seconds]
237
+ dispatch_to(TestController, :index,{
238
+ :session_id=>"threshold_all_actions_test"
239
+ }).access_history(:test_controller).should have(1).accesses
240
+
241
+ dispatch_to(TestController, :create,{
242
+ :session_id=>"threshold_all_actions_test"
243
+ }).access_history(:test_controller).should have(2).accesses
244
+
245
+ dispatch_to(TestController, :destroy,{
246
+ :session_id=>"threshold_all_actions_test"
247
+ }).access_history(:test_controller).should have(3).accesses
248
+ end
249
+
250
+ it 'should only apply the threshold to the action(s) specified' do
251
+ TestController.threshold_actions :destroy
252
+ TestController._before_filters.first.last[:only].length.should be(1)
253
+ TestController._before_filters.first.last[:only].first.should == "destroy"
254
+ end
255
+
256
+ # Given same rule 2 per 30 seconds for index, create, destroy
257
+ # each should maintain its own history of accesses
258
+ #
259
+ it 'when multiple actions are specified their access histories should be kept separate' do
260
+ TestController.threshold_actions :index, :create, :limit => [2, 30, :seconds]
261
+
262
+ dispatch_to(TestController, :index,{:session_id=>"separate_history_test"})
263
+ @response1=dispatch_to(TestController, :index,{:session_id=>"separate_history_test"})
264
+ @response2=dispatch_to(TestController, :create,{:session_id=>"separate_history_test"})
265
+ @response3=dispatch_to(TestController, :destroy,{:session_id=>"separate_history_test"})
266
+
267
+ @response1.session[:merb_threshold_history][:"test_controller/index"].should have(2).request
268
+ @response2.session[:merb_threshold_history][:"test_controller/create"].should have(1).request
269
+ @response3.session[:merb_threshold_history][:"test_controller/destroy"].should be_nil
270
+ end
271
+ end