hanami 2.2.1 → 2.3.0.beta2

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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -1
  3. data/README.md +20 -35
  4. data/hanami.gemspec +9 -8
  5. data/lib/hanami/app.rb +2 -0
  6. data/lib/hanami/config/actions/content_security_policy.rb +23 -0
  7. data/lib/hanami/config/actions.rb +21 -0
  8. data/lib/hanami/config/console.rb +79 -0
  9. data/lib/hanami/config/logger.rb +1 -1
  10. data/lib/hanami/config.rb +14 -1
  11. data/lib/hanami/constants.rb +3 -0
  12. data/lib/hanami/extensions/db/repo.rb +11 -6
  13. data/lib/hanami/extensions/view/context.rb +10 -10
  14. data/lib/hanami/extensions/view/slice_configured_context.rb +0 -7
  15. data/lib/hanami/helpers/assets_helper.rb +92 -25
  16. data/lib/hanami/middleware/content_security_policy_nonce.rb +53 -0
  17. data/lib/hanami/routes.rb +3 -3
  18. data/lib/hanami/slice.rb +34 -6
  19. data/lib/hanami/slice_registrar.rb +1 -1
  20. data/lib/hanami/version.rb +1 -1
  21. data/lib/hanami.rb +10 -2
  22. data/spec/integration/action/format_config_spec.rb +6 -3
  23. data/spec/integration/action/slice_configuration_spec.rb +36 -36
  24. data/spec/integration/assets/cross_slice_assets_helpers_spec.rb +0 -1
  25. data/spec/integration/assets/serve_static_assets_spec.rb +1 -1
  26. data/spec/integration/container/autoloader_spec.rb +2 -0
  27. data/spec/integration/db/db_spec.rb +1 -1
  28. data/spec/integration/db/logging_spec.rb +63 -0
  29. data/spec/integration/db/repo_spec.rb +87 -2
  30. data/spec/integration/logging/exception_logging_spec.rb +6 -1
  31. data/spec/integration/rack_app/body_parser_spec.rb +2 -1
  32. data/spec/integration/rack_app/middleware_spec.rb +4 -11
  33. data/spec/integration/rack_app/rack_app_spec.rb +2 -2
  34. data/spec/integration/view/helpers/form_helper_spec.rb +1 -1
  35. data/spec/integration/web/content_security_policy_nonce_spec.rb +251 -0
  36. data/spec/support/app_integration.rb +2 -1
  37. data/spec/unit/hanami/config/actions/content_security_policy_spec.rb +7 -0
  38. data/spec/unit/hanami/config/actions_spec.rb +2 -2
  39. data/spec/unit/hanami/config/console_spec.rb +22 -0
  40. data/spec/unit/hanami/env_spec.rb +10 -13
  41. data/spec/unit/hanami/slice_spec.rb +18 -0
  42. data/spec/unit/hanami/web/rack_logger_spec.rb +11 -4
  43. metadata +34 -29
  44. data/spec/integration/view/context/settings_spec.rb +0 -46
  45. data/spec/support/shared_examples/cli/generate/app.rb +0 -494
  46. data/spec/support/shared_examples/cli/generate/migration.rb +0 -32
  47. data/spec/support/shared_examples/cli/generate/model.rb +0 -81
  48. data/spec/support/shared_examples/cli/new.rb +0 -97
  49. data/spec/unit/hanami/version_spec.rb +0 -7
@@ -62,7 +62,7 @@ RSpec.describe "Serve Static Assets", :app_integration do
62
62
  get "/assets/../../config/app.rb"
63
63
 
64
64
  expect(last_response.status).to eq(404)
65
- expect(last_response.body).to match(/Not Found/i)
65
+ expect(last_response.body).to match("Hanami::Router::NotFoundError")
66
66
  end
67
67
  end
68
68
 
@@ -70,11 +70,13 @@ RSpec.describe "App autoloader", :app_integration do
70
70
  expect(NonApp::Thing).to be
