hanami 2.0.3 → 2.1.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -2
  3. data/LICENSE.md +1 -1
  4. data/README.md +26 -10
  5. data/hanami.gemspec +2 -2
  6. data/lib/hanami/app.rb +5 -0
  7. data/lib/hanami/config/actions.rb +4 -11
  8. data/lib/hanami/config/assets.rb +84 -0
  9. data/lib/hanami/config/null_config.rb +3 -0
  10. data/lib/hanami/config/views.rb +0 -4
  11. data/lib/hanami/config.rb +71 -5
  12. data/lib/hanami/extensions/action/slice_configured_action.rb +15 -7
  13. data/lib/hanami/extensions/action.rb +8 -6
  14. data/lib/hanami/extensions/router/errors.rb +58 -0
  15. data/lib/hanami/extensions/view/context.rb +129 -60
  16. data/lib/hanami/extensions/view/part.rb +26 -0
  17. data/lib/hanami/extensions/view/scope.rb +26 -0
  18. data/lib/hanami/extensions/view/slice_configured_context.rb +0 -2
  19. data/lib/hanami/extensions/view/slice_configured_helpers.rb +44 -0
  20. data/lib/hanami/extensions/view/slice_configured_view.rb +106 -21
  21. data/lib/hanami/extensions/view/standard_helpers.rb +18 -0
  22. data/lib/hanami/extensions.rb +10 -3
  23. data/lib/hanami/helpers/assets_helper.rb +752 -0
  24. data/lib/hanami/helpers/form_helper/form_builder.rb +1391 -0
  25. data/lib/hanami/helpers/form_helper/values.rb +75 -0
  26. data/lib/hanami/helpers/form_helper.rb +213 -0
  27. data/lib/hanami/middleware/assets.rb +21 -0
  28. data/lib/hanami/middleware/public_errors_app.rb +75 -0
  29. data/lib/hanami/middleware/render_errors.rb +90 -0
  30. data/lib/hanami/providers/assets.rb +44 -0
  31. data/lib/hanami/rake_tasks.rb +19 -18
  32. data/lib/hanami/settings.rb +1 -1
  33. data/lib/hanami/slice.rb +48 -2
  34. data/lib/hanami/slice_configurable.rb +3 -2
  35. data/lib/hanami/version.rb +1 -1
  36. data/lib/hanami/web/rack_logger.rb +1 -1
  37. data/lib/hanami.rb +3 -3
  38. data/spec/integration/action/view_rendering/view_context_spec.rb +221 -0
  39. data/spec/integration/action/view_rendering_spec.rb +0 -18
  40. data/spec/integration/assets/assets_spec.rb +101 -0
  41. data/spec/integration/assets/serve_static_assets_spec.rb +152 -0
  42. data/spec/integration/logging/exception_logging_spec.rb +115 -0
  43. data/spec/integration/logging/notifications_spec.rb +68 -0
  44. data/spec/integration/logging/request_logging_spec.rb +128 -0
  45. data/spec/integration/rack_app/middleware_spec.rb +22 -22
  46. data/spec/integration/rack_app/rack_app_spec.rb +3 -220
  47. data/spec/integration/rake_tasks_spec.rb +107 -0
  48. data/spec/integration/view/config/default_context_spec.rb +149 -0
  49. data/spec/integration/view/{inflector_spec.rb → config/inflector_spec.rb} +1 -1
  50. data/spec/integration/view/config/part_class_spec.rb +147 -0
  51. data/spec/integration/view/config/part_namespace_spec.rb +103 -0
  52. data/spec/integration/view/config/paths_spec.rb +119 -0
  53. data/spec/integration/view/config/scope_class_spec.rb +147 -0
  54. data/spec/integration/view/config/scope_namespace_spec.rb +103 -0
  55. data/spec/integration/view/config/template_spec.rb +38 -0
  56. data/spec/integration/view/context/assets_spec.rb +3 -9
  57. data/spec/integration/view/context/request_spec.rb +3 -7
  58. data/spec/integration/view/helpers/form_helper_spec.rb +174 -0
  59. data/spec/integration/view/helpers/part_helpers_spec.rb +124 -0
  60. data/spec/integration/view/helpers/scope_helpers_spec.rb +84 -0
  61. data/spec/integration/view/helpers/user_defined_helpers/part_helpers_spec.rb +162 -0
  62. data/spec/integration/view/helpers/user_defined_helpers/scope_helpers_spec.rb +119 -0
  63. data/spec/integration/view/slice_configuration_spec.rb +9 -9
  64. data/spec/integration/web/render_detailed_errors_spec.rb +107 -0
  65. data/spec/integration/web/render_errors_spec.rb +242 -0
  66. data/spec/spec_helper.rb +1 -1
  67. data/spec/support/app_integration.rb +46 -2
  68. data/spec/support/matchers.rb +32 -0
  69. data/spec/unit/hanami/config/actions/content_security_policy_spec.rb +24 -36
  70. data/spec/unit/hanami/config/actions/csrf_protection_spec.rb +4 -3
  71. data/spec/unit/hanami/config/actions/default_values_spec.rb +3 -6
  72. data/spec/unit/hanami/config/render_detailed_errors_spec.rb +25 -0
  73. data/spec/unit/hanami/config/render_errors_spec.rb +25 -0
  74. data/spec/unit/hanami/config/views_spec.rb +0 -18
  75. data/spec/unit/hanami/env_spec.rb +11 -25
  76. data/spec/unit/hanami/extensions/view/context_spec.rb +59 -0
  77. data/spec/unit/hanami/helpers/assets_helper/asset_url_spec.rb +109 -0
  78. data/spec/unit/hanami/helpers/assets_helper/audio_tag_spec.rb +132 -0
  79. data/spec/unit/hanami/helpers/assets_helper/favicon_link_tag_spec.rb +91 -0
  80. data/spec/unit/hanami/helpers/assets_helper/image_tag_spec.rb +92 -0
  81. data/spec/unit/hanami/helpers/assets_helper/javascript_tag_spec.rb +143 -0
  82. data/spec/unit/hanami/helpers/assets_helper/stylesheet_link_tag_spec.rb +126 -0
  83. data/spec/unit/hanami/helpers/assets_helper/video_tag_spec.rb +132 -0
  84. data/spec/unit/hanami/helpers/form_helper_spec.rb +2826 -0
  85. data/spec/unit/hanami/router/errors/not_allowed_error_spec.rb +27 -0
  86. data/spec/unit/hanami/router/errors/not_found_error_spec.rb +22 -0
  87. data/spec/unit/hanami/slice_configurable_spec.rb +18 -0
  88. data/spec/unit/hanami/version_spec.rb +1 -1
  89. data/spec/unit/hanami/web/rack_logger_spec.rb +1 -1
  90. metadata +95 -35
  91. data/lib/hanami/assets/app_config.rb +0 -61
  92. data/lib/hanami/assets/config.rb +0 -53
  93. data/spec/integration/action/view_integration_spec.rb +0 -165
  94. data/spec/integration/view/part_namespace_spec.rb +0 -96
  95. data/spec/integration/view/path_spec.rb +0 -56
  96. data/spec/integration/view/template_spec.rb +0 -68
  97. data/spec/isolation/hanami/application/already_configured_spec.rb +0 -19
  98. data/spec/isolation/hanami/application/inherit_anonymous_class_spec.rb +0 -10
  99. data/spec/isolation/hanami/application/inherit_concrete_class_spec.rb +0 -14
  100. data/spec/isolation/hanami/application/not_configured_spec.rb +0 -9
  101. data/spec/isolation/hanami/application/routes/configured_spec.rb +0 -44
  102. data/spec/isolation/hanami/application/routes/not_configured_spec.rb +0 -16
  103. data/spec/isolation/hanami/boot/success_spec.rb +0 -50
