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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +49 -1
- data/README.md +20 -35
- data/hanami.gemspec +9 -8
- data/lib/hanami/app.rb +2 -0
- data/lib/hanami/config/actions/content_security_policy.rb +23 -0
- data/lib/hanami/config/actions.rb +21 -0
- data/lib/hanami/config/console.rb +79 -0
- data/lib/hanami/config/logger.rb +1 -1
- data/lib/hanami/config.rb +14 -1
- data/lib/hanami/constants.rb +3 -0
- data/lib/hanami/extensions/db/repo.rb +11 -6
- data/lib/hanami/extensions/view/context.rb +10 -10
- data/lib/hanami/extensions/view/slice_configured_context.rb +0 -7
- data/lib/hanami/helpers/assets_helper.rb +92 -25
- data/lib/hanami/middleware/content_security_policy_nonce.rb +53 -0
- data/lib/hanami/routes.rb +3 -3
- data/lib/hanami/slice.rb +34 -6
- data/lib/hanami/slice_registrar.rb +1 -1
- data/lib/hanami/version.rb +1 -1
- data/lib/hanami.rb +10 -2
- data/spec/integration/action/format_config_spec.rb +6 -3
- data/spec/integration/action/slice_configuration_spec.rb +36 -36
- data/spec/integration/assets/cross_slice_assets_helpers_spec.rb +0 -1
- data/spec/integration/assets/serve_static_assets_spec.rb +1 -1
- data/spec/integration/container/autoloader_spec.rb +2 -0
- data/spec/integration/db/db_spec.rb +1 -1
- data/spec/integration/db/logging_spec.rb +63 -0
- data/spec/integration/db/repo_spec.rb +87 -2
- data/spec/integration/logging/exception_logging_spec.rb +6 -1
- data/spec/integration/rack_app/body_parser_spec.rb +2 -1
- data/spec/integration/rack_app/middleware_spec.rb +4 -11
- data/spec/integration/rack_app/rack_app_spec.rb +2 -2
- data/spec/integration/view/helpers/form_helper_spec.rb +1 -1
- data/spec/integration/web/content_security_policy_nonce_spec.rb +251 -0
- data/spec/support/app_integration.rb +2 -1
- data/spec/unit/hanami/config/actions/content_security_policy_spec.rb +7 -0
- data/spec/unit/hanami/config/actions_spec.rb +2 -2
- data/spec/unit/hanami/config/console_spec.rb +22 -0
- data/spec/unit/hanami/env_spec.rb +10 -13
- data/spec/unit/hanami/slice_spec.rb +18 -0
- data/spec/unit/hanami/web/rack_logger_spec.rb +11 -4
- metadata +34 -29
- data/spec/integration/view/context/settings_spec.rb +0 -46
- data/spec/support/shared_examples/cli/generate/app.rb +0 -494
- data/spec/support/shared_examples/cli/generate/migration.rb +0 -32
- data/spec/support/shared_examples/cli/generate/model.rb +0 -81
- data/spec/support/shared_examples/cli/new.rb +0 -97
- 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(
|
|
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
|
|
@@ -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 <
|
|
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 <
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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::
|
|
613
|
-
use TestApp::Middleware::
|
|
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
|
|
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
|
|
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: "
|
|
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
|
-
#
|
|
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.
|
|
26
|
+
expect { actions.formats.register(:json, "app/json") }
|
|
27
27
|
.to change { actions.formats.mapping }
|
|
28
|
-
.to include("app/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
|