71
71
 
72
72
  expect(TestApp::NBAJam::GetThatOuttaHere).to be
73
+ expect(TestApp::App.autoloader.tag).to eq("hanami.app.test_app")
73
74
 
74
75
  expect(Admin::Slice["operations.create_game"]).to be_an_instance_of(Admin::Operations::CreateGame)
75
76
  expect(Admin::Slice["operations.create_game"].call).to be_an_instance_of(Admin::Entities::Game)
76
77
 
77
78
  expect(Admin::Entities::Quarter).to be
79
+ expect(Admin::Slice.autoloader.tag).to eq("hanami.slices.admin")
78
80
  end
79
81
  end
80
82
  end
@@ -10,7 +10,7 @@ RSpec.describe "DB", :app_integration do
10
10
  ENV.replace(@env)
11
11
  end
12
12
 
13
- it "sets up ROM and reigsters relations" do
13
+ it "sets up ROM and registers relations" do
14
14
  with_tmp_directory(Dir.mktmpdir) do
15
15
  write "config/app.rb", <<~RUBY
16
16
  require "hanami"
@@ -235,4 +235,67 @@ RSpec.describe "DB / Logging", :app_integration do
235
235
  end
236
236
  end
237
237
  end
238
+
239
+ describe "in production" do
240
+ it "logs SQL queries" do
241
+ with_tmp_directory(Dir.mktmpdir) do
242
+ write "config/app.rb", <<~RUBY
243
+ require "hanami"
244
+
245
+ module TestApp
246
+ class App < Hanami::App
247
+ end
248
+ end
249
+ RUBY
250
+
251
+ write "app/relations/posts.rb", <<~RUBY
252
+ module TestApp
253
+ module Relations
254
+ class Posts < Hanami::DB::Relation
255
+ schema :posts, infer: true
256
+ end
257
+ end
258
+ end
259
+ RUBY
260
+
261
+ ENV["DATABASE_URL"] = "sqlite::memory"
262
+ ENV["HANAMI_ENV"] = "production"
263
+
264
+ require "hanami/setup"
265
+
266
+ logger_stream = StringIO.new
267
+ Hanami.app.config.logger.stream = logger_stream
268
+
269
+ require "hanami/prepare"
270
+
271
+ Hanami.app.prepare :db
272
+
273
+ # Manually run a migration and add a test record
274
+ gateway = Hanami.app["db.gateway"]
275
+ migration = gateway.migration do
276
+ change do
277
+ create_table :posts do
278
+ primary_key :id
279
+ column :title, :text, null: false
280
+ end
281
+
282
+ create_table :authors do
283
+ primary_key :id
284
+ end
285
+ end
286
+ end
287
+ migration.apply(gateway, :up)
288
+ gateway.connection.execute("INSERT INTO posts (title) VALUES ('Together breakfast')")
289
+
290
+ relation = Hanami.app["relations.posts"]
291
+ expect(relation.select(:title).to_a).to eq [{:title=>"Together breakfast"}]
292
+
293
+ logger_stream.rewind
294
+ log_lines = logger_stream.read.split("\n")
295
+
296
+ expect(log_lines.length).to eq 1
297
+ expect(log_lines.first).to match /Loaded :sqlite in \d+ms SELECT `posts`.`title` FROM `posts` ORDER BY `posts`.`id`/
298
+ end
299
+ end
300
+ end
238
301
  end
@@ -114,6 +114,13 @@ RSpec.describe "DB / Repo", :app_integration do
114
114
  end
115
115
  RUBY
116
116
 
117
+ write "app/repo.rb", <<~RUBY
118
+ module TestApp
119
+ class Repo < Hanami::DB::Repo
120
+ end
121
+ end
122
+ RUBY
123
+
117
124
  ENV["DATABASE_URL"] = "sqlite::memory"
118
125
 
119
126
  write "slices/admin/db/struct.rb", <<~RUBY