@@ -7,7 +7,7 @@ module Hanami
7
7
  # @api private
8
8
  module Version
9
9
  # @api public
10
- VERSION = "2.0.3"
10
+ VERSION = "2.1.0.beta2"
11
11
 
12
12
  # @since 0.9.0
13
13
  # @api private
@@ -29,7 +29,7 @@ module Hanami
29
29
  ROUTER_PARAMS = "router.params"
30
30
  private_constant :ROUTER_PARAMS
31
31
 
32
- CONTENT_LENGTH = "Content-Length"
32
+ CONTENT_LENGTH = "CONTENT_LENGTH"
33
33
  private_constant :CONTENT_LENGTH
34
34
 
35
35
  MILISECOND = "ms"
data/lib/hanami.rb CHANGED
@@ -18,7 +18,7 @@ module Hanami
18
18
  def self.loader
19
19
  @loader ||= Zeitwerk::Loader.for_gem.tap do |loader|
20
20
  loader.ignore(
21
- "#{loader.dirs.first}/hanami/{constants,boot,errors,prepare,rake_tasks,setup}.rb"
21
+ "#{loader.dirs.first}/hanami/{constants,boot,errors,extensions/router/errors,prepare,rake_tasks,setup}.rb"
22
22
  )
23
23
  end
24
24
  end
