rack-berater 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2a5812fba3c2523be30274bcc192d67ba7cebbb1d2b52dcc0ca403d69c39630c
4
- data.tar.gz: 46152472d4af26a86e506a9150ff9c56743490707b630ecddc94beb0e63f7448
3
+ metadata.gz: 3475519672f1074d749de982b36d3a2d0d15a468d38d35b91e0f237de02ba2f8
4
+ data.tar.gz: fd08c05ccc694adc8ca540a2468956144c0d180611dd4622070e15aa8dd120ef
5
5
  SHA512:
6
- metadata.gz: '079407a6fd8d63c849809c972ef15739de09bd490c1e3a4046ae047dbba8b36cb7e5f0606c17b5e5a6a74ce8ec9fa95f61139afc3937a5c30643fe85ff59e91d'
7
- data.tar.gz: 7d6ede65719ed13181c25aca35a48c941ee9c4777caa4f56eede0552b4acdc39d2989069659217327a0b3bc494d0c178b1ee1faa21e42f5c70dd4067f4fb077e
6
+ metadata.gz: 88428422463a296369f8a2f141c74dadc90f132e2d2072c3505bf33012e344668277e23146125cf43bca55a7d6ec83da0871d6e5b23684a72f3875e48f5d43d7
7
+ data.tar.gz: be42f6faa19469ccbfed7995f02b81533051b828e688779d8b5986be6a83174baf71c8ca399b9fabb419766a59fbd8a0c366eefdee977865d7c215970e815527
@@ -1,7 +1,7 @@
1
1
  require 'rails/railtie'
2
2
 
3
3
  module Rack
4
- module Berater
4
+ class Berater
5
5
  class Railtie < Rails::Railtie
6
6
  initializer "rack.berater.initializer" do |app|
7
7
  app.middleware.use Rack::Berater
@@ -1,5 +1,5 @@
1
1
  module Rack
2
- module Berater
3
- VERSION = "0.0.2"
2
+ class Berater
3
+ VERSION = "0.1.0"
4
4
  end
5
5
  end
data/lib/rack/berater.rb CHANGED
@@ -1,9 +1,61 @@
1
+ require "berater"
2
+ require "rack"
1
3
  require "rack/berater/version"
4
+ require "set"
2
5
 
3
6
  module Rack
4
- module Berater
5
- autoload :Handler, "rack/berater/handler"
6
- # autoload :Limiter, "rack/berater/limiter"
7
+ class Berater
7
8
  autoload :Railtie, "rack/berater/railtie"
9
+
10
+ ERROR_TYPES = Set.new([ ::Berater::Overloaded ])
11
+
12
+ def initialize(app, options = {})
13
+ @app = app
14
+ @enabled = options[:enabled?]
15
+ @limiter = options[:limiter]
16
+ @options = {
17
+ headers: {},
18
+ status_code: options.fetch(:status_code, 429),
19
+ }
20
+
21
+ # configure body
22
+ @options[:body] = case options[:body]
23
+ when true, nil
24
+ Rack::Utils::HTTP_STATUS_CODES[@options[:status_code]]
25
+ when false
26
+ nil
27
+ when String
28
+ options[:body]
29
+ else
30
+ raise ArgumentError, "invalid :body option: #{options[:body]}"
31
+ end
32
+
33
+ # configure headers
34
+ if @options[:body]
35
+ @options[:headers][Rack::CONTENT_TYPE] = "text/plain"
36
+ end
37
+ @options[:headers].update(options.fetch(:headers, {}))
38
+ end
39
+
40
+ def call(env)
41
+ if enabled?(env)
42
+ @limiter.limit { @app.call(env) }
43
+ else
44
+ @app.call(env)
45
+ end
46
+ rescue *ERROR_TYPES => e
47
+ [
48
+ @options[:status_code],
49
+ @options[:headers],
50
+ [ @options[:body] ].compact,
51
+ ]
52
+ end
53
+
54
+ private
55
+
56
+ def enabled?(env)
57
+ return false unless @limiter
58
+ @enabled.nil? ? true : @enabled.call(env)
59
+ end
8
60
  end
9
61
  end
data/lib/rack-berater.rb CHANGED
@@ -1,3 +1 @@
1
- require "berater"
2
- require "rack"
3
1
  require "rack/berater"