@@ -137,7 +144,7 @@ RSpec.describe "DB / Repo", :app_integration do
137
144
 
138
145
  write "slices/admin/repo.rb", <<~RUBY
139
146
  module Admin
140
- class Repo < Hanami::DB::Repo
147
+ class Repo < TestApp::Repo
141
148
  end
142
149
  end
143
150
  RUBY
@@ -172,7 +179,7 @@ RSpec.describe "DB / Repo", :app_integration do
172
179
 
173
180
  write "slices/main/repo.rb", <<~RUBY
174
181
  module Main
175
- class Repo < Hanami::DB::Repo
182
+ class Repo < TestApp::Repo
176
183
  end
177
184
  end
178
185
  RUBY
@@ -186,6 +193,15 @@ RSpec.describe "DB / Repo", :app_integration do
186
193
  end
187
194
  RUBY
188
195
 
196
+ write "slices/main/repositories/post_repository.rb", <<~RUBY
197
+ module Main
198
+ module Repositories
199
+ class PostRepository < Repo
200
+ end
201
+ end
202
+ end
203
+ RUBY
204
+
189
205
  require "hanami/prepare"
190
206
 
191
207
  Admin::Slice.prepare :db
@@ -204,12 +220,81 @@ RSpec.describe "DB / Repo", :app_integration do
204
220
  migration.apply(gateway, :up)
205
221
  gateway.connection.execute("INSERT INTO posts (title) VALUES ('Together breakfast')")
206
222
 
223
+ expect(Admin::Slice["repos.post_repo"].root).to eql Admin::Slice["relations.posts"]
207
224
  expect(Admin::Slice["repos.post_repo"].posts).to eql Admin::Slice["relations.posts"]
208
225
  expect(Admin::Slice["repos.post_repo"].posts.by_pk(1).one!.class).to be < Admin::Structs::Post
209
226
 
210
227
  expect(Main::Slice["repos.post_repo"].posts).to eql Main::Slice["relations.posts"]
228
+ expect(Main::Slice["repositories.post_repository"].posts).to eql Main::Slice["relations.posts"]
211
229
  # Slice struct namespace used even when no concrete struct classes are defined
212
230
  expect(Main::Slice["repos.post_repo"].posts.by_pk(1).one!.class).to be < Main::Structs::Post
213
231
  end
214
232
  end
233
+
234
+ specify "repos resolve registered container dependencies with Deps[]" do
235
+ with_tmp_directory(Dir.mktmpdir) do
236
+ write "config/app.rb", <<~RUBY
237
+ require "hanami"
238
+
239
+ module TestApp
240
+ class App < Hanami::App
241
+ Hanami.app.register("dependency") { -> { "Hello dependency!" } }
242
+ end
243
+ end
244
+ RUBY
245
+
246
+ write "app/repo.rb", <<~RUBY
247
+ module TestApp
248
+ class Repo < Hanami::DB::Repo
249
+ end
250
+ end
251
+ RUBY
252
+
253
+ write "app/relations/posts.rb", <<~RUBY
254
+ module TestApp
255
+ module Relations
256
+ class Posts < Hanami::DB::Relation
257
+ schema :posts, infer: true
258
+ end
259
+ end
260
+ end
261
+ RUBY
262
+
263
+ write "app/repos/post_repo.rb", <<~RUBY
264
+ module TestApp
265
+ module Repos
266
+ class PostRepo < Repo
267
+ include Deps["dependency"]
268
+
269
+ def test
270
+ dependency.call
271
+ end
272
+
273
+ end
274
+ end
275
+ end
276
+ RUBY
277
+
278
+ ENV["DATABASE_URL"] = "sqlite::memory"
279
+
280
+ require "hanami/prepare"
281
+
282
+ Hanami.app.prepare :db
283
+
284
+ # Manually run a migration and add a test record
285
+ gateway = Hanami.app["db.gateway"]
286
+ migration = gateway.migration do
287
+ change do
288
+ create_table :posts do
289
+ primary_key :id
290
+ column :title, :text, null: false
291
+ end
292
+ end
293
+ end
294
+ migration.apply(gateway, :up)
295
+
296
+ repo = Hanami.app["repos.post_repo"]
297
+ expect(repo.test).to eq "Hello dependency!"
298
+ end
299
+ end
215
300
  end