@@ -144,8 +144,8 @@ module Hanami
144
144
  #
145
145
  # @api public
146
146
  # @since 2.0.0
147
- def self.env
148
- ENV.fetch("HANAMI_ENV") { ENV.fetch("RACK_ENV", "development") }.to_sym
147
+ def self.env(e: ENV)
148
+ e.fetch("HANAMI_ENV") { e.fetch("RACK_ENV", "development") }.to_sym
149
149
  end
150
150
 
151
151
  # Returns true if {.env} matches any of the given names
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe "App action / View rendering / View context", :app_integration do
4
+ subject(:context) {
5
+ # We capture the context during rendering via our view spies; see the view classes below
6
+ action.call("REQUEST_METHOD" => "GET", "QUERY_STRING" => "/mock_request")
7
+ action.view.called_with[:context]
8
+ }
9
+
10
+ before do
11
+ with_directory(make_tmp_directory) do
12
+ write "config/app.rb", <<~RUBY
13
+ module TestApp
14
+ class App < Hanami::App
15
+ end
16
+ end
17
+ RUBY
18
+
19
+ write "app/action.rb", <<~RUBY
20
+ # auto_register: false
21
+
22
+ require "hanami/view"
23
+
24
+ module TestApp
25
+ class Action < Hanami::Action
26
+ end
27
+ end
28
+ RUBY
29
+
30
+ before_prepare if respond_to?(:before_prepare)
31
+ require "hanami/prepare"
32
+ end
33
+ end
34
+
35
+ describe "app action" do
36
+ let(:action) { TestApp::App["actions.posts.show"] }
37
+
38
+ def before_prepare
39
+ write "app/actions/posts/show.rb", <<~RUBY
40
+ module TestApp
41
+ module Actions
42
+ module Posts
43
+ class Show < TestApp::Action
44
+ end
45
+ end
46
+ end
47
+ end
48
+ RUBY
49
+
50
+ # Custom view class as a spy for `#call` args
51
+ write "app/views/posts/show.rb", <<~RUBY
52
+ module TestApp
53
+ module Views
54
+ module Posts
55
+ class Show
56
+ attr_reader :called_with
57
+
58
+ def call(**args)
59
+ @called_with = args
60
+ ""
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ RUBY
67
+ end
68
+
69
+ context "no context class defined" do
70
+ it "defines and uses a context class" do
71
+ expect(context).to be_an_instance_of TestApp::Views::Context
72
+ expect(context.class).to be < Hanami::View::Context
73
+ end
74
+
75
+ it "includes the request" do
76
+ expect(context.request).to be_an_instance_of Hanami::Action::Request
77
+ expect(context.request.env["QUERY_STRING"]).to eq "/mock_request"
78
+ end
79
+ end
80
+
81
+ context "context class defined" do
82
+ def before_prepare
83
+ super()
84
+
85
+ write "app/views/context.rb", <<~RUBY
86
+ # auto_register: false
87
+
88
+ module TestApp
89
+ module Views
90
+ class Context < Hanami::View::Context
91
+ def concrete_app_context?
92
+ true
93
+ end
94
+ end
95
+ end
96
+ end
97
+ RUBY
98
+ end
99
+
100
+ it "uses the defined context class" do
101
+ expect(context).to be_an_instance_of TestApp::Views::Context
102
+ expect(context).to be_a_concrete_app_context
103
+ end
104
+ end
105
+
106
+ context "hanami-view not bundled" do
107
+ before do
108
+ allow(Hanami).to receive(:bundled?).and_call_original
109
+ expect(Hanami).to receive(:bundled?).with("hanami-view").and_return false
110
+ end
111
+
112
+ it "does not provide a context" do
113
+ expect(context).to be nil
114
+ end
115
+
116
+ context "context class defined" do
117
+ def before_prepare
118
+ super()
119
+
120
+ write "app/views/context.rb", <<~RUBY
121
+ module TestApp
122
+ module Views
123
+ class Context
124
+ def initialize(**)
125
+ end
126
+
127
+ def with(**)
128
+ self
129
+ end
130
+
131
+ def concrete_app_context?
132
+ true
133
+ end
134
+ end
135
+ end
136
+ end
137
+ RUBY
138
+ end
139
+
140
+ it "uses the defined context class" do
141
+ expect(context).to be_an_instance_of TestApp::Views::Context
142
+ expect(context).to be_a_concrete_app_context
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ describe "slice action" do
149
+ let(:action) { Main::Slice["actions.posts.show"] }
150
+
151
+ def before_prepare
152
+ write "slices/main/action.rb", <<~RUBY
153
+ module Main
154
+ class Action < TestApp::Action
155
+ end
156
+ end
157
+ RUBY
158
+
159
+ write "slices/main/actions/posts/show.rb", <<~RUBY
160
+ module Main
161
+ module Actions
162
+ module Posts
163
+ class Show < Main::Action
164
+ end
165
+ end
166
+ end
167
+ end
168
+ RUBY
169
+
170
+ # Custom view class as a spy for `#call` args
171
+ write "slices/main/views/posts/show.rb", <<~RUBY
172
+ module Main
173
+ module Views
174
+ module Posts
175
+ class Show
176
+ attr_reader :called_with
177
+
178
+ def call(**args)
179
+ @called_with = args
180
+ ""
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
186
+ RUBY
187
+ end
188
+
189
+ context "no context class defined" do
190
+ it "defines and uses a context class" do
191
+ expect(context).to be_an_instance_of Main::Views::Context
192
+ expect(context.class).to be < Hanami::View::Context
193
+ end
194
+ end
195
+
196
+ context "context class defined" do
197
+ def before_prepare
198
+ super()
199
+
200
+ write "slices/main/views/context.rb", <<~RUBY
201
+ # auto_register: false
202
+
203
+ module Main
204
+ module Views
205
+ class Context < Hanami::View::Context
206
+ def concrete_slice_context?
207
+ true
208
+ end
209
+ end
210
+ end
211
+ end
212
+ RUBY
213
+ end
214
+
215
+ it "uses the defined context class" do
216
+ expect(context).to be_an_instance_of Main::Views::Context
217
+ expect(context).to be_a_concrete_slice_context
218
+ end
219
+ end
220
+ end
221
+ end
@@ -32,24 +32,6 @@ RSpec.describe "App action / View rendering", :app_integration do
32
32
  end
