hanami 2.2.1 → 2.3.0.beta1
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 +37 -1
- data/README.md +20 -35
- data/hanami.gemspec +3 -2
- 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 +13 -0
- data/lib/hanami/constants.rb +3 -0
- data/lib/hanami/extensions/db/repo.rb +11 -6
- data/lib/hanami/extensions/view/context.rb +10 -0
- data/lib/hanami/helpers/assets_helper.rb +92 -25
- data/lib/hanami/middleware/content_security_policy_nonce.rb +53 -0
- data/lib/hanami/slice.rb +22 -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/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/middleware_spec.rb +4 -11
- 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/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/version_spec.rb +1 -1
- data/spec/unit/hanami/web/rack_logger_spec.rb +11 -4
- metadata +27 -18
- 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
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rack"
|
4
|
+
require "securerandom"
|
5
|
+
require_relative "../constants"
|
6
|
+
|
7
|
+
module Hanami
|
8
|
+
module Middleware
|
9
|
+
# Generates a random per request nonce value like `mSMnSwfVZVe+LyQy1SPCGw==`, stores it as
|
10
|
+
# `"hanami.content_security_policy_nonce"` in the Rack environment, and replaces all occurrences
|
11
|
+
# of `'nonce'` in the `Content-Security-Policy header with the actual nonce value for the
|
12
|
+
# request, e.g. `'nonce-mSMnSwfVZVe+LyQy1SPCGw=='`.
|
13
|
+
#
|
14
|
+
# @see Hanami::Middleware::ContentSecurityPolicyNonce
|
15
|
+
#
|
16
|
+
# @api private
|
17
|
+
# @since x.x.x
|
18
|
+
class ContentSecurityPolicyNonce
|
19
|
+
# @api private
|
20
|
+
# @since x.x.x
|
21
|
+
def initialize(app)
|
22
|
+
@app = app
|
23
|
+
end
|
24
|
+
|
25
|
+
# @api private
|
26
|
+
# @since x.x.x
|
27
|
+
def call(env)
|
28
|
+
return @app.call(env) unless Hanami.app.config.actions.content_security_policy?
|
29
|
+
|
30
|
+
args = nonce_generator.arity == 1 ? [Rack::Request.new(env)] : []
|
31
|
+
request_nonce = nonce_generator.call(*args)
|
32
|
+
|
33
|
+
env[CONTENT_SECURITY_POLICY_NONCE_REQUEST_KEY] = request_nonce
|
34
|
+
|
35
|
+
_, headers, _ = response = @app.call(env)
|
36
|
+
|
37
|
+
headers["Content-Security-Policy"] = sub_nonce(headers["Content-Security-Policy"], request_nonce)
|
38
|
+
|
39
|
+
response
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def nonce_generator
|
45
|
+
Hanami.app.config.actions.content_security_policy_nonce_generator
|
46
|
+
end
|
47
|
+
|
48
|
+
def sub_nonce(string, nonce)
|
49
|
+
string&.gsub("'nonce'", "'nonce-#{nonce}'")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/hanami/slice.rb
CHANGED
@@ -729,7 +729,9 @@ module Hanami
|
|
729
729
|
# @api public
|
730
730
|
# @since 2.0.0
|
731
731
|
def routes
|
732
|
-
@routes
|
732
|
+
return @routes if instance_variable_defined?(:@routes)
|
733
|
+
|
734
|
+
@routes = load_routes
|
733
735
|
end
|
734
736
|
|
735
737
|
# Returns the slice's router, if or nil if no routes are defined.
|
@@ -925,7 +927,7 @@ module Hanami
|
|
925
927
|
# Check here for the `routes` definition only, not `router` itself, because the
|
926
928
|
# `router` requires the slice to be prepared before it can be loaded, and at this
|
927
929
|
# point we're still in the process of preparing.
|
928
|
-
if routes
|
930
|
+
if routes?
|
929
931
|
require_relative "providers/routes"
|
930
932
|
register_provider(:routes, source: Providers::Routes)
|
931
933
|
end
|
@@ -954,9 +956,10 @@ module Hanami
|
|
954
956
|
end
|
955
957
|
|
956
958
|
def prepare_autoloader
|
957
|
-
|
958
|
-
|
959
|
-
#
|
959
|
+
autoloader.tag = "hanami.slices.#{slice_name.to_s}"
|
960
|
+
|
961
|
+
# Component dirs are automatically pushed to the autoloader by dry-system's zeitwerk plugin.
|
962
|
+
# This method adds other dirs that are not otherwise configured as component dirs.
|
960
963
|
|
961
964
|
# Everything in the slice root can be autoloaded except `config/` and `slices/`,
|
962
965
|
# which are framework-managed directories
|
@@ -977,11 +980,19 @@ module Hanami
|
|
977
980
|
slices.freeze
|
978
981
|
end
|
979
982
|
|
983
|
+
def routes?
|
984
|
+
return false unless Hanami.bundled?("hanami-router")
|
985
|
+
|
986
|
+
return true if namespace.const_defined?(ROUTES_CLASS_NAME)
|
987
|
+
|
988
|
+
root.join("#{ROUTES_PATH}#{RB_EXT}").file?
|
989
|
+
end
|
990
|
+
|
980
991
|
def load_routes
|
981
992
|
return false unless Hanami.bundled?("hanami-router")
|
982
993
|
|
983
994
|
if root.directory?
|
984
|
-
routes_require_path =
|
995
|
+
routes_require_path = root.join(ROUTES_PATH).to_s
|
985
996
|
|
986
997
|
begin
|
987
998
|
require_relative "./routes"
|
@@ -1050,6 +1061,11 @@ module Hanami
|
|
1050
1061
|
if config.actions.sessions.enabled?
|
1051
1062
|
use(*config.actions.sessions.middleware)
|
1052
1063
|
end
|
1064
|
+
|
1065
|
+
if config.actions.content_security_policy && # rubocop:disable Style/SafeNavigation
|
1066
|
+
config.actions.content_security_policy.nonce?
|
1067
|
+
use(*config.actions.content_security_policy.middleware)
|
1068
|
+
end
|
1053
1069
|
end
|
1054
1070
|
|
1055
1071
|
if Hanami.bundled?("hanami-assets") && config.assets.serve
|
data/lib/hanami/version.rb
CHANGED
data/lib/hanami.rb
CHANGED
@@ -138,7 +138,15 @@ module Hanami
|
|
138
138
|
end
|
139
139
|
end
|
140
140
|
|
141
|
-
# Returns the Hanami app environment as
|
141
|
+
# Returns the Hanami app environment as determined from the environment.
|
142
|
+
#
|
143
|
+
# Checks the following environment variables in order:
|
144
|
+
#
|
145
|
+
# - `HANAMI_ENV`
|
146
|
+
# - `APP_ENV`
|
147
|
+
# - `RACK_ENV`
|
148
|
+
#
|
149
|
+
# Defaults to `:development` if no environment variable is set.
|
142
150
|
#
|
143
151
|
# @example
|
144
152
|
# Hanami.env # => :development
|
@@ -148,7 +156,7 @@ module Hanami
|
|
148
156
|
# @api public
|
149
157
|
# @since 2.0.0
|
150
158
|
def self.env(e: ENV)
|
151
|
-
e
|
159
|
+
(e["HANAMI_ENV"] || e["APP_ENV"] || e["RACK_ENV"] || :development).to_sym
|
152
160
|
end
|
153
161
|
|
154
162
|
# Returns true if {.env} matches any of the given names
|
@@ -123,7 +123,6 @@ RSpec.describe "Cross-slice assets via helpers", :app_integration do
|
|
123
123
|
compile_assets!
|
124
124
|
|
125
125
|
output = Admin::Slice["views.posts.show"].call.to_s
|
126
|
-
|
127
126
|
expect(output).to match(%r{<link href="/assets/app-[A-Z0-9]{8}.css" type="text/css" rel="stylesheet">})
|
128
127
|
expect(output).to match(%r{<script src="/assets/app-[A-Z0-9]{8}.js" type="text/javascript"></script>})
|
129
128
|
end
|
@@ -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
|
@@ -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"
|
@@ -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
|