hanami 2.2.0 → 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 +47 -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/operation.rb +1 -1
- 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
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "uri"
|
4
4
|
require "hanami/view"
|
5
|
+
require_relative "../constants"
|
5
6
|
|
6
7
|
# rubocop:disable Metrics/ModuleLength
|
7
8
|
|
@@ -61,7 +62,10 @@ module Hanami
|
|
61
62
|
|
62
63
|
# @since 0.3.0
|
63
64
|
# @api private
|
64
|
-
|
65
|
+
# TODO: we can drop the defined?-check and fallback once Ruby 3.3 becomes our minimum required version
|
66
|
+
ABSOLUTE_URL_MATCHER = (
|
67
|
+
defined?(URI::RFC2396_PARSER) ? URI::RFC2396_PARSER : URI::DEFAULT_PARSER
|
68
|
+
).make_regexp
|
65
69
|
|
66
70
|
# @since 1.1.0
|
67
71
|
# @api private
|
@@ -85,7 +89,12 @@ module Hanami
|
|
85
89
|
# name of the algorithm, then a hyphen, then the hash value of the file.
|
86
90
|
# If more than one algorithm is used, they"ll be separated by a space.
|
87
91
|
#
|
88
|
-
#
|
92
|
+
# If the Content Security Policy uses 'nonce' and the source is not
|
93
|
+
# absolute, the nonce value of the current request is automatically added
|
94
|
+
# as an attribute. You can override this with the `nonce: false` option.
|
95
|
+
# See {#content_security_policy_nonce} for more.
|
96
|
+
#
|
97
|
+
# @param sources [Array<String, #url>] one or more assets by name or absolute URL
|
89
98
|
#
|
90
99
|
# @return [Hanami::View::HTML::SafeString] the markup
|
91
100
|
#
|
@@ -136,6 +145,10 @@ module Hanami
|
|
136
145
|
#
|
137
146
|
# # <script src="/assets/application.js" type="text/javascript" defer="defer"></script>
|
138
147
|
#
|
148
|
+
# @example Disable nonce
|
149
|
+
#
|
150
|
+
# <%= javascript_tag "application", nonce: false %>
|
151
|
+
#
|
139
152
|
# @example Absolute URL
|
140
153
|
#
|
141
154
|
# <%= javascript_tag "https://code.jquery.com/jquery-2.1.4.min.js" %>
|
@@ -154,13 +167,15 @@ module Hanami
|
|
154
167
|
#
|
155
168
|
# # <script src="https://assets.bookshelf.org/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.js"
|
156
169
|
# # type="text/javascript"></script>
|
157
|
-
def javascript_tag(*
|
170
|
+
def javascript_tag(*sources, **options)
|
158
171
|
options = options.reject { |k, _| k.to_sym == :src }
|
172
|
+
nonce_option = options.delete(:nonce)
|
159
173
|
|
160
|
-
_safe_tags(*
|
174
|
+
_safe_tags(*sources) do |source|
|
161
175
|
attributes = {
|
162
|
-
src:
|
163
|
-
type: JAVASCRIPT_MIME_TYPE
|
176
|
+
src: _typed_url(source, JAVASCRIPT_EXT),
|
177
|
+
type: JAVASCRIPT_MIME_TYPE,
|
178
|
+
nonce: _nonce(source, nonce_option)
|
164
179
|
}
|
165
180
|
attributes.merge!(options)
|
166
181
|
|
@@ -189,7 +204,12 @@ module Hanami
|
|
189
204
|
# name of the algorithm, then a hyphen, then the hashed value of the file.
|
190
205
|
# If more than one algorithm is used, they"ll be separated by a space.
|
191
206
|
#
|
192
|
-
#
|
207
|
+
# If the Content Security Policy uses 'nonce' and the source is not
|
208
|
+
# absolute, the nonce value of the current request is automatically added
|
209
|
+
# as an attribute. You can override this with the `nonce: false` option.
|
210
|
+
# See {#content_security_policy_nonce} for more.
|
211
|
+
#
|
212
|
+
# @param sources [Array<String, #url>] one or more assets by name or absolute URL
|
193
213
|
#
|
194
214
|
# @return [Hanami::View::HTML::SafeString] the markup
|
195
215
|
#
|
@@ -214,6 +234,10 @@ module Hanami
|
|
214
234
|
# # <link href="/assets/application.css" type="text/css" rel="stylesheet">
|
215
235
|
# # <link href="/assets/dashboard.css" type="text/css" rel="stylesheet">
|
216
236
|
#
|
237
|
+
# @example Disable nonce
|
238
|
+
#
|
239
|
+
# <%= stylesheet_tag "application", nonce: false %>
|
240
|
+
#
|
217
241
|
# @example Subresource Integrity
|
218
242
|
#
|
219
243
|
# <%= stylesheet_tag "application" %>
|
@@ -247,19 +271,21 @@ module Hanami
|
|
247
271
|
#
|
248
272
|
# # <link href="https://assets.bookshelf.org/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.css"
|
249
273
|
# # type="text/css" rel="stylesheet">
|
250
|
-
def stylesheet_tag(*
|
274
|
+
def stylesheet_tag(*sources, **options)
|
251
275
|
options = options.reject { |k, _| k.to_sym == :href }
|
276
|
+
nonce_option = options.delete(:nonce)
|
252
277
|
|
253
|
-
_safe_tags(*
|
278
|
+
_safe_tags(*sources) do |source|
|
254
279
|
attributes = {
|
255
|
-
href:
|
280
|
+
href: _typed_url(source, STYLESHEET_EXT),
|
256
281
|
type: STYLESHEET_MIME_TYPE,
|
257
|
-
rel: STYLESHEET_REL
|
282
|
+
rel: STYLESHEET_REL,
|
283
|
+
nonce: _nonce(source, nonce_option)
|
258
284
|
}
|
259
285
|
attributes.merge!(options)
|
260
286
|
|
261
287
|
if _context.assets.subresource_integrity? || attributes.include?(:integrity)
|
262
|
-
attributes[:integrity] ||= _subresource_integrity_value(
|
288
|
+
attributes[:integrity] ||= _subresource_integrity_value(source, STYLESHEET_EXT)
|
263
289
|
attributes[:crossorigin] ||= CROSSORIGIN_ANONYMOUS
|
264
290
|
end
|
265
291
|
|
@@ -626,7 +652,7 @@ module Hanami
|
|
626
652
|
#
|
627
653
|
# If CDN mode is on, it returns the absolute URL of the asset.
|
628
654
|
#
|
629
|
-
# @param
|
655
|
+
# @param source [String, #url] the asset name or asset object
|
630
656
|
#
|
631
657
|
# @return [String] the asset path
|
632
658
|
#
|
@@ -665,42 +691,71 @@ module Hanami
|
|
665
691
|
# <%= asset_url "application.js" %>
|
666
692
|
#
|
667
693
|
# # "https://assets.bookshelf.org/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.js"
|
668
|
-
def asset_url(
|
669
|
-
return
|
670
|
-
return
|
694
|
+
def asset_url(source)
|
695
|
+
return source.url if source.respond_to?(:url)
|
696
|
+
return source if _absolute_url?(source)
|
671
697
|
|
672
|
-
_context.assets[
|
698
|
+
_context.assets[source].url
|
699
|
+
end
|
700
|
+
|
701
|
+
# Random per request nonce value for Content Security Policy (CSP) rules.
|
702
|
+
#
|
703
|
+
# If the `Hanami::Middleware::ContentSecurityPolicyNonce` middleware is
|
704
|
+
# in use, this helper returns the nonce value for the current request
|
705
|
+
# or `nil` otherwise.
|
706
|
+
#
|
707
|
+
# For this policy to work in the browser, you have to add the `'nonce'`
|
708
|
+
# placeholder to the script and/or style source policy rule. It will be
|
709
|
+
# substituted by the current nonce value like `'nonce-A12OggyZ'.
|
710
|
+
#
|
711
|
+
# @return [String, nil] nonce value of the current request
|
712
|
+
#
|
713
|
+
# @since x.x.x
|
714
|
+
#
|
715
|
+
# @example App configuration
|
716
|
+
#
|
717
|
+
# config.middleware.use Hanami::Middleware::ContentSecurityPolicyNonce
|
718
|
+
# config.actions.content_security_policy[:script_src] = "'self' 'nonce'"
|
719
|
+
# config.actions.content_security_policy[:style_src] = "'self' 'nonce'"
|
720
|
+
#
|
721
|
+
# @example View helper
|
722
|
+
#
|
723
|
+
# <script nonce="<%= content_security_policy_nonce %>">
|
724
|
+
def content_security_policy_nonce
|
725
|
+
return unless _context.request?
|
726
|
+
|
727
|
+
_context.request.env[CONTENT_SECURITY_POLICY_NONCE_REQUEST_KEY]
|
673
728
|
end
|
674
729
|
|
675
730
|
private
|
676
731
|
|
677
732
|
# @since 2.1.0
|
678
733
|
# @api private
|
679
|
-
def _safe_tags(*
|
734
|
+
def _safe_tags(*sources, &blk)
|
680
735
|
::Hanami::View::HTML::SafeString.new(
|
681
|
-
|
736
|
+
sources.map(&blk).join(NEW_LINE_SEPARATOR)
|
682
737
|
)
|
683
738
|
end
|
684
739
|
|
685
740
|
# @since 2.1.0
|
686
741
|
# @api private
|
687
|
-
def
|
742
|
+
def _typed_url(source, ext)
|
688
743
|
source = "#{source}#{ext}" if source.is_a?(String) && _append_extension?(source, ext)
|
689
744
|
asset_url(source)
|
690
745
|
end
|
691
746
|
|
692
747
|
# @api private
|
693
|
-
def _subresource_integrity_value(
|
694
|
-
return if _absolute_url?(
|
748
|
+
def _subresource_integrity_value(source, ext)
|
749
|
+
return if _absolute_url?(source)
|
695
750
|
|
696
|
-
|
697
|
-
_context.assets[
|
751
|
+
source = "#{source}#{ext}" unless /#{Regexp.escape(ext)}\z/.match?(source)
|
752
|
+
_context.assets[source].sri
|
698
753
|
end
|
699
754
|
|
700
755
|
# @since 2.1.0
|
701
756
|
# @api private
|
702
757
|
def _absolute_url?(source)
|
703
|
-
ABSOLUTE_URL_MATCHER.match(source)
|
758
|
+
ABSOLUTE_URL_MATCHER.match?(source.respond_to?(:url) ? source.url : source)
|
704
759
|
end
|
705
760
|
|
706
761
|
# @since 1.2.0
|
@@ -711,6 +766,18 @@ module Hanami
|
|
711
766
|
_context.assets.crossorigin?(source)
|
712
767
|
end
|
713
768
|
|
769
|
+
# @since x.x.x
|
770
|
+
# @api private
|
771
|
+
def _nonce(source, nonce_option)
|
772
|
+
if nonce_option == false
|
773
|
+
nil
|
774
|
+
elsif nonce_option == true || (nonce_option.nil? && !_absolute_url?(source))
|
775
|
+
content_security_policy_nonce
|
776
|
+
else
|
777
|
+
nonce_option
|
778
|
+
end
|
779
|
+
end
|
780
|
+
|
714
781
|
# @since 2.1.0
|
715
782
|
# @api private
|
716
783
|
def _source_options(src, options, &blk)
|
@@ -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
|