33
33
  RUBY
34
34
 
35
- write "app/views/context.rb", <<~RUBY
36
- require "hanami/view/context"
37
-
38
- module TestApp
39
- module Views
40
- class Context < Hanami::View::Context
41
- def request
42
- _options.fetch(:request)
43
- end
44
-
45
- def response
46
- _options.fetch(:response)
47
- end
48
- end
49
- end
50
- end
51
- RUBY
52
-
53
35
  write "app/actions/users/show.rb", <<~RUBY
54
36
  module TestApp
55
37
  module Actions
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/test"
4
+ require "stringio"
5
+
6
+ RSpec.describe "Assets", :app_integration do
7
+ include Rack::Test::Methods
8
+ let(:app) { Hanami.app }
9
+ let(:root) { make_tmp_directory }
10
+
11
+ before do
12
+ with_directory(root) do
13
+ write "config/app.rb", <<~RUBY
14
+ module TestApp
15
+ class App < Hanami::App
16
+ config.logger.stream = StringIO.new
17
+ end
18
+ end
19
+ RUBY
20
+
21
+ write "config/routes.rb", <<~RUBY
22
+ module TestApp
23
+ class Routes < Hanami::Routes
24
+ get "posts/:id/edit", to: "posts.edit"
25
+ put "posts/:id", to: "posts.update"
26
+ end
27
+ end
28
+ RUBY
29
+
30
+ write "app/action.rb", <<~RUBY
31
+ # auto_register: false
32
+
33
+ require "hanami/action"
34
+
35
+ module TestApp
36
+ class Action < Hanami::Action
37
+ end
38
+ end
39
+ RUBY
40
+
41
+ write "app/view.rb", <<~RUBY
42
+ # auto_register: false
43
+
44
+ require "hanami/view"
45
+
46
+ module TestApp
47
+ class View < Hanami::View
48
+ config.layout = nil
49
+ end
50
+ end
51
+ RUBY
52
+
53
+ write "app/views/posts/show.rb", <<~RUBY
54
+ module TestApp
55
+ module Views
56
+ module Posts
57
+ class Show < TestApp::View
58
+ end
59
+ end
60
+ end
61
+ end
62
+ RUBY
63
+
64
+ write "app/templates/posts/show.html.erb", <<~ERB
65
+ <%= stylesheet_link_tag("app") %>
66
+ <%= css("app") %>
67
+ <%= javascript_tag("app") %>
68
+ <%= js("app") %>
69
+ ERB
70
+
71
+ write "app/assets/js/app.ts", <<~TS
72
+ import "../css/app.css";
73
+
74
+ console.log("Hello from index.ts");
75
+ TS
76
+
77
+ write "app/assets/css/app.css", <<~CSS
78
+ .btn {
79
+ background: #f00;
80
+ }
81
+ CSS
82
+
83
+ before_prepare if respond_to?(:before_prepare)
84
+ require "hanami/prepare"
85
+ end
86
+ end
87
+
88
+ specify "assets are available in helpers and in `assets` component" do
89
+ compile_assets!
90
+
91
+ output = Hanami.app["views.posts.show"].call.to_s
92
+
93
+ expect(output).to match(%r{<link href="/assets/app-[A-Z0-9]{8}.css" type="text/css" rel="stylesheet">})
94
+ expect(output).to match(%r{<script src="/assets/app-[A-Z0-9]{8}.js" type="text/javascript"></script>})
95
+
96
+ assets = Hanami.app["assets"]
97
+
98
+ expect(assets["app.css"].to_s).to match(%r{/assets/app-[A-Z0-9]{8}.css})
99
+ expect(assets["app.js"].to_s).to match(%r{/assets/app-[A-Z0-9]{8}.js})
100
+ end
101
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/test"
4
+ require "stringio"
5
+
6
+ RSpec.describe "Serve Static Assets", :app_integration do
7
+ include Rack::Test::Methods
8
+ let(:app) { Hanami.app }
9
+ let(:root) { make_tmp_directory }
10
+ let!(:env) { ENV.to_h }
11
+
12
+ before do
13
+ with_directory(root) do
14
+ write "config/app.rb", <<~RUBY
15
+ module TestApp
16
+ class App < Hanami::App
17
+ config.logger.stream = StringIO.new
18
+ end
19
+ end
20
+ RUBY
21
+
22
+ write "config/routes.rb", <<~RUBY
23
+ module TestApp
24
+ class Routes < Hanami::Routes
25
+ root to: ->(env) { [200, {}, ["Hello from root"]] }
26
+ end
27
+ end
28
+ RUBY
29
+
30
+ write "public/assets/app.js", <<~JS
31
+ console.log("Hello from app.js");
32
+ JS
33
+ end
34
+ end
35
+
36
+ after do
37
+ ENV.replace(env)
38
+ end
39
+
40
+ context "with default configuration" do
41
+ before do
42
+ with_directory(root) do
43
+ require "hanami/prepare"
44
+ end
45
+ end
46
+
47
+ it "serves static assets" do
48
+ get "/assets/app.js"
49
+
50
+ expect(last_response.status).to eq(200)
51
+ expect(last_response.body).to match(/Hello/)
52
+ end
53
+
54
+ it "returns 404 for missing asset" do
55
+ get "/assets/missing.js"
56
+
57
+ expect(last_response.status).to eq(404)
58
+ expect(last_response.body).to match(/Not Found/i)
59
+ end
60
+
61
+ it "doesn't escape from root directory" do
62
+ get "/assets/../../config/app.rb"
63
+
64
+ expect(last_response.status).to eq(404)
65
+ expect(last_response.body).to match(/Not Found/i)
66
+ end
67
+ end
68
+
69
+ context "when configuration is set to false" do
70
+ before do
71
+ with_directory(root) do
72
+ write "config/app.rb", <<~RUBY
73
+ module TestApp
74
+ class App < Hanami::App
75
+ config.logger.stream = StringIO.new
76
+ config.assets.serve = false
77
+ end
78
+ end
79
+ RUBY
80
+
81
+ require "hanami/boot"
82
+ end
83
+ end
84
+
85
+ it "doesn't serve static assets" do
86
+ get "/assets/app.js"
87
+
88
+ expect(last_response.status).to eq(404)
89
+ end
90
+ end
91
+
92
+ context "when env var is set to true" do
93
+ before do
94
+ with_directory(root) do
95
+ ENV["HANAMI_SERVE_ASSETS"] = "true"
96
+ require "hanami/boot"
97
+ end
98
+ end
99
+
100
+ it "serves static assets" do
101
+ get "/assets/app.js"
102
+
103
+ expect(last_response.status).to eq(200)
104
+ end
105
+ end
106
+
107
+ context "when env var is set to false" do
108
+ before do
109
+ with_directory(root) do
110
+ ENV["HANAMI_SERVE_ASSETS"] = "false"
111
+ require "hanami/boot"
112
+ end
113
+ end
114
+
115
+ it "doesn't serve static assets" do
116
+ get "/assets/app.js"
117
+
118
+ expect(last_response.status).to eq(404)
119
+ end
120
+ end
121
+
122
+ context "when Hanami.env is not :development or :test" do
123
+ before do
124
+ with_directory(root) do
125
+ ENV["HANAMI_ENV"] = "production"
126
+ require "hanami/boot"
127
+ end
128
+ end
129
+
130
+ it "doesn't serve static assets" do
131
+ get "/assets/app.js"
132
+
133
+ expect(last_response.status).to eq(404)
134
+ end
135
+ end
136
+
137
+ context "when Hanami.env is not :development or :test, but env var is set to true" do
138
+ before do
139
+ with_directory(root) do
140
+ ENV["HANAMI_ENV"] = "production"
141
+ ENV["HANAMI_SERVE_ASSETS"] = "true"
142
+ require "hanami/boot"
143
+ end
144
+ end
145
+
146
+ it "serves static assets" do
147
+ get "/assets/app.js"
148
+
149
+ expect(last_response.status).to eq(200)
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/test"
4
+ require "stringio"
5
+
6
+ RSpec.describe "Logging / Exception logging", :app_integration do
7
+ include Rack::Test::Methods
8
+
9
+ let(:app) { Hanami.app }
10
+
11
+ let(:logger_stream) { StringIO.new }
12
+
13
+ def configure_logger
14
+ Hanami.app.config.logger.stream = logger_stream
15
+ end
16
+
17
+ def logs
18
+ @logs ||= (logger_stream.rewind and logger_stream.read)
19
+ end
20
+
21
+ before do
22
+ with_directory(make_tmp_directory) do
23
+ write "config/app.rb", <<~RUBY
24
+ module TestApp
25
+ class App < Hanami::App
26
+ # Disable framework-level error rendering so we can test the raw action behavior
27
+ config.render_errors = false
28
+ config.render_detailed_errors = false
29
+ end
30
+ end
31
+ RUBY
32
+
33
+ write "config/routes.rb", <<~RUBY
34
+ module TestApp
35
+ class Routes < Hanami::Routes
36
+ root to: "test"
37
+ end
38
+ end
39
+ RUBY
40
+
41
+ require "hanami/setup"
42
+ configure_logger
43
+
44
+ before_prepare if respond_to?(:before_prepare)
45
+ require "hanami/prepare"
46
+ end
47
+ end
48
+
49
+ describe "unhandled exceptions" do
50
+ def before_prepare
51
+ write "app/actions/test.rb", <<~RUBY
52
+ module TestApp
53
+ module Actions
54
+ class Test < Hanami::Action
55
+ UnhandledError = Class.new(StandardError)
56
+
57
+ def handle(request, response)
58
+ raise UnhandledError, "unhandled"
59
+ end
60
+ end
61
+ end
62
+ end
63
+ RUBY
64
+ end
65
+
66
+ it "logs a 500 error and full exception details when an exception is raised" do
67
+ # Make the request with a rescue so the raised exception doesn't crash the tests
68
+ begin
69
+ get "/"
70
+ rescue TestApp::Actions::Test::UnhandledError # rubocop:disable Lint/SuppressedException
71
+ end
72
+
73
+ expect(logs.lines.length).to be > 10
74
+ expect(logs).to match %r{GET 500 \d+(µs|ms) 127.0.0.1 /}
75
+ expect(logs).to include("unhandled (TestApp::Actions::Test::UnhandledError)")
76
+ expect(logs).to include("app/actions/test.rb:7:in `handle'")
77
+ end
78
+
79
+ it "re-raises the exception" do
80
+ expect { get "/" }.to raise_error(TestApp::Actions::Test::UnhandledError)
81
+ end
82
+ end
83
+
84
+ describe "errors handled by handle_exception" do
85
+ def before_prepare
86
+ write "app/actions/test.rb", <<~RUBY
87
+ module TestApp
88
+ module Actions
89
+ class Test < Hanami::Action
90
+ NotFoundError = Class.new(StandardError)
91
+
92
+ handle_exception NotFoundError => :handle_not_found_error
93
+
94
+ def handle(request, response)
95
+ raise NotFoundError
96
+ end
97
+
98
+ private
99
+
100
+ def handle_not_found_error(request, response, exception)
101
+ halt 404
102
+ end
103
+ end
104
+ end
105
+ end
106
+ RUBY
107
+ end
108
+
109
+ it "does not log an error" do
110
+ get "/"
111
+
112
+ expect(logs).to match %r{GET 404 \d+(µs|ms) 127.0.0.1 /}
113
+ end
114
+ end
115
+ end