@@ -73,7 +73,12 @@ RSpec.describe "Logging / Exception logging", :app_integration do
73
73
  expect(logs.lines.length).to be > 10
74
74
  expect(logs).to match %r{GET 500 \d+(µs|ms) 127.0.0.1 /}
75
75
  expect(logs).to include("unhandled (TestApp::Actions::Test::UnhandledError)")
76
- expect(logs).to include("app/actions/test.rb:7:in `handle'")
76
+
77
+ if RUBY_VERSION < "3.4"
78
+ expect(logs).to include("app/actions/test.rb:7:in `handle'")
79
+ else
80
+ expect(logs).to include("app/actions/test.rb:7:in 'TestApp::Actions::Test#handle'")
81
+ end
77
82
  end
78
83
 
79
84
  it "re-raises the exception" do
@@ -65,7 +65,8 @@ RSpec.describe "Hanami web app", :app_integration do
65
65
 
66
66
  module TestApp
67
67
  class App < Hanami::App
68
- config.actions.formats.add :json, ["application/json+scim"]
68
+ config.actions.formats.register :json, "application/json+scim"
69
+ config.actions.formats.accept :json
69
70
  config.middleware.use :body_parser, [json: "application/json+scim"]
70
71
  config.logger.stream = StringIO.new
71
72
  end
@@ -507,7 +507,7 @@ RSpec.describe "Hanami web app", :app_integration do
507
507
  write "lib/test_app/middleware/api_version.rb", <<~RUBY
508
508
  module TestApp
509
509
  module Middleware
510
- class ApiVersion
510
+ class APIVersion
511
511
  def self.inspect
512
512
  "<Middleware::API::Version>"
513
513
  end
@@ -530,7 +530,7 @@ RSpec.describe "Hanami web app", :app_integration do
530
530
  write "lib/test_app/middleware/api_deprecation.rb", <<~RUBY
531
531
  module TestApp
532
532
  module Middleware
533
- class ApiDeprecation
533
+ class APIDeprecation
534
534
  def self.inspect
535
535
  "<Middleware::API::Deprecation>"
536
536
  end
@@ -578,13 +578,6 @@ RSpec.describe "Hanami web app", :app_integration do
578
578
  RUBY
579
579
 
580
580
  write "config/routes.rb", <<~RUBY
581
- require "test_app/middleware/elapsed"
582
- require "test_app/middleware/authentication"
583
- require "test_app/middleware/rate_limiter"
584
- require "test_app/middleware/api_version"
585
- require "test_app/middleware/api_deprecation"
586
- require "test_app/middleware/scope_identifier"
587
-
588
581
  module TestApp
589
582
  class Routes < Hanami::Routes
590
583
  use TestApp::Middleware::Elapsed
@@ -609,8 +602,8 @@ RSpec.describe "Hanami web app", :app_integration do
609
602
  root to: ->(*) { [200, {"Content-Length" => "3"}, ["API"]] }
610
603
 
611
604
  slice :api_v1, at: "/v1" do
612
- use TestApp::Middleware::ApiVersion
613
- use TestApp::Middleware::ApiDeprecation
605
+ use TestApp::Middleware::APIVersion
606
+ use TestApp::Middleware::APIDeprecation
614
607
  use TestApp::Middleware::ScopeIdentifier, "API-V1"
615
608
 
616
609
  root to: "home.show"
@@ -305,7 +305,7 @@ RSpec.describe "Hanami web app", :app_integration do
305
305
  expect { Hanami.app.rack_app }.to raise_error do |exception|
306
306
  expect(exception).to be_kind_of(Hanami::Routes::MissingActionError)
