coryodaniel-merb_threshold 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 = :"#{controller_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 = :"#{controller_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