@@ -0,0 +1,96 @@
1
+ describe Rack::Berater do
2
+ before do
3
+ app.use described_class, limiter: limiter, enabled?: enabled?
4
+ end
5
+ let(:limiter) { nil }
6
+ let(:enabled?) { nil }
7
+
8
+ let(:app) do
9
+ Rack::Builder.new do
10
+ use Rack::Lint
11
+ run (lambda do |env|
12
+ [200, {"Content-Type" => "text/plain"}, ["OK"]]
13
+ end)
14
+ end
15
+ end
16
+ let(:response) { get "/" }
17
+
18
+ shared_examples "works nominally" do
19
+ it "has the correct status code" do
20
+ expect(response.status).to eq 200
21
+ end
22
+
23
+ it "has the correct headers" do
24
+ expect(response.headers).to eq({
25
+ "Content-Type" => "text/plain",
26
+ "Content-Length" => "2",
27
+ })
28
+ end
29
+
30
+ it "has the correct body" do
31
+ expect(response.body).to eq "OK"
32
+ end
33
+ end
34
+
35
+ context "without a limiter" do
36
+ before { Berater.test_mode = :fail }
37
+
38
+ include_examples "works nominally"
39
+ end
40
+
41
+ describe "limiter option" do
42
+ let(:limiter) { ::Berater::Unlimiter.new }
43
+
44
+ include_examples "works nominally"
45
+
46
+ it "calls the limiter" do
47
+ expect(limiter).to receive(:limit).and_call_original
48
+ response
49
+ end
50
+
51
+ context "when operating beyond limits" do
52
+ before { Berater.test_mode = :fail }
53
+
54
+ it "returns an error" do
55
+ expect(response.status).to eq 429
56
+ end
57
+ end
58
+ end
59
+
60
+ describe "enabled? option" do
61
+ after { expect(response.status).to eq 200 }
62
+
63
+ let(:enabled?) { double }
64
+
65
+ context "when there is a limiter" do
66
+ let(:limiter) { ::Berater::Unlimiter.new }
67
+
68
+ it "should be called with the env hash" do
69
+ expect(enabled?).to receive(:call) do |env|
70
+ expect(env).to be_a Hash
71
+ expect(Rack::Request.new(env).path).to eq "/"
72
+ end
73
+ end
74
+
75
+ context "when enabled" do
76
+ it "should call the limiter" do
77
+ expect(enabled?).to receive(:call).and_return(true)
78
+ expect(limiter).to receive(:limit).and_call_original
79
+ end
80
+ end
81
+
82
+ context "when disabled" do
83
+ it "should not call the limiter" do
84
+ expect(enabled?).to receive(:call).and_return(false)
85
+ expect(limiter).not_to receive(:limit)
86
+ end
87
+ end
88
+ end
89
+
90
+ context "when there is no limiter" do
91
+ it "should not call enabled?" do
92
+ expect(enabled?).not_to receive(:call)
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,145 @@
1
+ describe Rack::Berater do
2
+ let(:app) do
3
+ Rack::Builder.new do
4
+ use Rack::Lint
5
+ run (lambda do |env|
6
+ Berater::Unlimiter() do
7
+ [200, {"Content-Type" => "text/plain"}, ["OK"]]
8
+ end
9
+ end)
10
+ end
11
+ end
12
+ let(:response) { get "/" }
13
+
14
+ shared_examples "works nominally" do
15
+ it "has the correct status code" do
16
+ expect(response.status).to eq 200
17
+ end
18
+
19
+ it "has the correct headers" do
20
+ expect(response.headers).to eq({
21
+ "Content-Type" => "text/plain",
22
+ "Content-Length" => "2",
23
+ })
24
+ end
25
+
26
+ it "has the correct body" do
27
+ expect(response.body).to eq "OK"
28
+ end
29
+ end
30
+
31
+ context "without middleware" do
32
+ include_examples "works nominally"
33
+
34
+ it "does not catch limit errors" do
35
+ Berater.test_mode = :fail
36
+ expect {
37
+ response
38
+ }.to be_overloaded
39
+ end
40
+ end
41
+
42
+ context "with middleware using default settings" do
43
+ before { app.use described_class }
44
+
45
+ include_examples "works nominally"
46
+
47
+ it "catches and transforms limit errors" do
48
+ Berater.test_mode = :fail
49
+ expect(response.status).to eq 429
50
+ expect(response.body).to eq "Too Many Requests"
51
+ end
52
+ end
53
+
54
+ context "with middleware using custom settings" do
55
+ before do
56
+ app.use described_class, options
57
+ Berater.test_mode = :fail
58
+ end
59
+
60
+ context "with a custom body" do
61
+ context "with body nil" do
62
+ let(:options) { { body: nil } }
63
+
64
+ it "should default to sending a body" do
65
+ expect(response.body).to eq "Too Many Requests"
66
+ end
67
+ end
68
+
69
+ context "with body disabled" do
70
+ let(:options) { { body: false } }
71
+
72
+ it "should not send a body" do
73
+ expect(response.body).to be_empty
74
+ end
75
+
76
+ it "should not send the Content-Type header" do
77
+ expect(response.headers.keys).not_to include(Rack::CONTENT_TYPE)
78
+ end
79
+ end
80
+
81
+ context "with a string" do
82
+ let(:body) { "none shall pass!" }
83
+ let(:options) { { body: body } }
84
+
85
+ it "should send the custom string" do
86
+ expect(response.body).to eq body
87
+ end
88
+ end
89
+ end
90
+
91
+ context "with custom headers" do
92
+ context "with an extra header" do
93
+ let(:options) { { headers: { Rack::CACHE_CONTROL => "no-cache" } } }
94
+
95
+ it "should contain the default headers" do
96
+ expect(response.headers.keys).to include(Rack::CONTENT_TYPE)
97
+ end
98
+
99
+ it "should also contain the custom header" do
100
+ expect(response.headers).to include(options[:headers])
101
+ end
102
+ end
103
+
104
+ context "with a new content type" do
105
+ let(:options) { { headers: { Rack::CONTENT_TYPE => "application/json" } } }
106
+
107
+ it "should override the Content-Type header" do
108
+ expect(response.headers).to include(options[:headers])
109
+ end
110
+ end
111
+ end
112
+
113
+ context "with custom status code" do
114
+ let(:options) { { status_code: 503 } }
115
+
116
+ it "catches and transforms limit errors" do
117
+ expect(response.status).to eq 503
118
+ expect(response.body).to eq "Service Unavailable"
119
+ end
120
+ end
121
+ end
122
+
123
+ context "with custom error type" do
124
+ before do
125
+ app.use described_class
126
+ expect(Berater::Limiter).to receive(:new).and_raise(IOError)
127
+ end
128
+
129
+ it "normally crashes the app" do
130
+ expect { response }.to raise_error(IOError)
131
+ end
132
+
133
+ context "when an error type is registered with middleware" do
134
+ around do |example|
135
+ Rack::Berater::ERROR_TYPES << IOError
136
+ example.run
137
+ Rack::Berater::ERROR_TYPES.delete(IOError)
138
+ end
139
+
140
+ it "catches and transforms limit errors" do
141
+ expect(response.status).to eq 429
142
+ end
143
+ end
144
+ end
145
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-berater
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Pepper
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-09-13 00:00:00.000000000 Z
11
+ date: 2021-10-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: berater
@@ -116,10 +116,10 @@ extra_rdoc_files: []
116
116
  files:
117
117
  - lib/rack-berater.rb
118
118
  - lib/rack/berater.rb
119
- - lib/rack/berater/handler.rb
120
119
  - lib/rack/berater/railtie.rb
121
120
  - lib/rack/berater/version.rb
122
- - spec/handler_spec.rb
121
+ - spec/limiter_spec.rb
122
+ - spec/rescuer_spec.rb
123
123
  homepage: https://github.com/dpep/rack-berater
124
124
  licenses:
125
125
  - MIT
@@ -139,9 +139,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
139
139
  - !ruby/object:Gem::Version
140
140
  version: '0'
141
141
  requirements: []
142
- rubygems_version: 3.0.8
142
+ rubygems_version: 3.1.4
143
143
  signing_key:
144
144
  specification_version: 4
145
145
  summary: Rack::Berater
146
146
  test_files:
147
- - spec/handler_spec.rb
147
+ - spec/rescuer_spec.rb
148
+ - spec/limiter_spec.rb
@@ -1,43 +0,0 @@
1
- module Rack
2
- module Berater
3
- class Handler
4
- def initialize(app, options = {})
5
- @app = app
6
- @options = {
7
- status_code: options.fetch(:status_code, 429),
8
- headers: {
9
- Rack::CONTENT_TYPE => "text/plain",
10
- }.update(options.fetch(:headers, {})),
11
- body: options.fetch(:body, true),
12
- }
13
- end
14
-
15
- def call(env)
16
- @app.call(env)
17
- rescue ::Berater::Overloaded => e
18
- code = @options[:status_code]
19
-
20
- body = case @options[:body]
21
- when true
22
- Rack::Utils::HTTP_STATUS_CODES[code]
23
- when nil, false
24
- nil
25
- when String
26
- @options[:body]
27
- when Proc
28
- @options[:body].call(env, e)
29
- else
30
- raise ArgumentError, "invalid :body option: #{@options[:body]}"
31
- end
32
-
33
- headers = body ? @options[:headers] : {}
34
-
35
- [
36
- code,
37
- headers,
38
- [ body ].compact,
39
- ]
40
- end
41
- end
42
- end
43
- end
data/spec/handler_spec.rb DELETED
@@ -1,126 +0,0 @@
1
- describe Rack::Berater::Handler do
2
- let(:app) do
3
- Rack::Builder.new do
4
- use Rack::Lint
5
- run (lambda do |env|
6
- raise ::Berater::Overloaded if ::Berater.test_mode == :fail
7
- [200, {"Content-Type" => "text/plain"}, ["OK"]]
8
- end)
9
- end
10
- end
11
- let(:response) { get "/" }
12
-
13
- shared_examples "works nominally" do
14
- it "has the correct status code" do
15
- expect(response.status).to eq 200
16
- end
17
-
18
- it "has the correct headers" do
19
- expect(response.headers).to eq({
20
- "Content-Type" => "text/plain",
21
- "Content-Length" => "2",
22
- })
23
- end
24
-
25
- it "has the correct body" do
26
- expect(response.body).to eq "OK"
27
- end
28
- end
29
-
30
- context "without Handler" do
31
- include_examples "works nominally"
32
-
33
- it "does not catch limit errors" do
34
- Berater.test_mode = :fail
35
- expect {
36
- response
37
- }.to be_overloaded
38
- end
39
- end
40
-
41
- context "with Handler using default settings" do
42
- context "with default settings" do
43
- before { app.use described_class }
44
-
45
- include_examples "works nominally"
46
-
47
- it "catches and transforms limit errors" do
48
- Berater.test_mode = :fail
49
- expect(response.status).to eq 429
50
- expect(response.body).to eq "Too Many Requests"
51
- end
52
- end
53
- end
54
-
55
- context "with Handler using custom settings" do
56
- before do
57
- app.use described_class, options
58
- Berater.test_mode = :fail
59
- end
60
-
61
- context "with custom status code" do
62
- let(:options) { { status_code: 503 } }
63
-
64
- it "catches and transforms limit errors" do
65
- expect(response.status).to eq 503
66
- expect(response.body).to eq "Service Unavailable"
67
- end
68
- end
69
-
70
- context "with body disabled" do
71
- let(:options) { { body: false } }
72
-
73
- it "should not send a body" do
74
- expect(response.body).to be_empty
75
- end
76
-
77
- it "should not send the Content-Type header" do
78
- expect(response.headers.keys).not_to include(Rack::CONTENT_TYPE)
79
- end
80
- end
81
-
82
- context "with body nil" do
83
- let(:options) { { body: nil } }
84
-
85
- it "should not send a body" do
86
- expect(response.body).to be_empty
87
- end
88
- end
89
-
90
- context "with custom body" do
91
- let(:body) { "none shall pass!" }
92
- let(:options) { { body: body } }
93
-
94
- it "should send the custom string" do
95
- expect(response.body).to eq body
96
- end
97
- end
98
-
99
- context "with a dynamic body" do
100
- let(:body) { "none shall pass!" }
101
- let(:fn) { proc { body } }
102
- let(:options) { { body: fn } }
103
-
104
- it "should call the Proc and send the result" do
105
- expect(response.body).to eq body
106
- end
107
-
108
- it "should pass in the env and error" do
109
- expect(fn).to receive(:call).with(Hash, ::Berater::Overloaded)
110
- response
111
- end
112
- end
113
-
114
- context "with custom headers" do
115
- let(:options) { { headers: { Rack::CACHE_CONTROL => "no-cache" } } }
116
-
117
- it "should contain the default headers" do
118
- expect(response.headers.keys).to include(Rack::CONTENT_TYPE)
119
- end
120
-
121
- it "should also contain custom header" do
122
- expect(response.headers).to include(options[:headers])
123
- end
124
- end
125
- end
126
- end