307
307
  expect(exception.message).to include("Could not find action with key \"actions.missing.action\" in TestApp::App")
308
- expect(exception.message).to match(%r{define the action class TestApp::Actions::Missing::Action.+actions/missing/action.rb})
308
+ expect(exception.message).to include("define the action class TestApp::Actions::Missing::Action in app/actions/missing/action.rb")
309
309
  end
310
310
  end
311
311
  end
@@ -337,7 +337,7 @@ RSpec.describe "Hanami web app", :app_integration do
337
337
  expect { Hanami.app.rack_app }.to raise_error do |exception|
338
338
  expect(exception).to be_kind_of(Hanami::Routes::MissingActionError)
339
339
  expect(exception.message).to include("Could not find action with key \"actions.missing.action\" in Admin::Slice")
340
- expect(exception.message).to match(%r{define the action class Admin::Actions::Missing::Action.+slices/admin/actions/missing/action.rb})
340
+ expect(exception.message).to include("define the action class Admin::Actions::Missing::Action in slices/admin/actions/missing/action.rb")
341
341
  end
342
342
  end
343
343
  end
@@ -156,7 +156,7 @@ RSpec.describe "Helpers / FormHelper", :app_integration do
156
156
  module TestApp
157
157
  class App < Hanami::App
158
158
  config.logger.stream = StringIO.new
159
- config.actions.sessions = :cookie, {secret: "xyz"}
159
+ config.actions.sessions = :cookie, {secret: "wxyz"*16} # Rack demands >=64 characters
160
160
  end
161
161
  end
162
162
  RUBY
