├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── CONTRIBUTORS.md
├── Gemfile
├── Guardfile
├── README.md
├── Rakefile
├── VERSION
├── lib
└── rack
│ ├── redis_throttle.rb
│ └── redis_throttle
│ ├── connection.rb
│ ├── daily.rb
│ ├── interval.rb
│ ├── limiter.rb
│ ├── testing
│ └── connection.rb
│ ├── time_window.rb
│ └── version.rb
├── redis_throttle.gemspec
└── spec
├── fixtures
└── fake_app.rb
├── rack
├── redis_throttle
│ ├── daily_no_redis_spec.rb
│ └── daily_spec.rb
└── spec_helper.rb
├── spec_helper.rb
└── support
└── matchers
└── body.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | pkg/*
2 | Gemfile.lock
3 | *.gem
4 | .bundle
5 | .yardoc
6 | *.swp
7 | *.swo
8 | *.DS_Store
9 | coverage/*
10 | doc/*
11 | .rvmrc
12 | .yardoc
13 | .ruby-version
14 | tmp/*
15 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | services:
3 | - redis-server
4 | rvm:
5 | - 1.9.3
6 | - jruby-19mode
7 | - rbx
8 | - 2.0.0
9 | - 2.1.0
10 | matrix:
11 | allow_failures:
12 | - rvm: rbx
13 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## Release v0.0.1 - First release (01/01/2013)
4 |
--------------------------------------------------------------------------------
/CONTRIBUTORS.md:
--------------------------------------------------------------------------------
1 | Special thanks to the following people for submitting patches
2 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | # Specify your gem's dependencies in rack_proxify.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/Guardfile:
--------------------------------------------------------------------------------
1 | guard 'rspec', cli: '--format Fuubar --color', all_after_pass: false do
2 | watch(%r{^spec/.+_spec\.rb})
3 | watch(%r{^lib/(.+)\.rb}) { |m| "spec/#{m[1]}_spec.rb" }
4 | watch(%r{^spec/spec_helper.rb}) { |m| "spec/" }
5 | end
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Redis Throttle Middleware
2 | [](https://travis-ci.org/lelylan/redis-throttle)
3 |
4 | This is a fork of the [Rack Throttle](http://github.com/datagraph/rack-throttle) middleware
5 | that provides logic for rate-limiting incoming HTTP requests to Rack applications using
6 | Redis as storage system. You can use `Rack::RedisThrottle` with any Ruby web framework based
7 | on Rack, including Ruby on Rails 3.0 and Sinatra. This gem was designed to experiment rate
8 | limit with Rails 3.x and [Doorkeeper](https://github.com/applicake/doorkeeper/).
9 |
10 | #### Thanks to Open Source
11 |
12 | Redis Throttle Middleware come to life thanks to the work I've made in Lelylan, an open source microservices architecture for the Internet of Things. If this project helped you in any way, think about giving us a star on Github.
13 |
14 |
15 |
16 |
17 |
18 | ## Features
19 |
20 | * Works only with Redis.
21 | * Automatically deploy by setting `ENV['REDIS_RATE_LIMIT_URL']`.
22 | * When the Redis connection is not available redis throttle skips the rate limit check (it does not blow up).
23 | * Automatically adds `X-RateLimit-Limit` and `X-RateLimit-Remaining` headers.
24 | * Set MockRedis while running your tests
25 |
26 |
27 | ## Requirements
28 |
29 | Redis Throttle is tested against MRI 1.9.3, 2.0, and 2.1.x.
30 |
31 |
32 | ## Installation
33 |
34 | Update your gem file and run `bundle`
35 |
36 | ```ruby
37 | gem 'redis-throttle', git: 'git://github.com/lelylan/redis-throttle.git'
38 | ```
39 |
40 | ## Rails Example
41 |
42 | ```ruby
43 | # At the top of config/application.rb
44 | require 'rack/redis_throttle'
45 |
46 | # Inside the class of config/application.rb
47 | class Application < Rails::Application
48 | # Limit the daily number of requests to 2500
49 | config.middleware.use Rack::RedisThrottle::Daily, max: 2500
50 | end
51 | ```
52 |
53 | ## Sinatra example
54 |
55 | ```ruby
56 | #!/usr/bin/env ruby -rubygems
57 | require 'sinatra'
58 | require 'rack/throttle'
59 | use Rack::Throttle::Daily, max: 2500
60 | ```
61 |
62 | ## Rack app example
63 |
64 | ```ruby
65 | #!/usr/bin/env rackup
66 | require 'rack/throttle'
67 | use Rack::Throttle::Daily max: 2500
68 |
69 | run lambda { |env| [200, {'Content-Type' => 'text/plain'}, "Hello, world!\n"] }
70 | ```
71 |
72 | ## Customizations
73 |
74 | You can fully customize the implementation details of any of these strategies
75 | by simply subclassing one of the default implementations.
76 |
77 | In our example we want to reach these goals:
78 |
79 | * We want to use Doorkeper as authorization system (OAuth2)
80 | * The number of daily requests are based on the user id and not the IP
81 | address (default in `Rack::RedisThrottle`)
82 | * The number of daily requests is dynamically set per user by the
83 | `user#rate_limit` field.
84 |
85 | Now subclass `Rack::RedisThrottle::Daily`, create your own rules and use it in your Rails app
86 |
87 | ```ruby
88 | # /lib/middlewares/daily_rate_limit
89 | require 'rack/redis_throttle'
90 |
91 | class DailyRateLimit < Rack::RedisThrottle::Daily
92 |
93 | def call(env)
94 | @user_rate_limit = user_rate_limit(env)
95 | super
96 | end
97 |
98 | def client_identifier(request)
99 | @user_rate_limit.respond_to?(:_id) ? @user_rate_limit._id : 'user-unknown'
100 | end
101 |
102 | def max_per_window(request)
103 | @user_rate_limit.respond_to?(:rate_limit) ? @user_rate_limit.rate_limit : 1000
104 | end
105 |
106 | # Rate limit only requests sending the access token
107 | def need_protection?(request)
108 | request.env.has_key?('HTTP_AUTHORIZATION')
109 | end
110 |
111 | private
112 |
113 | def user_rate_limit(env)
114 | request = Rack::Request.new(env)
115 | token = request.env['HTTP_AUTHORIZATION'].split(' ')[-1]
116 | access_token = Doorkeeper::AccessToken.where(token: token).first
117 | access_token ? User.find(access_token.resource_owner_id) : nil
118 | end
119 | end
120 | ```
121 |
122 | Now you can use it in your Rails App.
123 |
124 | ```ruby
125 | # config/application.rb
126 | module App
127 | class Application < Rails::Application
128 |
129 | # Puts your rate limit middleware as high as you can in your middleware stack
130 | config.middleware.insert_after Rack::Lock, 'DailyRateLimit'
131 | ```
132 |
133 | ## Rate limit headers
134 |
135 | `Rack::RedisThrottle` automatically sets two rate limits headers to let the
136 | client know the max number of requests and the one availables.
137 |
138 | HTTP/1.1 200 OK
139 | X-RateLimit-Limit: 5000
140 | X-RateLimit-Remaining: 4999
141 |
142 | When you exceed the API calls limit your request is forbidden.
143 |
144 | HTTP/1.1 403 Forbidden
145 | X-RateLimit-Limit: 5000
146 | X-RateLimit-Remaining: 0
147 |
148 |
149 | # Testing your apps
150 |
151 | While testing your Rack app Mock the redis connection by requiring this file
152 |
153 | ```ruby
154 | # Rate limit fake redis connection
155 | require 'rack/redis_throttle/testing/connection'
156 | ```
157 |
158 |
159 |
160 | ## HTTP client identification
161 |
162 | The rate-limiting counters stored and maintained by `Rack::RedisThrottle` are
163 | keyed to unique HTTP clients. By default, HTTP clients are uniquely identified
164 | by their IP address as returned by `Rack::Request#ip`. If you wish to instead
165 | use a more granular, application-specific identifier such as a session key or
166 | a user account name, you need only subclass a throttling strategy implementation
167 | and override the `#client_identifier` method.
168 |
169 |
170 | ## HTTP Response Codes and Headers
171 |
172 | When a client exceeds their rate limit, `Rack::RedisThrottle` by default returns
173 | a "403 Forbidden" response with an associated "Rate Limit Exceeded" message
174 | in the response body. If you need personalize it, for example with a
175 | JSON message.
176 |
177 | ```ruby
178 | def http_error(request, code, message = nil, headers = {})
179 | [ code, { 'Content-Type' => 'application/json' }.merge(headers), [body(request).to_json] ]
180 | end
181 |
182 | def body(request)
183 | {
184 | status: 403,
185 | method: request.env['REQUEST_METHOD'],
186 | request: "#{request.env['rack.url_scheme']}://#{request.env['HTTP_HOST']}#{request.env['PATH_INFO']}",
187 | description: 'Rate limit exceeded',
188 | daily_rate_limit: max_per_window(request)
189 | }
190 | end
191 | ```
192 |
193 |
194 | ## Notes
195 |
196 | ### Testing coverage
197 |
198 | Only `Rack::RedisThrottle::Daily` has a test suite. We will cover all
199 | the gem whenever I'll find more time and I'll see it being used widely.
200 |
201 |
202 | ## Contributing
203 |
204 | Fork the repo on github and send a pull requests with topic branches. Do not forget to
205 | provide specs to your contribution.
206 |
207 |
208 | ### Running specs
209 |
210 | * Fork and clone the repository.
211 | * Run `gem install bundler` to get the latest for the gemset.
212 | * Run `bundle install` for dependencies.
213 | * Run `bundle exec guard` and press enter to execute all specs.
214 |
215 |
216 | ## Spec guidelines
217 |
218 | Follow [betterspecs.org](http://betterspecs.org) guidelines.
219 |
220 |
221 | ## Coding guidelines
222 |
223 | Follow [github](https://github.com/styleguide/) guidelines.
224 |
225 |
226 | ## Feedback
227 |
228 | Use the [issue tracker](https://github.com/andreareginato/redis-throttle/issues) for bugs.
229 | [Mail](mailto:andrea.reginato@gmail.com) or [Tweet](http://twitter.com/andreareginato)
230 | us for any idea that can improve the project.
231 |
232 |
233 | ## Links
234 |
235 | * [GIT Repository](https://github.com/andreareginato/redis-throttle)
236 | * Initial inspiration from [Martinciu's dev blog](http://martinciu.com/2011/08/how-to-add-api-throttle-to-your-rails-app.html)
237 |
238 |
239 | ## Authors
240 |
241 | [Andrea Reginato](http://twitter.com/andreareginato)
242 | Thanks to [Lelylan](http://lelylan.com) for letting me share the code.
243 |
244 |
245 | ## Contributors
246 |
247 | Special thanks to the following people for submitting patches.
248 |
249 |
250 | ## Changelog
251 |
252 | See [CHANGELOG](redis-throttle/blob/master/CHANGELOG.md)
253 |
254 |
255 | ## Copyright
256 |
257 | Redis Throttle is free and unencumbered public domain software.
258 | See [LICENCE](redis-throttle/blob/master/LICENSE.md)
259 |
260 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'bundler/gem_tasks'
2 |
3 | task default: :spec
4 |
5 | require 'rspec/core/rake_task'
6 | RSpec::Core::RakeTask.new do |t|
7 | t.rspec_opts = ['--color', '--format doc']
8 | end
9 |
10 |
--------------------------------------------------------------------------------
/VERSION:
--------------------------------------------------------------------------------
1 | 0.0.1
2 |
--------------------------------------------------------------------------------
/lib/rack/redis_throttle.rb:
--------------------------------------------------------------------------------
1 | require 'rack'
2 | require 'rack/throttle'
3 | require 'redis'
4 | require 'hiredis'
5 | require 'redis-namespace'
6 | require 'active_support/core_ext/hash/reverse_merge'
7 | require 'active_support/core_ext/time/calculations'
8 | require 'active_support/core_ext/date/calculations'
9 |
10 | module Rack
11 | module RedisThrottle
12 | autoload :Connection, 'rack/redis_throttle/connection'
13 | autoload :Limiter, 'rack/redis_throttle/limiter'
14 | autoload :TimeWindow, 'rack/redis_throttle/time_window'
15 | autoload :Daily, 'rack/redis_throttle/daily'
16 | autoload :Interval, 'rack/redis_throttle/interval'
17 | autoload :VERSION, 'rack/redis_throttle/version'
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/rack/redis_throttle/connection.rb:
--------------------------------------------------------------------------------
1 | require 'rack'
2 |
3 | module Rack
4 | module RedisThrottle
5 | class Connection
6 |
7 | def self.create(options={})
8 | url = redis_provider || 'redis://localhost:6379/0'
9 | options.reverse_merge!({ url: url })
10 | client = Redis.connect(url: options[:url], driver: :hiredis)
11 | Redis::Namespace.new("redis-throttle:#{ENV['RACK_ENV']}:rate", redis: client)
12 | end
13 |
14 | private
15 |
16 | def self.redis_provider
17 | ENV['REDIS_RATE_LIMIT_URL']
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/rack/redis_throttle/daily.rb:
--------------------------------------------------------------------------------
1 | require 'rack'
2 |
3 | module Rack
4 | module RedisThrottle
5 | class Daily < TimeWindow
6 |
7 | def max_per_day(request)
8 | @max_per_day ||= options[:max_per_day] || options[:max] || 86400
9 | end
10 |
11 | alias_method :max_per_window, :max_per_day
12 |
13 | protected
14 |
15 | def cache_key(request)
16 | [super, Time.now.utc.strftime('%Y-%m-%d')].join(':')
17 | end
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/rack/redis_throttle/interval.rb:
--------------------------------------------------------------------------------
1 | module Rack
2 | module RedisThrottle
3 | class Interval < Limiter
4 |
5 | # Returns `true` if sufficient time has passed since the last request.
6 | def allowed?(request)
7 | t1 = request_start_time(request)
8 | t0 = cache_get(key = cache_key(request)) rescue nil
9 | allowed = !t0 || (dt = t1 - t0.to_f) >= minimum_interval
10 | begin
11 | cache_set(key, t1)
12 | allowed
13 | rescue => e
14 | # If an error occurred while trying to update the timestamp stored
15 | # in the cache, we will fall back to allowing the request through.
16 | # This prevents the Rack application blowing up merely due to a
17 | # backend cache server (Memcached, Redis, etc.) being offline.
18 | allowed = true
19 | end
20 | end
21 |
22 | def retry_after
23 | minimum_interval
24 | end
25 |
26 | def minimum_interval
27 | @min ||= (@options[:min] || 1.0).to_f
28 | end
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/rack/redis_throttle/limiter.rb:
--------------------------------------------------------------------------------
1 | require 'rack'
2 |
3 | module Rack
4 | module RedisThrottle
5 | class Limiter < Rack::Throttle::Limiter
6 |
7 | def initialize(app, options = {})
8 | options.reverse_merge!({ cache: Rack::RedisThrottle::Connection.create })
9 | @app, @options = app, options
10 | end
11 |
12 | def call(env)
13 | request = Rack::Request.new(env)
14 | if allowed?(request)
15 | status, headers, body = app.call(env)
16 | headers = rate_limit_headers(request, headers) if need_protection?(request)
17 | [status, headers, body]
18 | else
19 | rate_limit_exceeded(request)
20 | end
21 | end
22 |
23 | def cache
24 | begin
25 | case cache = (options[:cache] ||= {})
26 | when Proc then cache.call
27 | else cache
28 | end
29 | rescue => e
30 | puts "ERROR: Redis connection not available. Rescuing cache.call" if ENV['DEBUG']
31 | return {}
32 | end
33 | end
34 |
35 | def cache_has?(key)
36 | cache.get(key) rescue false
37 | end
38 |
39 | def cache_get(key, default = nil)
40 | begin
41 | cache.get(key) || default
42 | rescue Redis::BaseConnectionError => e
43 | puts "ERROR: Redis connection not available. Rescuing cache.get(key)" if ENV['DEBUG']
44 | return 0
45 | end
46 | end
47 |
48 | def cache_set(key, value)
49 | cache.set(key, value) rescue 0
50 | end
51 |
52 | def cache_incr(request)
53 | begin
54 | key = cache_key(request)
55 | count = cache.incr(key)
56 | cache.expire(key, 1.day) if count == 1
57 | count
58 | rescue Redis::BaseConnectionError => e
59 | puts "ERROR: Redis connection not available. Rescuing cache.incr(key)" if ENV['DEBUG']
60 | return 0
61 | end
62 | end
63 |
64 | def cache_key(request)
65 | id = client_identifier(request)
66 | case
67 | when options.has_key?(:key)
68 | options[:key].call(request)
69 | when options.has_key?(:key_prefix)
70 | [options[:key_prefix], id].join(':')
71 | else id
72 | end
73 | end
74 |
75 | # used to define the cache key
76 | def client_identifier(request)
77 | request.ip.to_s
78 | end
79 |
80 | def rate_limit_exceeded(request)
81 | headers = respond_to?(:retry_after) ? {'Retry-After' => retry_after.to_f.ceil.to_s} : {}
82 | http_error(request, options[:code] || 403, options[:message] || 'Rate Limit Exceeded', headers)
83 | end
84 |
85 | def http_error(request, code, message = nil, headers = {})
86 | [code, {'Content-Type' => 'text/plain; charset=utf-8'}.merge(headers),
87 | [ http_status(code) + (message.nil? ? "\n" : " (#{message})")]]
88 | end
89 | end
90 | end
91 | end
92 |
--------------------------------------------------------------------------------
/lib/rack/redis_throttle/testing/connection.rb:
--------------------------------------------------------------------------------
1 | require 'rack'
2 | require 'mock_redis'
3 |
4 | module Rack
5 | module RedisThrottle
6 | class Connection
7 |
8 | def self.create(options={})
9 | MockRedis.new
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/rack/redis_throttle/time_window.rb:
--------------------------------------------------------------------------------
1 | module Rack
2 | module RedisThrottle
3 | class TimeWindow < Limiter
4 |
5 | # Check my rate limit
6 | def allowed?(request)
7 | case
8 | when whitelisted?(request) then true
9 | when blacklisted?(request) then false
10 | else need_protection?(request) ? cache_incr(request) <= max_per_window(request) : true
11 | end
12 | end
13 |
14 | def need_protection?(request)
15 | true
16 | end
17 |
18 | def rate_limit_headers(request, headers)
19 | headers['X-RateLimit-Limit'] = max_per_window(request).to_s
20 | headers['X-RateLimit-Remaining'] = ([0, max_per_window(request) - (cache_get(cache_key(request)).to_i rescue 1)].max).to_s
21 | headers
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/rack/redis_throttle/version.rb:
--------------------------------------------------------------------------------
1 | module Rack
2 | module RedisThrottle
3 | module VERSION
4 | MAJOR = 0
5 | MINOR = 1
6 | TINY = 0
7 | EXTRA = nil
8 |
9 | STRING = [MAJOR, MINOR, TINY].join('.')
10 | STRING << "-#{EXTRA}" if EXTRA
11 |
12 | ##
13 | # @return [String]
14 | def self.to_s() STRING end
15 |
16 | ##
17 | # @return [String]
18 | def self.to_str() STRING end
19 |
20 | ##
21 | # @return [Array(Integer, Integer, Integer)]
22 | def self.to_a() [MAJOR, MINOR, TINY] end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/redis_throttle.gemspec:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby -rubygems
2 | # -*- encoding: utf-8 -*-
3 |
4 | Gem::Specification.new do |gem|
5 | gem.version = File.read('VERSION').chomp
6 | gem.date = File.mtime('VERSION').strftime('%Y-%m-%d')
7 | gem.name = 'redis-throttle'
8 | gem.homepage = 'https://github.com/andreareginato'
9 | gem.summary = 'HTTP request rate limiter for Rack applications with Redigem.'
10 | gem.description = 'Rack middleware for rate-limiting incoming HTTP requests with Redigem.'
11 |
12 | gem.authors = ['Andrea Reginato']
13 | gem.email = ['andrea.reginato@gmail.com']
14 |
15 | gem.platform = Gem::Platform::RUBY
16 | gem.files = %w(CONTRIBUTORS.md README.md LICENSE.md VERSION) + Dir.glob('lib/**/*.rb')
17 | gem.bindir = %q(bin)
18 | gem.executables = %w()
19 | gem.default_executable = gem.executables.first
20 | gem.require_paths = %w(lib)
21 | gem.extensions = %w()
22 | gem.test_files = %w()
23 | gem.has_rdoc = false
24 |
25 | gem.rubyforge_project = 'redis-throttle'
26 | gem.post_install_message = nil
27 |
28 | gem.add_dependency 'rack'
29 | gem.add_dependency 'rack-throttle'
30 | gem.add_dependency 'redis'
31 | gem.add_dependency 'hiredis'
32 | gem.add_dependency 'redis-namespace'
33 | gem.add_dependency 'activesupport'
34 |
35 | gem.add_development_dependency 'rake'
36 | gem.add_development_dependency 'rspec'
37 | gem.add_development_dependency 'sinatra'
38 | gem.add_development_dependency 'foreman'
39 | gem.add_development_dependency 'timecop'
40 | gem.add_development_dependency 'mock_redis'
41 | gem.add_development_dependency 'hashie'
42 | gem.add_development_dependency 'rack-test'
43 | gem.add_development_dependency 'rb-fsevent' if RUBY_PLATFORM =~ /darwin/i
44 | gem.add_development_dependency 'guard'
45 | gem.add_development_dependency 'guard-rspec'
46 | gem.add_development_dependency 'fuubar'
47 | gem.add_development_dependency 'growl'
48 | end
49 |
--------------------------------------------------------------------------------
/spec/fixtures/fake_app.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'sinatra'
3 | require 'rack/redis_throttle'
4 |
5 | module Rack
6 | module Test
7 | class FakeApp < Sinatra::Base
8 |
9 | get '/foo' do
10 | 'Hello Redis Throttler!'
11 | end
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/spec/rack/redis_throttle/daily_no_redis_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 |
4 | describe Rack::RedisThrottle::Daily do
5 |
6 | describe 'when the redis connection is missing' do
7 |
8 | # middleware settings
9 | let(:cache) { Rack::RedisThrottle::Connection.create(url: 'redis://localhost:9999/0') }
10 | before { app.options[:max] = 5000 }
11 | before { app.options[:cache] = cache }
12 |
13 |
14 | describe 'when makes a request' do
15 |
16 | describe 'with the Authorization header' do
17 |
18 | describe 'when the rate limit is not reached' do
19 |
20 | before { get '/foo' }
21 |
22 | it 'returns a 200 status' do
23 | expect(last_response.status).to eq(200)
24 | end
25 |
26 | it 'returns the remaining requests header' do
27 | expect(last_response.headers['X-RateLimit-Remaining']).to eq('5000')
28 | end
29 | end
30 | end
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/spec/rack/redis_throttle/daily_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Rack::RedisThrottle::Daily do
4 |
5 | # middleware settings
6 | before { app.options[:max] = 5000 }
7 | before { app.options[:cache] = MockRedis.new }
8 |
9 |
10 | let(:cache) { app.options[:cache] }
11 |
12 | let(:time_key) { Time.now.utc.strftime('%Y-%m-%d') }
13 | let(:client_key) { '127.0.0.1' }
14 | let(:cache_key) { "#{client_key}:#{time_key}" }
15 |
16 | let(:tomorrow_time_key) { Time.now.tomorrow.utc.strftime('%Y-%m-%d') }
17 | let(:tomorrow_cache_key) { "#{client_key}:#{tomorrow_time_key}" }
18 |
19 | before { cache.set cache_key, 1 }
20 | before { cache.set tomorrow_cache_key, 1 }
21 |
22 | describe 'when makes a request' do
23 |
24 | describe 'with the Authorization header' do
25 |
26 | describe 'when the rate limit is not reached' do
27 |
28 | before { get '/foo' }
29 |
30 | it 'returns a 200 status' do
31 | expect(last_response.status).to eq(200)
32 | end
33 |
34 | it 'returns the requests limit headers' do
35 | expect(last_response.headers['X-RateLimit-Limit']).not_to be_nil
36 | end
37 |
38 | it 'returns the remaining requests header' do
39 | expect(last_response.headers['X-RateLimit-Remaining']).not_to be_nil
40 | end
41 |
42 | it 'decreases the available requests' do
43 | previous = last_response.headers['X-RateLimit-Remaining'].to_i
44 | get '/', {}, 'AUTHORIZATION' => 'Bearer '
45 | expect(previous).to eq(last_response.headers['X-RateLimit-Remaining'].to_i + 1)
46 | end
47 | end
48 |
49 | describe 'when reaches the rate limit' do
50 |
51 | before { cache.set cache_key, 5000 }
52 | before { get '/foo' }
53 |
54 | it 'returns a 403 status' do
55 | expect(last_response.status).to eq(403)
56 | end
57 |
58 | it 'returns a rate limited exceeded body' do
59 | expect(last_response.body).to eq('403 Forbidden (Rate Limit Exceeded)')
60 | end
61 |
62 | describe 'when comes the new day' do
63 |
64 | # If we are the 12-12-07 (any time) it gives the 12-12-08 00:00:00 UTC
65 | let!(:tomorrow) { Time.now.utc.tomorrow.beginning_of_day }
66 | before { Time.now.utc }
67 | before { Timecop.travel(tomorrow) }
68 | before { Time.now.utc }
69 | before { get '/foo', {}, 'AUTHORIZATION' => 'Bearer ' }
70 | after { Timecop.return }
71 |
72 | it 'returns a 200 status' do
73 | expect(last_response.status).to eq(200)
74 | end
75 |
76 | it 'returns a new rate limit' do
77 | end
78 | end
79 | end
80 | end
81 | end
82 | end
83 |
--------------------------------------------------------------------------------
/spec/rack/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'bundler/setup'
3 | require 'rack'
4 | require 'rack/test'
5 | require 'rspec'
6 | require 'rack/throttle'
7 |
8 | Dir[File.dirname(__FILE__) + '/support/**/*.rb'].each {|f| require f}
9 |
10 | require 'rack/redis_throttle'
11 | require File.dirname(__FILE__) + '/fixtures/fake_app'
12 |
13 | RSpec.configure do |config|
14 | config.mock_with :rspec
15 | config.include Rack::Test::Methods
16 |
17 | def app
18 | Rack::Lint.new(Rack::Test::FakeApp.new)
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'bundler/setup'
3 | require 'rack'
4 | require 'rack/test'
5 | require 'mock_redis'
6 | require 'rspec'
7 | require 'timecop'
8 |
9 | require File.dirname(__FILE__) + '/fixtures/fake_app'
10 |
11 | Dir[File.dirname(__FILE__) + '/support/**/*.rb'].each {|f| require f}
12 |
13 | RSpec.configure do |config|
14 | config.mock_with :rspec
15 | config.include Rack::Test::Methods
16 | end
17 |
18 | def app
19 | @target_app ||= Rack::Lint.new(Rack::Test::FakeApp.new)
20 | @daily_app ||= Rack::RedisThrottle::Daily.new(@target_app)
21 | end
22 |
--------------------------------------------------------------------------------
/spec/support/matchers/body.rb:
--------------------------------------------------------------------------------
1 | RSpec::Matchers.define :have_body do |expected|
2 | match do |response|
3 | response.body.should == expected
4 | end
5 |
6 | description do
7 | "have body #{expected.inspect}"
8 | end
9 | end
10 |
11 |
12 | def mock_cache
13 | MockRedis.new
14 | end
15 |
--------------------------------------------------------------------------------