@@ -0,0 +1,251 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/test"
4
+
5
+ RSpec.describe "Web / Content security policy nonce", :app_integration do
6
+ include Rack::Test::Methods
7
+
8
+ let(:app) { Hanami.app }
9
+
10
+ before do
11
+ with_directory(@dir = make_tmp_directory) do
12
+ write "config/routes.rb", <<~RUBY
13
+ module TestApp
14
+ class Routes < Hanami::Routes
15
+ get "index", to: "index"
16
+ end
17
+ end
18
+ RUBY
19
+
20
+ write "app/actions/index.rb", <<~RUBY
21
+ module TestApp
22
+ module Actions
23
+ class Index < Hanami::Action
24
+ end
25
+ end
26
+ end
27
+ RUBY
28
+
29
+ write "app/views/index.rb", <<~RUBY
30
+ module TestApp
31
+ module Views
32
+ class Index < Hanami::View
33
+ config.layout = false
34
+ end
35
+ end
36
+ end
37
+ RUBY
38
+
39
+ write "app/templates/index.html.erb", <<~HTML
40
+ <!DOCTYPE html>
41
+ <html lang="en">
42
+ <head>
43
+ <%= stylesheet_tag "app", class: "nonce-true", nonce: true %>
44
+ <%= stylesheet_tag "app", class: "nonce-false", nonce: false %>
45
+ <%= stylesheet_tag "app", class: "nonce-explicit", nonce: "explicit" %>
46
+ <%= stylesheet_tag "app", class: "nonce-generated" %>
47
+ <%= stylesheet_tag "https://example.com/app.css", class: "nonce-absolute" %>
48
+ </head>
49
+ <body>
50
+ <style nonce="<%= content_security_policy_nonce %>"></style>
51
+ <%= javascript_tag "app", class: "nonce-true", nonce: true %>
52
+ <%= javascript_tag "app", class: "nonce-false", nonce: false %>
53
+ <%= javascript_tag "app", class: "nonce-explicit", nonce: "explicit" %>
54
+ <%= javascript_tag "app", class: "nonce-generated" %>
55
+ <%= javascript_tag "https://example.com/app.js", class: "nonce-absolute" %>
56
+ </body>
57
+ </html>
58
+ HTML
59
+
60
+ write "package.json", <<~JSON
61
+ {
62
+ "type": "module"
63
+ }
64
+ JSON
65
+
66
+ write "config/assets.js", <<~JS
67
+ import * as assets from "hanami-assets";
68
+ await assets.run();
69
+ JS
70
+
71
+ write "app/assets/js/app.js", <<~JS
72
+ import "../css/app.css";
73
+ JS
74
+
75
+ write "app/assets/css/app.css", ""
76
+
77
+ before_prepare if respond_to?(:before_prepare)
78
+ require "hanami/prepare"
79
+ compile_assets!
80
+ end
81
+ end
82
+
83
+ describe "HTML request" do
84
+ context "CSP enabled" do
85
+ def before_prepare
86
+ write "config/app.rb", <<~RUBY
87
+ require "hanami"
88
+
89
+ module TestApp
90
+ class App < Hanami::App
91
+ config.actions.content_security_policy[:script_src] = "'self' 'nonce'"
92
+
93
+ config.logger.stream = File::NULL
94
+ end
95
+ end
96
+ RUBY
97
+ end
98
+
99
+ it "sets unique and per-request hanami.content_security_policy_nonce in Rack env" do
100
+ previous_nonces = []
101
+ 3.times do
102
+ get "/index"
103
+ nonce = last_request.env["hanami.content_security_policy_nonce"]
104
+
105
+ expect(nonce).to match(/\A[A-Za-z0-9\-_]{22}\z/)
106
+ expect(previous_nonces).not_to include nonce
107
+
108
+ previous_nonces << nonce
109
+ end
110
+ end
111
+
112
+ it "accepts custom nonce generator proc without arguments" do
113
+ Hanami.app.config.actions.content_security_policy_nonce_generator = -> { "foobar" }
114
+
115
+ get "/index"
116
+
117
+ expect(last_request.env["hanami.content_security_policy_nonce"]).to eql("foobar")
118
+ end
119
+
120
+ it "accepts custom nonce generator proc with Rack request as argument" do
121
+ Hanami.app.config.actions.content_security_policy_nonce_generator = ->(request) { request }
122
+
123
+ get "/index"
124
+
125
+ expect(last_request.env["hanami.content_security_policy_nonce"]).to be_a(Rack::Request)
126
+ end
127
+
128
+ it "substitutes 'nonce' in the CSP header" do
129
+ get "/index"
130
+ nonce = last_request.env["hanami.content_security_policy_nonce"]
131
+
132
+ expect(last_response.get_header("Content-Security-Policy")).to match(/script-src 'self' 'nonce-#{nonce}'/)
133
+ end
134
+
135
+ it "behaves the same with explicitly added middleware" do
136
+ Hanami.app.config.middleware.use Hanami::Middleware::ContentSecurityPolicyNonce
137
+ get "/index"
138
+
139
+ expect(last_request.env["hanami.content_security_policy_nonce"]).to match(/\A[A-Za-z0-9\-_]{22}\z/)
140
+ end
141
+
142
+ describe "content_security_policy_nonce" do
143
+ it "renders the current nonce" do
144
+ get "/index"
145
+ nonce = last_request.env["hanami.content_security_policy_nonce"]
146
+
147
+ expect(last_response.body).to include(%(<style nonce="#{nonce}">))
148
+ end
149
+ end
150
+
151
+ describe "stylesheet_tag" do
152
+ it "renders the correct nonce unless remote URL or nonce set to false" do
153
+ get "/index"
154
+ nonce = last_request.env["hanami.content_security_policy_nonce"]
155
+
156
+ expect(last_response.body).to include(%(<link href="/assets/app-KUHJPSX7.css" type="text/css" rel="stylesheet" nonce="#{nonce}" class="nonce-true">))
157
+ expect(last_response.body).to include(%(<link href="/assets/app-KUHJPSX7.css" type="text/css" rel="stylesheet" class="nonce-false">))
158
+ expect(last_response.body).to include(%(<link href="/assets/app-KUHJPSX7.css" type="text/css" rel="stylesheet" nonce="explicit" class="nonce-explicit">))
159
+ expect(last_response.body).to include(%(<link href="/assets/app-KUHJPSX7.css" type="text/css" rel="stylesheet" nonce="#{nonce}" class="nonce-generated">))
160
+ expect(last_response.body).to include(%(<link href="https://example.com/app.css" type="text/css" rel="stylesheet" class="nonce-absolute">))
161
+ end
162
+ end
163
+
164
+ describe "javascript_tag" do
165
+ it "renders the correct nonce unless remote URL or nonce set to false" do
166
+ get "/index"
167
+ nonce = last_request.env["hanami.content_security_policy_nonce"]
168
+
169
+ expect(last_response.body).to include(%(<script src="/assets/app-LSLFPUMX.js" type="text/javascript" nonce="#{nonce}" class="nonce-true"></script>))
170
+ expect(last_response.body).to include(%(<script src="/assets/app-LSLFPUMX.js" type="text/javascript" class="nonce-false"></script>))
171
+ expect(last_response.body).to include(%(<script src="/assets/app-LSLFPUMX.js" type="text/javascript" nonce="explicit" class="nonce-explicit"></script>))
172
+ expect(last_response.body).to include(%(<script src="/assets/app-LSLFPUMX.js" type="text/javascript" nonce="#{nonce}" class="nonce-generated"></script>))
173
+ expect(last_response.body).to include(%(<script src="https://example.com/app.js" type="text/javascript" class="nonce-absolute"></script>))
174
+ end
175
+ end
176
+ end
177
+
178
+ context "CSP disabled" do
179
+ def before_prepare
180
+ write "config/app.rb", <<~RUBY
181
+ require "hanami"
182
+
183
+ module TestApp
184
+ class App < Hanami::App
185
+ config.actions.content_security_policy = false
186
+
187
+ config.logger.stream = File::NULL
188
+ end
189
+ end
190
+ RUBY
191
+ end
192
+
193
+ it "does not set hanami.content_security_policy_nonce in Rack env" do
194
+ get "/index"
195
+
196
+ expect(last_request.env).to_not have_key "hanami.content_security_policy_nonce"
197
+ end
198
+
199
+ it "does not produce a CSP header" do
200
+ get "/index"
201
+
202
+ expect(last_response.headers).to_not have_key "Content-Security-Policy"
203
+ end
204
+
205
+ it "disables the content_security_policy_nonce helper" do
206
+ get "/index"
207
+
208
+ expect(last_response.body).to match(/<style nonce="">/)
209
+ end
210
+
211
+ it "behaves the same with explicitly added middleware" do
212
+ Hanami.app.config.middleware.use Hanami::Middleware::ContentSecurityPolicyNonce
213
+ get "/index"
214
+
215
+ expect(last_response.headers).to_not have_key "Content-Security-Policy"
216
+ end
217
+
218
+ describe "content_security_policy_nonce" do
219
+ it "renders nothing" do
220
+ get "/index"
221
+
222
+ expect(last_response.body).to include(%(<style nonce="">))
223
+ end
224
+ end
225
+
226
+ describe "stylesheet_tag" do
227
+ it "renders the correct explicit nonce only" do
228
+ get "/index"
229
+
230
+ expect(last_response.body).to include(%(<link href="/assets/app-KUHJPSX7.css" type="text/css" rel="stylesheet" class="nonce-true">))
231
+ expect(last_response.body).to include(%(<link href="/assets/app-KUHJPSX7.css" type="text/css" rel="stylesheet" class="nonce-false">))
232
+ expect(last_response.body).to include(%(<link href="/assets/app-KUHJPSX7.css" type="text/css" rel="stylesheet" nonce="explicit" class="nonce-explicit">))
233
+ expect(last_response.body).to include(%(<link href="/assets/app-KUHJPSX7.css" type="text/css" rel="stylesheet" class="nonce-generated">))
234
+ expect(last_response.body).to include(%(<link href="https://example.com/app.css" type="text/css" rel="stylesheet" class="nonce-absolute">))
235
+ end
236
+ end
237
+
238
+ describe "javascript_tag" do
239
+ it "renders the correct explicit nonce only" do
240
+ get "/index"
241
+
242
+ expect(last_response.body).to include(%(<script src="/assets/app-LSLFPUMX.js" type="text/javascript" class="nonce-true"></script>))
243
+ expect(last_response.body).to include(%(<script src="/assets/app-LSLFPUMX.js" type="text/javascript" class="nonce-false"></script>))
244
+ expect(last_response.body).to include(%(<script src="/assets/app-LSLFPUMX.js" type="text/javascript" nonce="explicit" class="nonce-explicit"></script>))
245
+ expect(last_response.body).to include(%(<script src="/assets/app-LSLFPUMX.js" type="text/javascript" class="nonce-generated"></script>))
246
+ expect(last_response.body).to include(%(<script src="https://example.com/app.js" type="text/javascript" class="nonce-absolute"></script>))
247
+ end
248
+ end
249
+ end
250
+ end
251
+ end
@@ -118,7 +118,8 @@ RSpec.configure do |config|
118
118
  Hanami.instance_variable_set(:@_bundled, {})
119
119
  Hanami.remove_instance_variable(:@_app) if Hanami.instance_variable_defined?(:@_app)
120
120
 
121
- # Clear cached DB gateways across slices
121
+ # Disconnect and clear cached DB gateways across slices
122
+ Hanami::Providers::DB.cache.values.map(&:disconnect)
122
123
  Hanami::Providers::DB.cache.clear
123
124
 
124
125
  $LOAD_PATH.replace(@load_paths)
@@ -28,6 +28,7 @@ RSpec.describe Hanami::Config::Actions, "#content_security_policy" do
28
28
  ].join(";")
29
29
 
30
30
  expect(content_security_policy.to_s).to eq(expected)
31
+ expect(content_security_policy.nonce?).to be(false)
31
32
  end
32
33
  end
33
34
 
@@ -68,6 +69,12 @@ RSpec.describe Hanami::Config::Actions, "#content_security_policy" do
68
69
  expect(content_security_policy[:a_custom_key]).to eq("foo")
69
70
  expect(content_security_policy.to_s).to match("a-custom-key foo")
70
71
  end
72
+
73
+ it "uses 'nonce' in value" do
74
+ content_security_policy[:javascript_src] = "'self' 'nonce'"
75
+
76
+ expect(content_security_policy.nonce?).to be(true)
77
+ end
71
78
  end
72
79
 
73
80
  context "with CSP enabled" do
@@ -23,9 +23,9 @@ RSpec.describe Hanami::Config, "#actions" do
23
23
  end
24
24
 
25
25
  it "configures base actions settings using custom methods" do
26
- expect { actions.formats.add(:json, "app/json") }
26
+ expect { actions.formats.register(:json, "app/json") }
27
27
  .to change { actions.formats.mapping }
28
- .to include("app/json" => :json)
28
+ .to include(json: have_attributes(media_type: "app/json"))
29
29
  end
30
30
 
31
31
  it "can be finalized" do
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/config"
4
+
5
+ RSpec.describe Hanami::Config, "#console" do
6
+ let(:config) { described_class.new(app_name: app_name, env: :development) }
7
+ let(:app_name) { "MyApp::App" }
8
+
9
+ subject(:console) { config.console }
10
+
11
+ it "is a full console configuration" do
12
+ is_expected.to be_an_instance_of(Hanami::Config::Console)
13
+
14
+ is_expected.to respond_to(:engine)
15
+ is_expected.to respond_to(:include)
16
+ is_expected.to respond_to(:extensions)
17
+ end
18
+
19
+ it "can be finalized" do
20
+ is_expected.to respond_to(:finalize!)
21
+ end
22
+ end